Page MenuHomePhorge

No OneTemporary

Size
424 KB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 57da4032..7eb9dee4 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -1,2391 +1,2424 @@
<?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) 2011, 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;
public $task = '?(?!logout).*';
public $rc;
public $driver;
public $home; // declare public to be used in other classes
public $urlbase;
public $timezone;
public $timezone_offset;
public $gmt_offset;
public $ical;
public $ui;
public $defaults = array(
'calendar_default_view' => "agendaWeek",
'calendar_date_format' => "yyyy-MM-dd",
'calendar_date_short' => "M-d",
'calendar_date_long' => "MMM d yyyy",
'calendar_date_agenda' => "ddd MM-dd",
'calendar_time_format' => "HH:mm",
'calendar_timeslots' => 2,
'calendar_first_day' => 1,
'calendar_first_hour' => 6,
'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_date_format_sets' => array(
'yyyy-MM-dd' => array('MMM d yyyy', 'M-d', 'ddd MM-dd'),
'dd-MM-yyyy' => array('d MMM yyyy', 'd-M', 'ddd dd-MM'),
'yyyy/MM/dd' => array('MMM d yyyy', 'M/d', 'ddd MM/dd'),
'MM/dd/yyyy' => array('MMM d yyyy', 'M/d', 'ddd MM/dd'),
'dd/MM/yyyy' => array('d MMM yyyy', 'd/M', 'ddd dd/MM'),
'dd.MM.yyyy' => array('dd. MMM yyyy', 'd.M', 'ddd dd.MM.'),
'd.M.yyyy' => array('d. MMM yyyy', 'd.M', 'ddd d.MM.'),
),
);
private $default_categories = array(
'Personal' => 'c0c0c0',
'Work' => 'ff0000',
'Family' => '00ff00',
'Holiday' => 'ff6600',
);
private $ics_parts = array();
/**
* Plugin initialization.
*/
function init()
{
$this->rc = rcmail::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'));
// set user's timezone
$this->timezone = new DateTimeZone($this->rc->config->get('timezone', 'GMT'));
$now = new DateTime('now', $this->timezone);
$this->timezone_offset = $now->format('Z') / 3600;
$this->dst_active = $now->format('I');
$this->gmt_offset = $now->getOffset();
require($this->home . '/lib/calendar_ui.php');
$this->ui = new calendar_ui($this);
// load Calendar user interface which includes jquery-ui
if (!$this->rc->output->ajax_call && !$this->rc->output->env['framed']) {
$this->require_plugin('jqueryui');
$this->ui->init();
// settings are required in (almost) every GUI step
if ($this->rc->action != 'attend')
$this->rc->output->set_env('calendar_settings', $this->load_settings());
}
// 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 if ($this->rc->task == 'calendar' && $this->rc->action != 'save-pref') {
if ($this->rc->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('mailimportevent', array($this, 'mail_import_event'));
$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'));
// 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 ($this->rc->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 ($this->rc->task == 'mail') {
// hooks to catch event invitations on incoming mails
if ($this->rc->action == 'show' || $this->rc->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');
}
}
// add hook to display alarms
$this->add_hook('keep_alive', array($this, 'keep_alive'));
}
/**
* 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');
switch ($driver_name) {
case "kolab":
$this->require_plugin('kolab_core');
default:
$this->driver = new $driver_class($this);
break;
}
}
/**
* 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) {
require_once($this->home . '/lib/calendar_ical.php');
$this->ical = new calendar_ical($this);
}
return $this->ical;
}
+ /**
+ *
+ */
+ public function get_default_calendar($writeable = false)
+ {
+ $cal_id = $this->rc->config->get('calendar_default_calendar');
+ $calendars = $this->driver->list_calendars();
+ $calendar = $calendars[$cal_id] ? $calendars[$cal_id] : null;
+ if (!$calendar || ($writeable && $calendar['readonly'])) {
+ foreach ($calendars as $cal) {
+ if (!$writeable || !$cal['readonly']) {
+ $calendar = $cal;
+ break;
+ }
+ }
+ }
+
+ return $calendar;
+ }
+
/**
* 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');
// initialize attendees autocompletion
rcube_autocomplete_init();
$this->rc->output->set_env('calendar_driver', $this->rc->config->get('calendar_driver'), false);
$this->rc->output->set_env('mscolors', $this->driver->get_color_values());
$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') {
$this->load_driver();
$p['blocks']['view']['name'] = $this->gettext('mainoptions');
$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'])),
);
$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($this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots'])),
);
$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($this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day'])),
);
$time_format = $this->rc->config->get('time_format', self::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)),
);
$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)),
);
$field_id = 'rcmfd_coloing';
$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'])),
);
$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($this->gettext(strtolower("alarm{$type}option")), $type);
$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($this->gettext('trigger' . $trigger), $trigger);
$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', '')),
);
$preset = self::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]),
);
// default calendar selection
$field_id = 'rcmfd_default_calendar';
$select_cal = new html_select(array('name' => '_default_calendar', 'id' => $field_id));
foreach ((array)$this->driver->list_calendars() as $id => $prop) {
if (!$prop['readonly'])
$select_cal->add($prop['name'], strval($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', '')),
);
// category definitions
if (!$this->driver->nocategories) {
$p['blocks']['categories']['name'] = $this->gettext('categories');
$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:mscolors });
}
}');
// include color picker
$this->include_script('lib/js/jquery.miniColors.min.js');
$this->include_stylesheet('skins/' .$this->rc->config->get('skin') . '/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');
}
}
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_alam = $alarm_offset[0] . intval(get_input_value('_alarm_value', RCUBE_INPUT_POST)) . $alarm_offset[1];
$p['prefs'] = array(
'calendar_default_view' => get_input_value('_default_view', RCUBE_INPUT_POST),
'calendar_timeslots' => get_input_value('_timeslots', RCUBE_INPUT_POST),
'calendar_first_day' => 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_alam,
'calendar_default_calendar' => get_input_value('_default_calendar', RCUBE_INPUT_POST),
'calendar_date_format' => null, // clear previously saved values
'calendar_time_format' => 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 = get_input_value('_categories', RCUBE_INPUT_POST);
$colors = 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');
// TODO: keep view and date selection
if ($success && $reload)
$this->rc->output->redirect('');
}
/**
* 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($attendee['email'], $emails)) {
$old['attendees'][$i]['status'] = 'DECLINED';
}
}
$itip = $this->load_itip();
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-status":
$action = 'rsvp';
$status = $event['fallback'];
$html = html::div('rsvp-status', $status != 'CANCELLED' ? $this->gettext('acceptinvitation') : '');
$this->load_driver();
if ($existing = $this->driver->get_event($event, true)) {
$emails = $this->get_user_emails();
foreach ($existing['attendees'] as $i => $attendee) {
if ($attendee['email'] && in_array($attendee['email'], $emails)) {
$status = $attendee['status'];
break;
}
}
}
else {
// get a list of writeable calendars
$calendars = $this->driver->list_calendars();
$calendar_select = new html_select(array('name' => 'calendar', 'id' => 'calendar-saveto'));
$numcals = 0;
foreach ($calendars as $calendar) {
if (!$calendar['readonly']) {
$calendar_select->add($calendar['name'], $calendar['id']);
$numcals++;
}
}
if ($numcals <= 1)
$calendar_select = null;
}
if ($status == 'unknown') {
$html = html::div('rsvp-status', $this->gettext('notanattendee'));
$action = 'import';
}
else if (in_array($status, array('ACCEPTED','TENTATIVE','DECLINED'))) {
$html = html::div('rsvp-status ' . strtolower($status), $this->gettext('youhave'.strtolower($status)));
if ($existing['changed'] && $event['changed'] < $existing['changed']) {
$action = '';
}
}
$this->rc->output->command('plugin.update_event_rsvp_status', array(
'uid' => $event['uid'],
'id' => asciiwords($event['uid'], true),
'status' => $status,
'action' => $action,
'html' => $html,
'select' => $calendar_select ? html::span('calendar-select', $this->gettext('saveincalendar') . '&nbsp;' . $calendar_select->show($this->rc->config->get('calendar_default_calendar'))) : '',
));
return;
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":
foreach (explode(',', $event['id']) as $id)
$success |= $this->driver->dismiss_alarm($id, $event['snooze']);
break;
}
// 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)) {
if ($this->notify_attendees($event, $old, $action) < 0)
$this->rc->output->show_message('calendar.errornotifying', 'error');
}
}
// 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');
}
// 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 pending notifications and pass them to the client
*/
function keep_alive($attr)
{
$this->load_driver();
$alarms = $this->driver->pending_alarms(time());
if ($alarms) {
// make sure texts and env vars are available on client
if ($this->rc->task != 'calendar') {
$this->add_texts('localization/', true);
$this->rc->output->set_env('snooze_select', $this->ui->snooze_select());
}
$this->rc->output->command('plugin.display_alarms', $this->_alarms_output($alarms));
}
}
/**
* Handler for check-recent requests which are accidentally sent to calendar taks
*/
function check_recent()
{
// NOP
$this->rc->output->send();
}
/**
*
*/
function import_events()
{
// Upload progress update
if (!empty($_GET['_progress'])) {
rcube_upload_progress();
}
$calendar = get_input_value('calendar', RCUBE_INPUT_GPC);
$uploadid = get_input_value('_uploadid', RCUBE_INPUT_GPC);
// process uploaded file if there is no error
$err = $_FILES['_data']['error'];
if (!$err && $_FILES['_data']['tmp_name']) {
$calendar = get_input_value('calendar', RCUBE_INPUT_GPC);
$events = $this->get_ical()->import_from_file($_FILES['_data']['tmp_name']);
$count = $errors = 0;
$rangestart = $_REQUEST['_range'] ? strtotime("now -" . intval($_REQUEST['_range']) . " months") : 0;
foreach ($events as $event) {
// TODO: correctly handle recurring events which start before $rangestart
if ($event['end'] < $rangestart && (!$event['recurrence'] || ($event['recurrence']['until'] && $event['recurrence']['until'] < $rangestart)))
continue;
$event['calendar'] = $calendar;
if ($success = $this->driver->new_event($event)) {
$count++;
}
else
$errors++;
}
if ($count) {
$this->rc->output->command('display_message', $this->gettext(array('name' => 'importsuccess', 'vars' => array('nr' => $count))), 'confirmation');
$this->rc->output->command('plugin.import_success', array('source' => $calendar, 'refetch' => true));
}
else if (!$errors) {
$this->rc->output->command('display_message', $this->gettext('importnone'), 'notice');
$this->rc->output->command('plugin.import_success', array('source' => $calendar));
}
else
$this->rc->output->command('display_message', $this->gettext('importerror'), 'error');
}
else {
if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
$msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array(
'size' => show_bytes(parse_bytes(ini_get('upload_max_filesize'))))));
}
else {
$msg = rcube_label('fileuploaderror');
}
$this->rc->output->command('display_message', $msg, 'error');
$this->rc->output->command('plugin.unlock_saving', false);
}
$this->rc->output->send('iframe');
}
/**
* Construct the ics file for exporting events to iCalendar format;
*/
function export_events($terminate = true)
{
$start = get_input_value('start', RCUBE_INPUT_GET);
$end = get_input_value('end', RCUBE_INPUT_GET);
if (!$start) $start = mktime(0, 0, 0, 1, date('n'), date('Y')-1);
if (!$end) $end = mktime(0, 0, 0, 31, 12, date('Y')+10);
$calid = $calname = get_input_value('source', RCUBE_INPUT_GET);
$calendars = $this->driver->list_calendars();
if ($calendars[$calid]) {
$calname = $calendars[$calid]['name'] ? $calendars[$calid]['name'] : $calid;
$events = $this->driver->load_events($start, $end, null, $calid, 0);
}
else
$events = array();
header("Content-Type: text/calendar");
header("Content-Disposition: inline; filename=".$calname.'.ics');
$this->get_ical()->export($events, '', true);
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->date_format_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_format'] = (string)$this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']);
$settings['time_format'] = (string)$this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format']);
$settings['date_short'] = (string)$this->rc->config->get('calendar_date_short', $this->defaults['calendar_date_short']);
$settings['date_long'] = (string)$this->rc->config->get('calendar_date_long', $this->defaults['calendar_date_long']);
$settings['dates_long'] = str_replace(' yyyy', '[ yyyy]', $settings['date_long']) . "{ '&mdash;' " . $settings['date_long'] . '}';
$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['timezone'] = $this->timezone_offset;
$settings['dst'] = $this->dst_active;
// localization
$settings['days'] = array(
rcube_label('sunday'), rcube_label('monday'),
rcube_label('tuesday'), rcube_label('wednesday'),
rcube_label('thursday'), rcube_label('friday'),
rcube_label('saturday')
);
$settings['days_short'] = array(
rcube_label('sun'), rcube_label('mon'),
rcube_label('tue'), rcube_label('wed'),
rcube_label('thu'), rcube_label('fri'),
rcube_label('sat')
);
$settings['months'] = array(
$this->rc->gettext('longjan'), $this->rc->gettext('longfeb'),
$this->rc->gettext('longmar'), $this->rc->gettext('longapr'),
$this->rc->gettext('longmay'), $this->rc->gettext('longjun'),
$this->rc->gettext('longjul'), $this->rc->gettext('longaug'),
$this->rc->gettext('longsep'), $this->rc->gettext('longoct'),
$this->rc->gettext('longnov'), $this->rc->gettext('longdec')
);
$settings['months_short'] = array(
$this->rc->gettext('jan'), $this->rc->gettext('feb'),
$this->rc->gettext('mar'), $this->rc->gettext('apr'),
$this->rc->gettext('may'), $this->rc->gettext('jun'),
$this->rc->gettext('jul'), $this->rc->gettext('aug'),
$this->rc->gettext('sep'), $this->rc->gettext('oct'),
$this->rc->gettext('nov'), $this->rc->gettext('dec')
);
$settings['today'] = $this->rc->gettext('today');
// 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'];
}
$identity['emails'][] = $this->rc->user->get_username();
$settings['identity'] = array('name' => $identity['name'], 'email' => $identity['email'], 'emails' => ';' . join(';', $identity['emails']));
}
return $settings;
}
/**
* Helper function to set date/time format according to config and user preferences
*/
private function date_format_defaults()
{
static $defaults = array();
// nothing to be done
if (isset($defaults['date_format']))
return;
$defaults['date_format'] = $this->rc->config->get('calendar_date_format', self::from_php_date_format($this->rc->config->get('date_format')));
$defaults['time_format'] = $this->rc->config->get('calendar_time_format', self::from_php_date_format($this->rc->config->get('time_format')));
// override defaults
if ($defaults['date_format'])
$this->defaults['calendar_date_format'] = $defaults['date_format'];
if ($defaults['time_format'])
$this->defaults['calendar_time_format'] = $defaults['time_format'];
// derive format variants from basic date format
$format_sets = $this->rc->config->get('calendar_date_format_sets', $this->defaults['calendar_date_format_sets']);
if ($format_set = $format_sets[$this->defaults['calendar_date_format']]) {
$this->defaults['calendar_date_long'] = $format_set[0];
$this->defaults['calendar_date_short'] = $format_set[1];
$this->defaults['calendar_date_agenda'] = $format_set[2];
}
}
/**
* Convert the given date string into a GMT-based time stamp
*/
function fromGMT($datetime)
{
$ts = is_numeric($datetime) ? $datetime : strtotime($datetime);
return $ts + $this->gmt_offset;
}
/**
* 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'] = $this->_alarms_text($event['alarms']);
if ($event['recurrence'])
$event['recurrence_text'] = $this->_recurrence_text($event['recurrence']);
foreach ((array)$event['attachments'] as $k => $attachment) {
$event['attachments'][$k]['classname'] = rcmail_filetype2classname($attachment['mimetype'], $attachment['name']);
}
return array(
'start' => gmdate('c', $this->fromGMT($event['start'])), // client treats date strings as they were in users's timezone
'end' => gmdate('c', $this->fromGMT($event['end'])), // so shift timestamps to users's timezone and render a date string
'description' => strval($event['description']),
'location' => strval($event['location']),
'className' => ($addcss ? 'fc-event-cal-'.asciiwords($event['calendar'], true).' ' : '') . 'fc-event-cat-' . asciiwords(strtolower($event['categories']), true),
'allDay' => ($event['allday'] == 1),
) + $event;
}
/**
* Generate reduced and streamlined output for pending alarms
*/
private function _alarms_output($alarms)
{
$out = array();
foreach ($alarms as $alarm) {
$out[] = array(
'id' => $alarm['id'],
'start' => gmdate('c', $this->fromGMT($alarm['start'])),
'end' => gmdate('c', $this->fromGMT($alarm['end'])),
'allDay' => ($event['allday'] == 1)?true:false,
'title' => $alarm['title'],
'location' => $alarm['location'],
'calendar' => $alarm['calendar'],
);
}
return $out;
}
/**
* Render localized text for alarm settings
*/
private function _alarms_text($alarm)
{
list($trigger, $action) = explode(':', $alarm);
$text = '';
switch ($action) {
case 'EMAIL':
$text = $this->gettext('alarmemail');
break;
case 'DISPLAY':
$text = $this->gettext('alarmdisplay');
break;
}
if (preg_match('/@(\d+)/', $trigger, $m)) {
$text .= ' ' . $this->gettext(array('name' => 'alarmat', 'vars' => array('datetime' => format_date($m[1]))));
}
else if ($val = self::parse_alaram_value($trigger)) {
$text .= ' ' . intval($val[0]) . ' ' . $this->gettext('trigger' . $val[1]);
}
else
return false;
return $text;
}
/**
* Render localized text describing the recurrence rule of an event
*/
private function _recurrence_text($rrule)
{
// 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 'YEARY':
$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'], self::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));
}
/**
* Helper function to convert alarm trigger strings
* into two-field values (e.g. "-45M" => 45, "-M")
*/
public static function parse_alaram_value($val)
{
if ($val[0] == '@')
return array(substr($val, 1));
else if (preg_match('/([+-])(\d+)([HMD])/', $val, $m))
return array($m[2], $m[1].$m[3]);
return false;
}
/**
* Convert the internal structured data into a vcalendar rrule 2.0 string
*/
public static function to_rrule($recurrence)
{
if (is_string($recurrence))
return $recurrence;
$rrule = '';
foreach ((array)$recurrence as $k => $val) {
$k = strtoupper($k);
switch ($k) {
case 'UNTIL':
$val = gmdate('Ymd\THis', $val);
break;
case 'EXDATE':
foreach ((array)$val as $i => $ex)
$val[$i] = gmdate('Ymd\THis', $ex);
$val = join(',', $val);
break;
}
$rrule .= $k . '=' . $val . ';';
}
return $rrule;
}
/**
* Convert from fullcalendar date format to PHP date() format string
*/
private static function to_php_date_format($from)
{
// "dd.MM.yyyy HH:mm:ss" => "d.m.Y H:i:s"
return strtr(strtr($from, array(
'yyyy' => 'Y',
'yy' => 'y',
'MMMM' => 'F',
'MMM' => 'M',
'MM' => 'm',
'M' => 'n',
'dddd' => 'l',
'ddd' => 'D',
'dd' => 'd',
'HH' => '**',
'hh' => '%%',
'H' => 'G',
'h' => 'g',
'mm' => 'i',
'ss' => 's',
'TT' => 'A',
'tt' => 'a',
'T' => 'A',
't' => 'a',
'u' => 'c',
)), array(
'**' => 'H',
'%%' => 'h',
));
}
/**
* Convert from PHP date() format to fullcalendar format string
*/
private static function from_php_date_format($from)
{
// "d.m.Y H:i:s" => "dd.MM.yyyy HH:mm:ss"
return strtr($from, array(
'y' => 'yy',
'Y' => 'yyyy',
'M' => 'MMM',
'F' => 'MMMM',
'm' => 'MM',
'n' => 'M',
'd' => 'dd',
'D' => 'ddd',
'l' => 'dddd',
'H' => 'HH',
'h' => 'hh',
'G' => 'H',
'g' => 'h',
'i' => 'mm',
's' => 'ss',
'A' => 'TT',
'a' => 'tt',
'c' => 'u',
));
}
/**
* 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 = array();
foreach ($this->driver->list_calendars() as $cid => $cal) {
if ($cal['active'])
$cals[$cid] = $cal;
}
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' => $start,
'end' => $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()
{
// Upload progress update
if (!empty($_GET['_progress'])) {
rcube_upload_progress();
}
$event = get_input_value('_id', RCUBE_INPUT_GPC);
$calendar = get_input_value('calendar', RCUBE_INPUT_GPC);
$uploadid = get_input_value('_uploadid', RCUBE_INPUT_GPC);
$eventid = 'cal:'.$event;
if (!is_array($_SESSION['event_session']) || $_SESSION['event_session']['id'] != $eventid) {
$_SESSION['event_session'] = array();
$_SESSION['event_session']['id'] = $eventid;
$_SESSION['event_session']['attachments'] = array();
}
// clear all stored output properties (like scripts and env vars)
$this->rc->output->reset();
if (is_array($_FILES['_attachments']['tmp_name'])) {
foreach ($_FILES['_attachments']['tmp_name'] as $i => $filepath) {
// Process uploaded attachment if there is no error
$err = $_FILES['_attachments']['error'][$i];
if (!$err) {
$attachment = array(
'path' => $filepath,
'size' => $_FILES['_attachments']['size'][$i],
'name' => $_FILES['_attachments']['name'][$i],
'mimetype' => rc_mime_content_type($filepath, $_FILES['_attachments']['name'][$i], $_FILES['_attachments']['type'][$i]),
'group' => $eventid,
);
$attachment = $this->rc->plugins->exec_hook('attachment_upload', $attachment);
}
if (!$err && $attachment['status'] && !$attachment['abort']) {
$id = $attachment['id'];
// store new attachment in session
unset($attachment['status'], $attachment['abort']);
$_SESSION['event_session']['attachments'][$id] = $attachment;
if (($icon = $_SESSION['calendar_deleteicon']) && is_file($icon)) {
$button = html::img(array(
'src' => $icon,
'alt' => rcube_label('delete')
));
}
else {
$button = Q(rcube_label('delete'));
}
$content = html::a(array(
'href' => "#delete",
'class' => 'delete',
'onclick' => sprintf("return %s.remove_from_attachment_list('rcmfile%s')", JS_OBJECT_NAME, $id),
'title' => rcube_label('delete'),
), $button);
$content .= Q($attachment['name']);
$this->rc->output->command('add2attachment_list', "rcmfile$id", array(
'html' => $content,
'name' => $attachment['name'],
'mimetype' => $attachment['mimetype'],
'classname' => rcmail_filetype2classname($attachment['mimetype'], $attachment['name']),
'complete' => true), $uploadid);
}
else { // upload failed
if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
$msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array(
'size' => show_bytes(parse_bytes(ini_get('upload_max_filesize'))))));
}
else if ($attachment['error']) {
$msg = $attachment['error'];
}
else {
$msg = rcube_label('fileuploaderror');
}
$this->rc->output->command('display_message', $msg, 'error');
$this->rc->output->command('remove_from_attachment_list', $uploadid);
}
}
}
else if ($_SERVER['REQUEST_METHOD'] == 'POST') {
// if filesize exceeds post_max_size then $_FILES array is empty,
// show filesizeerror instead of fileuploaderror
if ($maxsize = ini_get('post_max_size'))
$msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array(
'size' => show_bytes(parse_bytes($maxsize)))));
else
$msg = rcube_label('fileuploaderror');
$this->rc->output->command('display_message', $msg, 'error');
$this->rc->output->command('remove_from_attachment_list', $uploadid);
}
$this->rc->output->send('iframe');
}
/**
* Handler for attachments download/displaying
*/
public function attachment_get()
{
$event = get_input_value('_event', RCUBE_INPUT_GPC);
$calendar = get_input_value('_cal', RCUBE_INPUT_GPC);
$id = get_input_value('_id', RCUBE_INPUT_GPC);
$event = array('id' => $event, 'calendar' => $calendar);
// show loading page
if (!empty($_GET['_preload'])) {
$url = str_replace('&_preload=1', '', $_SERVER['REQUEST_URI']);
$message = rcube_label('loadingdata');
header('Content-Type: text/html; charset=' . RCMAIL_CHARSET);
print "<html>\n<head>\n"
. '<meta http-equiv="refresh" content="0; url='.Q($url).'">' . "\n"
. '<meta http-equiv="content-type" content="text/html; charset='.RCMAIL_CHARSET.'">' . "\n"
. "</head>\n<body>\n$message\n</body>\n</html>";
exit;
}
ob_end_clean();
send_nocacheing_headers();
if (isset($_SESSION['calendar_attachment']))
$attachment = $_SESSION['calendar_attachment'];
else
$attachment = $_SESSION['calendar_attachment'] = $this->driver->get_attachment($id, $event);
// show part page
if (!empty($_GET['_frame'])) {
$this->attachment = $attachment;
$this->register_handler('plugin.attachmentframe', array($this, 'attachment_frame'));
$this->register_handler('plugin.attachmentcontrols', array($this->ui, 'attachment_controls'));
$this->rc->output->send('calendar.attachment');
exit;
}
$this->rc->session->remove('calendar_attachment');
if ($attachment) {
$mimetype = strtolower($attachment['mimetype']);
list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
$browser = $this->rc->output->browser;
// send download headers
if ($_GET['_download']) {
header("Content-Type: application/octet-stream");
if ($browser->ie)
header("Content-Type: application/force-download");
}
else if ($ctype_primary == 'text') {
header("Content-Type: text/$ctype_secondary");
}
else {
// $mimetype = rcmail_fix_mimetype($mimetype);
header("Content-Type: $mimetype");
header("Content-Transfer-Encoding: binary");
}
$body = $this->driver->get_attachment_body($id, $event);
// display page, @TODO: support text/plain (and maybe some other text formats)
if ($mimetype == 'text/html' && empty($_GET['_download'])) {
$OUTPUT = new rcube_html_page();
// @TODO: use washtml on $body
$OUTPUT->write($body);
}
else {
// don't kill the connection if download takes more than 30 sec.
@set_time_limit(0);
$filename = $attachment['name'];
$filename = preg_replace('[\r\n]', '', $filename);
if ($browser->ie && $browser->ver < 7)
$filename = rawurlencode(abbreviate_string($filename, 55));
else if ($browser->ie)
$filename = rawurlencode($filename);
else
$filename = addcslashes($filename, '"');
$disposition = !empty($_GET['_download']) ? 'attachment' : 'inline';
header("Content-Disposition: $disposition; filename=\"$filename\"");
echo $body;
}
exit;
}
// if we arrive here, the requested part was not found
header('HTTP/1.1 404 Not Found');
exit;
}
/**
* Template object for attachment display frame
*/
public function attachment_frame($attrib)
{
$attachment = $_SESSION['calendar_attachment'];
$mimetype = strtolower($attachment['mimetype']);
list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
$attrib['src'] = './?' . str_replace('_frame=', ($ctype_primary == 'text' ? '_show=' : '_preload='), $_SERVER['QUERY_STRING']);
return html::iframe($attrib);
}
/**
* Prepares new/edited event properties before save
*/
private function prepare_event(&$event, $action)
{
$attachments = array();
$eventid = 'cal:'.$event['id'];
if (is_array($_SESSION['event_session']) && $_SESSION['event_session']['id'] == $eventid) {
if (!empty($_SESSION['event_session']['attachments'])) {
foreach ($_SESSION['event_session']['attachments'] as $id => $attachment) {
if (is_array($event['attachments']) && in_array($id, $event['attachments'])) {
$attachments[$id] = $this->rc->plugins->exec_hook('attachment_get', $attachment);
}
}
}
}
$event['attachments'] = $attachments;
// check for organizer in attendees
if ($event['attendees'] && ($action == 'new' || $action == 'edit')) {
$emails = $this->get_user_emails();
$organizer = $owner = false;
foreach ($event['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER')
$organizer = true;
if ($attendee['email'] == in_array($attendee['email'], $emails))
$owner = $i;
else if (!isset($attendee['rsvp']))
$event['attendees'][$i]['rsvp'] = true;
}
// set owner as organizer if yet missing
if (!$organizer && $owner !== false) {
$event['attendees'][$owner]['role'] = 'ORGANIZER';
unset($event['attendees'][$owner]['rsvp']);
}
else if (!$organizer && $action == 'new' && ($identity = $this->rc->user->get_identity()) && $identity['email']) {
array_unshift($event['attendees'], array('role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email'], 'status' => 'ACCEPTED'));
}
}
}
/**
* Releases some resources after successful event save
*/
private function cleanup_event(&$event)
{
// remove temp. attachment files
$eventid = 'cal:'.$event['id'];
if (!empty($_SESSION['event_session']) && ($eventid = $_SESSION['event_session']['id'])) {
$this->rc->plugins->exec_hook('attachments_cleanup', array('group' => $eventid));
unset($_SESSION['event_session']);
}
}
/**
* 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;
foreach ((array)$event['attendees'] as $attendee) {
// skip myself for obvious reasons
if (!$attendee['email'] || in_array($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;
}
return $sent;
}
/**
* Compose a date string for the given event
*/
public function event_date_text($event, $tzinfo = false)
{
$fromto = '';
$duration = $event['end'] - $event['start'];
$this->date_format_defaults();
$date_format = self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']));
$time_format = self::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format']));
if ($event['allday']) {
$fromto = format_date($event['start'], $date_format);
if (($todate = format_date($event['end'], $date_format)) != $fromto)
$fromto .= ' - ' . $todate;
}
else if ($duration < 86400 && gmdate('d', $event['start']) == gmdate('d', $event['end'])) {
$fromto = format_date($event['start'], $date_format) . ' ' . format_date($event['start'], $time_format) .
' - ' . format_date($event['end'], $time_format);
}
else {
$fromto = format_date($event['start'], $date_format) . ' ' . format_date($event['start'], $time_format) .
' - ' . format_date($event['end'], $date_format) . ' ' . format_date($event['end'], $time_format);
}
// add timezone information
if ($tzinfo && ($tzname = $this->timezone->getName())) {
$fromto .= ' (' . strtr($tzname, '_', ' ') . ')';
}
return $fromto;
}
/**
* 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);
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));
if (!$start) $start = time();
if (!$end) $end = $start + 86400 * 30;
if (!$interval) $interval = 60; // 1 hour
$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;
// 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;
$t = $t_end;
}
// let this information be cached for 5min
send_future_expire_header(300);
echo json_encode(array('email' => $email, 'start' => intval($start), 'end' => intval($t_end), 'interval' => $interval, 'slots' => $slots));
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 = $this->rc->config->get('skin');
$this->include_stylesheet('skins/' . $skin . '/fullcalendar.css');
$this->include_stylesheet('skins/' . $skin . '/print.css');
// Add JS files to the page header
$this->include_script('print.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.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;
}
/**** Event invitation plugin hooks ****/
/**
* Handler for URLs that allow an invitee to respond on his invitation mail
*/
public function itip_attend_response($p)
{
if ($p['action'] == 'attend') {
$this->rc->output->set_env('task', 'calendar'); // override some env vars
$this->rc->output->set_env('keep_alive', 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', $this->gettext('eventcancelled'));
}
// save submitted RSVP status
else if (!empty($_POST['rsvp'])) {
$status = null;
foreach (array('accepted','tentative','declined') as $method) {
if ($_POST['rsvp'] == $this->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), $this->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($this->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($p)
+ 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)) . $hidden->show();
+ 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 $idx => $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();
}
$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) {
// define buttons according to method
if ($this->ical->method == 'REPLY') {
$title = $this->gettext('itipreply');
$buttons = html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "')",
'value' => $this->gettext('updateattendeestatus'),
));
}
else if ($this->ical->method == 'REQUEST') {
$emails = $this->get_user_emails();
$title = $event['SEQUENCE'] > 0 ? $this->gettext('itipupdate') : $this->gettext('itipinvitation');
// add (hidden) buttons and activate them from asyncronous request
foreach (array('accepted','tentative','declined') as $method) {
$rsvp_buttons .= html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "', '$method')",
'value' => $this->gettext('itip' . $method),
));
}
$import_button = html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "')",
'value' => $this->gettext('importtocalendar'),
));
// check my status
$status = 'unknown';
foreach ($event['attendees'] as $i => $attendee) {
if ($attendee['email'] && in_array($attendee['email'], $emails)) {
$status = strtoupper($attendee['status']);
break;
}
}
$dom_id = asciiwords($event['uid'], true);
$buttons = html::div(array('id' => 'rsvp-'.$dom_id, 'style' => 'display:none'), $rsvp_buttons);
$buttons .= html::div(array('id' => 'import-'.$dom_id, 'style' => 'display:none'), $import_button);
$buttons_pre = html::div(array('id' => 'loading-'.$dom_id, 'class' => 'rsvp-status loading'), $this->gettext('loading'));
$this->rc->output->add_script('rcube_calendar.fetch_event_rsvp_status(' . json_serialize(array('uid' => $event['uid'], 'changed' => $event['changed'], 'fallback' => $status)) . ')', 'docready');
}
else if ($this->ical->method == 'CANCEL') {
$title = $this->gettext('itipcancellation');
// create buttons to be activated from async request checking existence of this event in local calendars
$button_import = html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "')",
'value' => $this->gettext('importtocalendar'),
));
$button_remove = html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_calendar.remove_event_from_mail('" . JQ($event['uid']) . "', '" . JQ($event['title']) . "')",
'value' => $this->gettext('removefromcalendar'),
));
$dom_id = asciiwords($event['uid'], true);
$buttons = html::div(array('id' => 'rsvp-'.$dom_id, 'style' => 'display:none'), $button_remove);
$buttons .= html::div(array('id' => 'import-'.$dom_id, 'style' => 'display:none'), $button_import);
$buttons_pre = html::div(array('id' => 'loading-'.$dom_id, 'class' => 'rsvp-status loading'), $this->gettext('loading'));
$this->rc->output->add_script('rcube_calendar.fetch_event_rsvp_status(' . json_serialize(array('uid' => $event['uid'], 'changed' => $event['changed'], 'fallback' => 'CANCELLED')) . ')', 'docready');
}
else {
$buttons = html::tag('input', array(
'type' => 'button',
'class' => 'button',
'onclick' => "rcube_calendar.add_event_from_mail('" . JQ($mime_id.':'.$idx) . "')",
'value' => $this->gettext('importtocalendar'),
));
}
// show event details with buttons
$html .= html::div('calendar-invitebox', $this->ui->event_details_table($event, $title) . $buttons_pre . html::div('rsvp-buttons', $buttons));
// 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');
}
return $p;
}
/**
* Handler for POST request to import an event attached to a mail message
*/
public function mail_import_event()
{
$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);
$charset = RCMAIL_CHARSET;
// establish imap connection
$this->rc->imap_connect();
$this->rc->imap->set_mailbox($mbox);
if ($uid && $mime_id) {
list($mime_id, $index) = explode(':', $mime_id);
$part = $this->rc->imap->get_message_part($uid, $mime_id);
if ($part->ctype_parameters['charset'])
$charset = $part->ctype_parameters['charset'];
$headers = $this->rc->imap->get_headers($uid);
}
$events = $this->get_ical()->import($part, $charset);
$error_msg = $this->gettext('errorimportingevent');
$success = false;
// successfully parsed events?
if (!empty($events) && ($event = $events[$index])) {
// find writeable calendar to store event
$cal_id = !empty($_REQUEST['_calendar']) ? get_input_value('_calendar', RCUBE_INPUT_POST) : $this->rc->config->get('calendar_default_calendar');
$calendars = $this->driver->list_calendars();
$calendar = $calendars[$cal_id] ? $calendars[$cal_id] : null;
if (!$calendar || $calendar['readonly']) {
foreach ($calendars as $cal) {
if (!$cal['readonly']) {
$calendar = $cal;
break;
}
}
}
// 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($attendee['email'], $emails)) {
$event['attendees'][$i]['status'] = strtoupper($status);
}
}
}
// save to calendar
if ($calendar && !$calendar['readonly']) {
$event['id'] = $event['uid'];
$event['calendar'] = $calendar['id'];
// check for existing event with the same UID
$existing = $this->driver->get_event($event['uid'], true);
if ($existing) {
// only update attendee status
if ($this->ical->method == 'REPLY') {
// try to identify the attendee using the email sender address
$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] : '';
$sender_utf = rcube_idn_to_utf8($sender);
$existing_attendee = -1;
foreach ($existing['attendees'] as $i => $attendee) {
if ($sender && ($attendee['email'] == $sender || $attendee['email'] == $sender_utf)) {
$existing_attendee = $i;
break;
}
}
$event_attendee = null;
foreach ($event['attendees'] as $attendee) {
if ($sender && ($attendee['email'] == $sender || $attendee['email'] == $sender_utf)) {
$event_attendee = $attendee;
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['changed'] >= $existing['changed'] && $event['attendees']) {
$existing['attendees'] = $event['attendees'];
$success = $this->driver->edit_event($existing);
}
else {
$error_msg = $this->gettext('newerversionexists');
}
}
// import the (newer) event
// TODO: compare SEQUENCE numbers instead of changed dates
else if ($event['changed'] >= $existing['changed']) {
$success = $this->driver->edit_event($event);
}
else if (!empty($status)) {
$existing['attendees'] = $event['attendees'];
$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' : 'importedsuccessfully';
$this->rc->output->command('display_message', $this->gettext(array('name' => $message, 'vars' => array('calendar' => $calendar['name']))), 'confirmation');
$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($organizer['email'], $emails) && !$error_msg) {
$itip = $this->load_itip();
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();
}
/**
* 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
$this->rc->imap_connect();
$this->rc->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['event_session']) || $_SESSION['event_session']['id'] != $eventid) {
$_SESSION['event_session'] = array();
$_SESSION['event_session']['id'] = $eventid;
$_SESSION['event_session']['attachments'] = array();
}
foreach ((array)$message->attachments as $part) {
$attachment = array(
'data' => $this->rc->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'];
// store new attachment in session
unset($attachment['status'], $attachment['abort'], $attachment['data']);
$_SESSION['event_session']['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()
{
$emails = array($this->rc->user->get_username());
foreach ($this->rc->user->list_identities() as $identity)
$emails[] = $identity['email'];
return array_unique($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 = 143;
}
$url = $schema . '://' . $_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);
}
}
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 2e50d43e..a2aac7cf 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -1,2733 +1,2733 @@
/**
* Client UI Javascript 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) 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/>.
*/
// Roundcube calendar UI client class
function rcube_calendar_ui(settings)
{
// extend base class
rcube_calendar.call(this, settings);
/*** member vars ***/
this.is_loading = false;
this.selected_event = null;
this.selected_calendar = null;
this.search_request = null;
this.saving_lock = null;
/*** private vars ***/
var DAY_MS = 86400000;
var HOUR_MS = 3600000;
var me = this;
var gmt_offset = (new Date().getTimezoneOffset() / -60) - (settings.timezone || 0) - (settings.dst || 0);
var client_timezone = new Date().getTimezoneOffset();
var day_clicked = day_clicked_ts = 0;
var ignore_click = false;
var event_defaults = { free_busy:'busy' };
var event_attendees = [];
var attendees_list;
var freebusy_ui = { workinhoursonly:false, needsupdate:false };
var freebusy_data = {};
var current_view = null;
var exec_deferred = bw.ie6 ? 5 : 1;
var sensitivitylabels = { 0:rcmail.gettext('public','calendar'), 1:rcmail.gettext('private','calendar'), 2:rcmail.gettext('confidential','calendar') };
var ui_loading = rcmail.set_busy(true, 'loading');
// general datepicker settings
var datepicker_settings = {
// translate from fullcalendar format to datepicker format
dateFormat: settings['date_format'].replace(/M/g, 'm').replace(/mmmmm/, 'MM').replace(/mmm/, 'M').replace(/dddd/, 'DD').replace(/ddd/, 'D').replace(/yy/g, 'y'),
firstDay : settings['first_day'],
dayNamesMin: settings['days_short'],
monthNames: settings['months'],
monthNamesShort: settings['months'],
changeMonth: false,
showOtherMonths: true,
selectOtherMonths: true
};
/*** private methods ***/
var Q = this.quote_html;
var text2html = function(str, maxlen, maxlines)
{
var html = Q(String(str));
// limit visible text length
if (maxlen) {
var morelink = ' <a href="#more" onclick="$(this).hide().next().show();return false" class="morelink">'+rcmail.gettext('showmore','calendar')+'</a><span style="display:none">',
lines = html.split(/\r?\n/),
words, out = '', len = 0;
for (var i=0; i < lines.length; i++) {
len += lines[i].length;
if (maxlines && i == maxlines - 1) {
out += lines[i] + '\n' + morelink;
maxlen = html.length * 2;
}
else if (len > maxlen) {
len = out.length;
words = lines[i].split(' ');
for (var j=0; j < words.length; j++) {
len += words[j].length + 1;
out += words[j] + ' ';
if (len > maxlen) {
out += morelink;
maxlen = html.length * 2;
}
}
out += '\n';
}
else
out += lines[i] + '\n';
}
if (maxlen > str.length)
out += '</span>';
html = out;
}
// simple link parser (similar to rcube_string_replacer class in PHP)
var utf_domain = '[^?&@"\'/\\(\\)\\s\\r\\t\\n]+\\.([^\x00-\x2f\x3b-\x40\x5b-\x60\x7b-\x7f]{2,}|xn--[a-z0-9]{2,})';
var url1 = '.:;,', url2 = 'a-z0-9%=#@+?&/_~\\[\\]-';
var link_pattern = new RegExp('([hf]t+ps?://)('+utf_domain+'(['+url1+']?['+url2+']+)*)?', 'ig');
var mailto_pattern = new RegExp('([^\\s\\n\\(\\);]+@'+utf_domain+')', 'ig');
return html
.replace(link_pattern, '<a href="$1$2" target="_blank">$1$2</a>')
.replace(mailto_pattern, '<a href="mailto:$1">$1</a>')
.replace(/(mailto:)([^"]+)"/g, '$1$2" onclick="rcmail.command(\'compose\', \'$2\');return false"')
.replace(/\n/g, "<br/>");
};
// same as str.split(delimiter) but it ignores delimiters within quoted strings
var explode_quoted_string = function(str, delimiter)
{
var result = [],
strlen = str.length,
q, p, i, char, last;
for (q = p = i = 0; i < strlen; i++) {
char = str.charAt(i);
if (char == '"' && last != '\\') {
q = !q;
}
else if (!q && char == delimiter) {
result.push(str.substring(p, i));
p = i + 1;
}
last = char;
}
result.push(str.substr(p));
return result;
};
// from time and date strings to a real date object
var parse_datetime = function(time, date)
{
// we use the utility function from datepicker to parse dates
var date = date ? $.datepicker.parseDate(datepicker_settings.dateFormat, date, datepicker_settings) : new Date();
var time_arr = time.replace(/\s*[ap][.m]*/i, '').replace(/0([0-9])/g, '$1').split(/[:.]/);
if (!isNaN(time_arr[0])) {
date.setHours(time_arr[0]);
if (time.match(/p[.m]*/i) && date.getHours() < 12)
date.setHours(parseInt(time_arr[0]) + 12);
else if (time.match(/a[.m]*/i) && date.getHours() == 12)
date.setHours(0);
}
if (!isNaN(time_arr[1]))
date.setMinutes(time_arr[1]);
return date;
};
// clone the given date object and optionally adjust time
var clone_date = function(date, adjust)
{
var d = new Date(date.getTime());
// set time to 00:00
if (adjust == 1) {
d.setHours(0);
d.setMinutes(0);
}
// set time to 23:59
else if (adjust == 2) {
d.setHours(23);
d.setMinutes(59);
}
return d;
};
// convert the given Date object into a unix timestamp respecting browser's and user's timezone settings
var date2unixtime = function(date)
{
var dst_offset = (client_timezone - date.getTimezoneOffset()) * 60; // adjust DST offset
return Math.round(date.getTime()/1000 + gmt_offset * 3600 + dst_offset);
};
var fromunixtime = function(ts)
{
ts -= gmt_offset * 3600;
var date = new Date(ts * 1000),
dst_offset = (client_timezone - date.getTimezoneOffset()) * 60;
if (dst_offset) // adjust DST offset
date.setTime((ts + 3600) * 1000);
return date;
};
// determine whether the given date is on a weekend
var is_weekend = function(date)
{
return date.getDay() == 0 || date.getDay() == 6;
};
var is_workinghour = function(date)
{
if (settings['work_start'] > settings['work_end'])
return date.getHours() >= settings['work_start'] || date.getHours() < settings['work_end'];
else
return date.getHours() >= settings['work_start'] && date.getHours() < settings['work_end'];
};
// check if the event has 'real' attendees, excluding the current user
var has_attendees = function(event)
{
return (event.attendees && (event.attendees.length > 1 || event.attendees[0].email != settings.identity.email));
};
// check if the current user is an attendee of this event
var is_attendee = function(event, role)
{
for (var i=0; event.attendees && i < event.attendees.length; i++) {
if ((!role || event.attendees[i].role == role) && event.attendees[i].email && settings.identity.emails.indexOf(';'+event.attendees[i].email) >= 0)
return true;
}
return false;
};
// check if the current user is the organizer
var is_organizer = function(event)
{
return is_attendee(event, 'ORGANIZER') || !event.id;
};
// create a nice human-readable string for the date/time range
var event_date_text = function(event)
{
var fromto, duration = event.end.getTime() / 1000 - event.start.getTime() / 1000;
if (event.allDay)
fromto = $.fullCalendar.formatDate(event.start, settings['date_format']) + (duration > 86400 || event.start.getDay() != event.end.getDay() ? ' &mdash; ' + $.fullCalendar.formatDate(event.end, settings['date_format']) : '');
else if (duration < 86400 && event.start.getDay() == event.end.getDay())
fromto = $.fullCalendar.formatDate(event.start, settings['date_format']) + ' ' + $.fullCalendar.formatDate(event.start, settings['time_format']) + ' &mdash; '
+ $.fullCalendar.formatDate(event.end, settings['time_format']);
else
fromto = $.fullCalendar.formatDate(event.start, settings['date_format']) + ' ' + $.fullCalendar.formatDate(event.start, settings['time_format']) + ' &mdash; '
+ $.fullCalendar.formatDate(event.end, settings['date_format']) + ' ' + $.fullCalendar.formatDate(event.end, settings['time_format']);
return fromto;
};
var load_attachment = function(event, att)
{
var qstring = '_id='+urlencode(att.id)+'&_event='+urlencode(event.recurrence_id||event.id)+'&_cal='+urlencode(event.calendar);
// open attachment in frame if it's of a supported mimetype
if (id && att.mimetype && $.inArray(att.mimetype, rcmail.mimetypes)>=0) {
rcmail.attachment_win = window.open(rcmail.env.comm_path+'&_action=get-attachment&'+qstring+'&_frame=1', 'rcubeeventattachment');
if (rcmail.attachment_win) {
window.setTimeout(function() { rcmail.attachment_win.focus(); }, 10);
return;
}
}
rcmail.goto_url('get-attachment', qstring+'&_download=1', false);
};
// build event attachments list
var event_show_attachments = function(list, container, event, edit)
{
var i, id, len, img, content, li, elem,
ul = document.createElement('UL');
ul.className = 'attachmentslist';
for (i=0, len=list.length; i<len; i++) {
elem = list[i];
li = document.createElement('LI');
li.className = elem.classname;
if (edit) {
rcmail.env.attachments[elem.id] = elem;
// delete icon
content = document.createElement('A');
content.href = '#delete';
content.title = rcmail.gettext('delete');
content.className = 'delete';
$(content).click({id: elem.id}, function(e) { remove_attachment(this, e.data.id); return false; });
if (!rcmail.env.deleteicon)
content.innerHTML = rcmail.gettext('delete');
else {
img = document.createElement('IMG');
img.src = rcmail.env.deleteicon;
img.alt = rcmail.gettext('delete');
content.appendChild(img);
}
li.appendChild(content);
}
// name/link
content = document.createElement('A');
content.innerHTML = elem.name;
content.className = 'file';
content.href = '#load';
$(content).click({event: event, att: elem}, function(e) {
load_attachment(e.data.event, e.data.att); return false; });
li.appendChild(content);
ul.appendChild(li);
}
if (edit && rcmail.gui_objects.attachmentlist) {
ul.id = rcmail.gui_objects.attachmentlist.id;
rcmail.gui_objects.attachmentlist = ul;
}
container.empty().append(ul);
};
var remove_attachment = function(elem, id)
{
$(elem.parentNode).hide();
rcmail.env.deleted_attachments.push(id);
delete rcmail.env.attachments[id];
};
// event details dialog (show only)
var event_show_dialog = function(event)
{
var $dialog = $("#eventshow").dialog('close').removeClass().addClass('uidialog');
var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:false };
me.selected_event = event;
$dialog.find('div.event-section, div.event-line').hide();
$('#event-title').html(Q(event.title)).show();
if (event.location)
$('#event-location').html('@ ' + text2html(event.location)).show();
if (event.description)
$('#event-description').show().children('.event-text').html(text2html(event.description, 300, 6));
// render from-to in a nice human-readable way
// -> now shown in dialog title
// $('#event-date').html(Q(me.event_date_text(event))).show();
if (event.recurrence && event.recurrence_text)
$('#event-repeat').show().children('.event-text').html(Q(event.recurrence_text));
if (event.alarms && event.alarms_text)
$('#event-alarm').show().children('.event-text').html(Q(event.alarms_text));
if (calendar.name)
$('#event-calendar').show().children('.event-text').html(Q(calendar.name)).removeClass().addClass('event-text').addClass('cal-'+calendar.id);
if (event.categories)
$('#event-category').show().children('.event-text').html(Q(event.categories)).removeClass().addClass('event-text cat-'+String(event.categories).replace(rcmail.identifier_expr, ''));
if (event.free_busy)
$('#event-free-busy').show().children('.event-text').html(Q(rcmail.gettext(event.free_busy, 'calendar')));
if (event.priority > 0) {
var priolabels = [ '', rcmail.gettext('high'), rcmail.gettext('highest'), '', '', rcmail.gettext('normal'), '', '', rcmail.gettext('low'), rcmail.gettext('lowest') ];
$('#event-priority').show().children('.event-text').html(Q(event.priority+' '+priolabels[event.priority]));
}
if (event.sensitivity != 0) {
var sensitivityclasses = { 0:'public', 1:'private', 2:'confidential' };
$('#event-sensitivity').show().children('.event-text').html(Q(sensitivitylabels[event.sensitivity]));
$dialog.addClass('sensitivity-'+sensitivityclasses[event.sensitivity]);
}
// create attachments list
if ($.isArray(event.attachments)) {
event_show_attachments(event.attachments, $('#event-attachments').children('.event-text'), event);
if (event.attachments.length > 0) {
$('#event-attachments').show();
}
}
else if (calendar.attachments) {
// fetch attachments, some drivers doesn't set 'attachments' prop of the event?
}
// list event attendees
if (calendar.attendees && event.attendees) {
var data, dispname, organizer = false, rsvp = false, html = '';
for (var j=0; j < event.attendees.length; j++) {
data = event.attendees[j];
dispname = Q(data.name || data.email);
if (data.email) {
dispname = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink">' + dispname + '</a>';
if (data.role == 'ORGANIZER')
organizer = true;
else if ((data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE') && settings.identity.emails.indexOf(';'+data.email) >= 0)
rsvp = data.status.toLowerCase();
}
html += '<span class="attendee ' + String(data.role == 'ORGANIZER' ? 'organizer' : data.status).toLowerCase() + '">' + dispname + '</span> ';
// stop listing attendees
if (j == 7 && event.attendees.length >= 7) {
html += ' <em>' + rcmail.gettext('andnmore', 'calendar').replace('$nr', event.attendees.length - j - 1) + '</em>';
break;
}
}
if (html && (event.attendees.length > 1 || !organizer)) {
$('#event-attendees').show()
.children('.event-text')
.html(html)
.find('a.mailtolink').click(function(e) { rcmail.redirect(rcmail.url('mail/compose', { _to:this.href.substr(7) })); return false; });
}
$('#event-rsvp')[(rsvp?'show':'hide')]();
$('#event-rsvp .rsvp-buttons input').prop('disabled', false).filter('input[rel='+rsvp+']').prop('disabled', true);
}
var buttons = {};
if (calendar.editable && event.editable !== false) {
buttons[rcmail.gettext('edit', 'calendar')] = function() {
event_edit_dialog('edit', event);
};
buttons[rcmail.gettext('remove', 'calendar')] = function() {
me.delete_event(event);
$dialog.dialog('close');
};
}
else {
buttons[rcmail.gettext('close', 'calendar')] = function(){
$dialog.dialog('close');
};
}
// open jquery UI dialog
$dialog.dialog({
modal: false,
resizable: !bw.ie6,
closeOnEscape: (!bw.ie6 && !bw.ie7), // disable for performance reasons
title: Q(me.event_date_text(event)),
close: function() {
$dialog.dialog('destroy').hide();
},
buttons: buttons,
minWidth: 320,
width: 420
}).show();
// set dialog size according to content
me.dialog_resize($dialog.get(0), $dialog.height(), 420);
/*
// add link for "more options" drop-down
$('<a>')
.attr('href', '#')
.html('More Options')
.addClass('dropdown-link')
.click(function(){ return false; })
.insertBefore($dialog.parent().find('.ui-dialog-buttonset').children().first());
*/
};
// bring up the event dialog (jquery-ui popup)
var event_edit_dialog = function(action, event)
{
// close show dialog first
$("#eventshow").dialog('close');
var $dialog = $('<div>');
var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:action=='new' };
me.selected_event = $.extend($.extend({}, event_defaults), event); // clone event object (with defaults)
event = me.selected_event; // change reference to clone
freebusy_ui.needsupdate = false;
// reset dialog first
$('#eventtabs').get(0).reset();
// event details
var title = $('#edit-title').val(event.title || '');
var location = $('#edit-location').val(event.location || '');
var description = $('#edit-description').html(event.description || '');
var categories = $('#edit-categories').val(event.categories);
var calendars = $('#edit-calendar').val(event.calendar);
var freebusy = $('#edit-free-busy').val(event.free_busy);
var priority = $('#edit-priority').val(event.priority);
var sensitivity = $('#edit-sensitivity').val(event.sensitivity);
var duration = Math.round((event.end.getTime() - event.start.getTime()) / 1000);
var startdate = $('#edit-startdate').val($.fullCalendar.formatDate(event.start, settings['date_format'])).data('duration', duration);
var starttime = $('#edit-starttime').val($.fullCalendar.formatDate(event.start, settings['time_format'])).show();
var enddate = $('#edit-enddate').val($.fullCalendar.formatDate(event.end, settings['date_format']));
var endtime = $('#edit-endtime').val($.fullCalendar.formatDate(event.end, settings['time_format'])).show();
var allday = $('#edit-allday').get(0);
var notify = $('#edit-attendees-donotify').get(0);
var invite = $('#edit-attendees-invite').get(0);
notify.checked = has_attendees(event), invite.checked = true;
if (event.allDay) {
starttime.val("12:00").hide();
endtime.val("13:00").hide();
allday.checked = true;
}
else {
allday.checked = false;
}
// set alarm(s)
// TODO: support multiple alarm entries
if (event.alarms) {
if (typeof event.alarms == 'string')
event.alarms = event.alarms.split(';');
for (var alarm, i=0; i < event.alarms.length; i++) {
alarm = String(event.alarms[i]).split(':');
if (!alarm[1] && alarm[0]) alarm[1] = 'DISPLAY';
$('select.edit-alarm-type').val(alarm[1]);
if (alarm[0].match(/@(\d+)/)) {
var ondate = fromunixtime(parseInt(RegExp.$1));
$('select.edit-alarm-offset').val('@');
$('input.edit-alarm-date').val($.fullCalendar.formatDate(ondate, settings['date_format']));
$('input.edit-alarm-time').val($.fullCalendar.formatDate(ondate, settings['time_format']));
}
else if (alarm[0].match(/([-+])(\d+)([MHD])/)) {
$('input.edit-alarm-value').val(RegExp.$2);
$('select.edit-alarm-offset').val(''+RegExp.$1+RegExp.$3);
}
}
}
// set correct visibility by triggering onchange handlers
$('select.edit-alarm-type, select.edit-alarm-offset').change();
// enable/disable alarm property according to backend support
$('#edit-alarms')[(calendar.alarms ? 'show' : 'hide')]();
// check categories drop-down: add value if not exists
if (event.categories && !categories.find("option[value='"+event.categories+"']").length) {
$('<option>').attr('value', event.categories).text(event.categories).appendTo(categories).prop('selected', true);
}
// set recurrence form
var recurrence, interval, rrtimes, rrenddate;
var load_recurrence_tab = function()
{
recurrence = $('#edit-recurrence-frequency').val(event.recurrence ? event.recurrence.FREQ : '').change();
interval = $('select.edit-recurrence-interval').val(event.recurrence ? event.recurrence.INTERVAL : 1);
rrtimes = $('#edit-recurrence-repeat-times').val(event.recurrence ? event.recurrence.COUNT : 1);
rrenddate = $('#edit-recurrence-enddate').val(event.recurrence && event.recurrence.UNTIL ? $.fullCalendar.formatDate(new Date(event.recurrence.UNTIL*1000), settings['date_format']) : '');
$('input.edit-recurrence-until:checked').prop('checked', false);
var weekdays = ['SU','MO','TU','WE','TH','FR','SA'];
var rrepeat_id = '#edit-recurrence-repeat-forever';
if (event.recurrence && event.recurrence.COUNT) rrepeat_id = '#edit-recurrence-repeat-count';
else if (event.recurrence && event.recurrence.UNTIL) rrepeat_id = '#edit-recurrence-repeat-until';
$(rrepeat_id).prop('checked', true);
if (event.recurrence && event.recurrence.BYDAY && event.recurrence.FREQ == 'WEEKLY') {
var wdays = event.recurrence.BYDAY.split(',');
$('input.edit-recurrence-weekly-byday').val(wdays);
}
if (event.recurrence && event.recurrence.BYMONTHDAY) {
$('input.edit-recurrence-monthly-bymonthday').val(String(event.recurrence.BYMONTHDAY).split(','));
$('input.edit-recurrence-monthly-mode').val(['BYMONTHDAY']);
}
if (event.recurrence && event.recurrence.BYDAY && (event.recurrence.FREQ == 'MONTHLY' || event.recurrence.FREQ == 'YEARLY')) {
var byday, section = event.recurrence.FREQ.toLowerCase();
if ((byday = String(event.recurrence.BYDAY).match(/(-?[1-4])([A-Z]+)/))) {
$('#edit-recurrence-'+section+'-prefix').val(byday[1]);
$('#edit-recurrence-'+section+'-byday').val(byday[2]);
}
$('input.edit-recurrence-'+section+'-mode').val(['BYDAY']);
}
else if (event.start) {
$('#edit-recurrence-monthly-byday').val(weekdays[event.start.getDay()]);
}
if (event.recurrence && event.recurrence.BYMONTH) {
$('input.edit-recurrence-yearly-bymonth').val(String(event.recurrence.BYMONTH).split(','));
}
else if (event.start) {
$('input.edit-recurrence-yearly-bymonth').val([String(event.start.getMonth()+1)]);
}
};
// show warning if editing a recurring event
if (event.id && event.recurrence) {
$('#edit-recurring-warning').show();
$('input.edit-recurring-savemode[value="all"]').prop('checked', true);
}
else
$('#edit-recurring-warning').hide();
// init attendees tab
var organizer = !event.attendees || is_organizer(event);
event_attendees = [];
attendees_list = $('#edit-attendees-table > tbody').html('');
$('#edit-attendees-notify')[(notify.checked && organizer ? 'show' : 'hide')]();
$('#edit-localchanges-warning')[(has_attendees(event) && !organizer ? 'show' : 'hide')]();
var load_attendees_tab = function()
{
if (event.attendees) {
for (var j=0; j < event.attendees.length; j++)
add_attendee(event.attendees[j], !organizer);
}
$('#edit-attendees-form')[(organizer?'show':'hide')]();
$('#edit-attendee-schedule')[(calendar.freebusy?'show':'hide')]();
};
// attachments
var load_attachments_tab = function()
{
rcmail.enable_command('remove-attachment', !calendar.readonly);
rcmail.env.deleted_attachments = [];
// we're sharing some code for uploads handling with app.js
rcmail.env.attachments = [];
rcmail.env.compose_id = event.id; // for rcmail.async_upload_form()
if ($.isArray(event.attachments)) {
event_show_attachments(event.attachments, $('#edit-attachments'), event, true);
}
else {
$('#edit-attachments > ul').empty();
// fetch attachments, some drivers doesn't set 'attachments' array for event?
}
};
// init dialog buttons
var buttons = {};
buttons[rcmail.gettext('save', 'calendar')] = function() {
var start = parse_datetime(allday.checked ? '12:00' : starttime.val(), startdate.val());
var end = parse_datetime(allday.checked ? '13:00' : endtime.val(), enddate.val());
// basic input validatetion
if (start.getTime() > end.getTime()) {
alert(rcmail.gettext('invalideventdates', 'calendar'));
return false;
}
// post data to server
var data = {
calendar: event.calendar,
start: date2unixtime(start),
end: date2unixtime(end),
allday: allday.checked?1:0,
title: title.val(),
description: description.val(),
location: location.val(),
categories: categories.val(),
free_busy: freebusy.val(),
priority: priority.val(),
sensitivity: sensitivity.val(),
recurrence: '',
alarms: '',
attendees: event_attendees,
deleted_attachments: rcmail.env.deleted_attachments,
attachments: []
};
// serialize alarm settings
// TODO: support multiple alarm entries
var alarm = $('select.edit-alarm-type').val();
if (alarm) {
var val, offset = $('select.edit-alarm-offset').val();
if (offset == '@')
data.alarms = '@' + date2unixtime(parse_datetime($('input.edit-alarm-time').val(), $('input.edit-alarm-date').val())) + ':' + alarm;
else if ((val = parseInt($('input.edit-alarm-value').val())) && !isNaN(val) && val >= 0)
data.alarms = offset[0] + val + offset[1] + ':' + alarm;
}
// uploaded attachments list
for (var i in rcmail.env.attachments)
if (i.match(/^rcmfile(.+)/))
data.attachments.push(RegExp.$1);
// read attendee roles
$('select.edit-attendee-role').each(function(i, elem){
if (data.attendees[i])
data.attendees[i].role = $(elem).val();
});
// don't submit attendees if only myself is added as organizer
if (data.attendees.length == 1 && data.attendees[0].role == 'ORGANIZER' && data.attendees[0].email == settings.identity.email)
data.attendees = [];
// tell server to send notifications
if (data.attendees.length && organizer && ((event.id && notify.checked) || (!event.id && invite.checked))) {
data._notify = 1;
}
// gather recurrence settings
var freq;
if ((freq = recurrence.val()) != '') {
data.recurrence = {
FREQ: freq,
INTERVAL: $('#edit-recurrence-interval-'+freq.toLowerCase()).val()
};
var until = $('input.edit-recurrence-until:checked').val();
if (until == 'count')
data.recurrence.COUNT = rrtimes.val();
else if (until == 'until')
data.recurrence.UNTIL = date2unixtime(parse_datetime(endtime.val(), rrenddate.val()));
if (freq == 'WEEKLY') {
var byday = [];
$('input.edit-recurrence-weekly-byday:checked').each(function(){ byday.push(this.value); });
if (byday.length)
data.recurrence.BYDAY = byday.join(',');
}
else if (freq == 'MONTHLY') {
var mode = $('input.edit-recurrence-monthly-mode:checked').val(), bymonday = [];
if (mode == 'BYMONTHDAY') {
$('input.edit-recurrence-monthly-bymonthday:checked').each(function(){ bymonday.push(this.value); });
if (bymonday.length)
data.recurrence.BYMONTHDAY = bymonday.join(',');
}
else
data.recurrence.BYDAY = $('#edit-recurrence-monthly-prefix').val() + $('#edit-recurrence-monthly-byday').val();
}
else if (freq == 'YEARLY') {
var byday, bymonth = [];
$('input.edit-recurrence-yearly-bymonth:checked').each(function(){ bymonth.push(this.value); });
if (bymonth.length)
data.recurrence.BYMONTH = bymonth.join(',');
if ((byday = $('#edit-recurrence-yearly-byday').val()))
data.recurrence.BYDAY = $('#edit-recurrence-yearly-prefix').val() + byday;
}
}
data.calendar = calendars.val();
if (event.id) {
data.id = event.id;
if (event.recurrence)
data._savemode = $('input.edit-recurring-savemode:checked').val();
if (data.calendar && data.calendar != event.calendar)
data._fromcalendar = event.calendar;
}
update_event(action, data);
$dialog.dialog("close");
};
if (event.id) {
buttons[rcmail.gettext('remove', 'calendar')] = function() {
me.delete_event(event);
$dialog.dialog('close');
};
}
buttons[rcmail.gettext('cancel', 'calendar')] = function() {
$dialog.dialog("close");
};
// show/hide tabs according to calendar's feature support
$('#edit-tab-attendees')[(calendar.attendees?'show':'hide')]();
$('#edit-tab-attachments')[(calendar.attachments?'show':'hide')]();
// activate the first tab
$('#eventtabs').tabs('select', 0);
// hack: set task to 'calendar' to make all dialog actions work correctly
var comm_path_before = rcmail.env.comm_path;
rcmail.env.comm_path = comm_path_before.replace(/_task=[a-z]+/, '_task=calendar');
var editform = $("#eventedit");
// open jquery UI dialog
$dialog.dialog({
modal: true,
resizable: (!bw.ie6 && !bw.ie7), // disable for performance reasons
closeOnEscape: false,
title: rcmail.gettext((action == 'edit' ? 'edit_event' : 'new_event'), 'calendar'),
close: function() {
editform.hide().appendTo(document.body);
$dialog.dialog("destroy").remove();
rcmail.ksearch_blur();
rcmail.ksearch_destroy();
freebusy_data = {};
rcmail.env.comm_path = comm_path_before; // restore comm_path
},
buttons: buttons,
minWidth: 500,
width: 580
}).append(editform.show()); // adding form content AFTERWARDS massively speeds up opening on IE6
// set dialog size according to form content
me.dialog_resize($dialog.get(0), editform.height() + (bw.ie ? 20 : 0), 530);
title.select();
// init other tabs asynchronously
window.setTimeout(load_recurrence_tab, exec_deferred);
if (calendar.attendees)
window.setTimeout(load_attendees_tab, exec_deferred);
if (calendar.attachments)
window.setTimeout(load_attachments_tab, exec_deferred);
};
// open a dialog to display detailed free-busy information and to find free slots
var event_freebusy_dialog = function()
{
var $dialog = $('#eventfreebusy').dialog('close');
var event = me.selected_event;
if (!event_attendees.length)
return false;
// set form elements
var allday = $('#edit-allday').get(0);
var duration = Math.round((event.end.getTime() - event.start.getTime()) / 1000);
freebusy_ui.startdate = $('#schedule-startdate').val($.fullCalendar.formatDate(event.start, settings['date_format'])).data('duration', duration);
freebusy_ui.starttime = $('#schedule-starttime').val($.fullCalendar.formatDate(event.start, settings['time_format'])).show();
freebusy_ui.enddate = $('#schedule-enddate').val($.fullCalendar.formatDate(event.end, settings['date_format']));
freebusy_ui.endtime = $('#schedule-endtime').val($.fullCalendar.formatDate(event.end, settings['time_format'])).show();
if (allday.checked) {
freebusy_ui.starttime.val("12:00").hide();
freebusy_ui.endtime.val("13:00").hide();
event.allDay = true;
}
// read attendee roles from drop-downs
$('select.edit-attendee-role').each(function(i, elem){
if (event_attendees[i])
event_attendees[i].role = $(elem).val();
});
// render time slots
var now = new Date(), fb_start = new Date(), fb_end = new Date();
fb_start.setTime(event.start);
fb_start.setHours(0); fb_start.setMinutes(0); fb_start.setSeconds(0); fb_start.setMilliseconds(0);
fb_end.setTime(fb_start.getTime() + DAY_MS);
freebusy_data = { required:{}, all:{} };
freebusy_ui.loading = 1; // prevent render_freebusy_grid() to load data yet
freebusy_ui.numdays = Math.max(allday.checked ? 14 : 1, Math.ceil(duration * 2 / 86400));
freebusy_ui.interval = allday.checked ? 1440 : 60;
freebusy_ui.start = fb_start;
freebusy_ui.end = new Date(freebusy_ui.start.getTime() + DAY_MS * freebusy_ui.numdays);
render_freebusy_grid(0);
// render list of attendees
freebusy_ui.attendees = {};
var domid, dispname, data, role_html, list_html = '';
for (var i=0; i < event_attendees.length; i++) {
data = event_attendees[i];
dispname = Q(data.name || data.email);
domid = String(data.email).replace(rcmail.identifier_expr, '');
role_html = '<a class="attendee-role-toggle" id="rcmlia' + domid + '" title="' + Q(rcmail.gettext('togglerole', 'calendar')) + '">&nbsp;</a>';
list_html += '<div class="attendee ' + String(data.role).toLowerCase() + '" id="rcmli' + domid + '">' + role_html + dispname + '</div>';
// clone attendees data for local modifications
freebusy_ui.attendees[i] = freebusy_ui.attendees[domid] = $.extend({}, data);
}
// add total row
list_html += '<div class="attendee spacer">&nbsp;</div>';
list_html += '<div class="attendee total">' + rcmail.gettext('reqallattendees','calendar') + '</div>';
$('#schedule-attendees-list').html(list_html)
.unbind('click.roleicons')
.bind('click.roleicons', function(e){
// toggle attendee status upon click on icon
if (e.target.id && e.target.id.match(/rcmlia(.+)/)) {
var attendee, domid = RegExp.$1, roles = [ 'REQ-PARTICIPANT', 'OPT-PARTICIPANT', 'CHAIR' ];
if ((attendee = freebusy_ui.attendees[domid]) && attendee.role != 'ORGANIZER') {
var req = attendee.role != 'OPT-PARTICIPANT';
var j = $.inArray(attendee.role, roles);
j = (j+1) % roles.length;
attendee.role = roles[j];
$(e.target).parent().removeClass().addClass('attendee '+String(attendee.role).toLowerCase());
// update total display if required-status changed
if (req != (roles[j] != 'OPT-PARTICIPANT')) {
compute_freebusy_totals();
update_freebusy_display(attendee.email);
}
}
}
return false;
});
// enable/disable buttons
$('#shedule-find-prev').button('option', 'disabled', (fb_start.getTime() < now.getTime()));
// dialog buttons
var buttons = {};
buttons[rcmail.gettext('select', 'calendar')] = function() {
$('#edit-startdate').val(freebusy_ui.startdate.val());
$('#edit-starttime').val(freebusy_ui.starttime.val());
$('#edit-enddate').val(freebusy_ui.enddate.val());
$('#edit-endtime').val(freebusy_ui.endtime.val());
// write role changes back to main dialog
$('select.edit-attendee-role').each(function(i, elem){
if (event_attendees[i] && freebusy_ui.attendees[i]) {
event_attendees[i].role = freebusy_ui.attendees[i].role;
$(elem).val(event_attendees[i].role);
}
});
if (freebusy_ui.needsupdate)
update_freebusy_status(me.selected_event);
freebusy_ui.needsupdate = false;
$dialog.dialog("close");
};
buttons[rcmail.gettext('cancel', 'calendar')] = function() {
$dialog.dialog("close");
};
$dialog.dialog({
modal: true,
resizable: true,
closeOnEscape: (!bw.ie6 && !bw.ie7),
title: rcmail.gettext('scheduletime', 'calendar'),
close: function() {
if (bw.ie6)
$("#edit-attendees-table").css('visibility','visible');
$dialog.dialog("destroy").hide();
},
resizeStop: function() {
render_freebusy_overlay();
},
buttons: buttons,
minWidth: 640,
width: 850
}).show();
// hide edit dialog on IE6 because of drop-down elements
if (bw.ie6)
$("#edit-attendees-table").css('visibility','hidden');
// adjust dialog size to fit grid without scrolling
var gridw = $('#schedule-freebusy-times').width();
var overflow = gridw - $('#attendees-freebusy-table td.times').width() + 1;
me.dialog_resize($dialog.get(0), $dialog.height() + (bw.ie ? 20 : 0), 800 + Math.max(0, overflow));
// fetch data from server
freebusy_ui.loading = 0;
load_freebusy_data(freebusy_ui.start, freebusy_ui.interval);
};
// render an HTML table showing free-busy status for all the event attendees
var render_freebusy_grid = function(delta)
{
if (delta) {
freebusy_ui.start.setTime(freebusy_ui.start.getTime() + DAY_MS * delta);
// skip weekends if in workinhoursonly-mode
if (Math.abs(delta) == 1 && freebusy_ui.workinhoursonly) {
while (is_weekend(freebusy_ui.start))
freebusy_ui.start.setTime(freebusy_ui.start.getTime() + DAY_MS * delta);
}
freebusy_ui.end = new Date(freebusy_ui.start.getTime() + DAY_MS * freebusy_ui.numdays);
}
var dayslots = Math.floor(1440 / freebusy_ui.interval);
var date_format = 'ddd '+ (dayslots <= 2 ? settings.date_short : settings.date_format);
var lastdate, datestr, css,
curdate = new Date(),
allday = (freebusy_ui.interval == 1440),
times_css = (allday ? 'allday ' : ''),
dates_row = '<tr class="dates">',
times_row = '<tr class="times">',
slots_row = '';
for (var s = 0, t = freebusy_ui.start.getTime(); t < freebusy_ui.end.getTime(); s++) {
curdate.setTime(t);
datestr = fc.fullCalendar('formatDate', curdate, date_format);
if (datestr != lastdate) {
dates_row += '<th colspan="' + dayslots + '" class="boxtitle date' + $.fullCalendar.formatDate(curdate, 'ddMMyyyy') + '">' + Q(datestr) + '</th>';
lastdate = datestr;
}
// set css class according to working hours
css = is_weekend(curdate) || (freebusy_ui.interval <= 60 && !is_workinghour(curdate)) ? 'offhours' : 'workinghours';
times_row += '<td class="' + times_css + css + '" id="t-' + Math.floor(t/1000) + '">' + Q(allday ? rcmail.gettext('all-day','calendar') : $.fullCalendar.formatDate(curdate, settings['time_format'])) + '</td>';
slots_row += '<td class="' + css + ' unknown">&nbsp;</td>';
t += freebusy_ui.interval * 60000;
}
dates_row += '</tr>';
times_row += '</tr>';
// render list of attendees
var domid, data, list_html = '', times_html = '';
for (var i=0; i < event_attendees.length; i++) {
data = event_attendees[i];
domid = String(data.email).replace(rcmail.identifier_expr, '');
times_html += '<tr id="fbrow' + domid + '">' + slots_row + '</tr>';
}
// add line for all/required attendees
times_html += '<tr class="spacer"><td colspan="' + (dayslots * freebusy_ui.numdays) + '">&nbsp;</td>';
times_html += '<tr id="fbrowall">' + slots_row + '</tr>';
var table = $('#schedule-freebusy-times');
table.children('thead').html(dates_row + times_row);
table.children('tbody').html(times_html);
// initialize event handlers on grid
if (!freebusy_ui.grid_events) {
freebusy_ui.grid_events = true;
table.children('thead').click(function(e){
// move event to the clicked date/time
if (e.target.id && e.target.id.match(/t-(\d+)/)) {
var newstart = new Date(RegExp.$1 * 1000);
// set time to 00:00
if (me.selected_event.allDay) {
newstart.setMinutes(0);
newstart.setHours(0);
}
update_freebusy_dates(newstart, new Date(newstart.getTime() + freebusy_ui.startdate.data('duration') * 1000));
render_freebusy_overlay();
}
})
}
// if we have loaded free-busy data, show it
if (!freebusy_ui.loading) {
if (date2unixtime(freebusy_ui.start) < freebusy_data.start || date2unixtime(freebusy_ui.end) > freebusy_data.end || freebusy_ui.interval != freebusy_data.interval) {
load_freebusy_data(freebusy_ui.start, freebusy_ui.interval);
}
else {
for (var email, i=0; i < event_attendees.length; i++) {
if ((email = event_attendees[i].email))
update_freebusy_display(email);
}
}
}
// render current event date/time selection over grid table
// use timeout to let the dom attributes (width/height/offset) be set first
window.setTimeout(function(){ render_freebusy_overlay(); }, 10);
};
// render overlay element over the grid to visiualize the current event date/time
var render_freebusy_overlay = function()
{
var overlay = $('#schedule-event-time');
if (me.selected_event.end.getTime() <= freebusy_ui.start.getTime() || me.selected_event.start.getTime() >= freebusy_ui.end.getTime()) {
overlay.draggable('disable').hide();
}
else {
var table = $('#schedule-freebusy-times'),
width = 0,
pos = { top:table.children('thead').height(), left:0 },
eventstart = date2unixtime(clone_date(me.selected_event.start, me.selected_event.allDay?1:0)),
eventend = date2unixtime(clone_date(me.selected_event.end, me.selected_event.allDay?2:0)) - 60,
slotstart = date2unixtime(freebusy_ui.start),
slotsize = freebusy_ui.interval * 60,
slotend, fraction, $cell;
// iterate through slots to determine position and size of the overlay
table.children('thead').find('td').each(function(i, cell){
slotend = slotstart + slotsize - 1;
// event starts in this slot: compute left
if (eventstart >= slotstart && eventstart <= slotend) {
fraction = 1 - (slotend - eventstart) / slotsize;
pos.left = Math.round(cell.offsetLeft + cell.offsetWidth * fraction);
}
// event ends in this slot: compute width
if (eventend >= slotstart && eventend <= slotend) {
fraction = 1 - (slotend - eventend) / slotsize;
width = Math.round(cell.offsetLeft + cell.offsetWidth * fraction) - pos.left;
}
slotstart = slotstart + slotsize;
});
if (!width)
width = table.width() - pos.left;
// overlay is visible
if (width > 0) {
overlay.css({ width: (width-5)+'px', height:(table.children('tbody').height() - 4)+'px', left:pos.left+'px', top:pos.top+'px' }).draggable('enable').show();
// configure draggable
if (!overlay.data('isdraggable')) {
overlay.draggable({
axis: 'x',
scroll: true,
stop: function(e, ui){
// convert pixels to time
var px = ui.position.left;
var range_p = $('#schedule-freebusy-times').width();
var range_t = freebusy_ui.end.getTime() - freebusy_ui.start.getTime();
var newstart = new Date(freebusy_ui.start.getTime() + px * (range_t / range_p));
newstart.setSeconds(0); newstart.setMilliseconds(0);
// snap to day boundaries
if (me.selected_event.allDay) {
if (newstart.getHours() >= 12) // snap to next day
newstart.setTime(newstart.getTime() + DAY_MS);
newstart.setMinutes(0);
newstart.setHours(0);
}
else {
// round to 5 minutes
var round = newstart.getMinutes() % 5;
if (round > 2.5) newstart.setTime(newstart.getTime() + (5 - round) * 60000);
else if (round > 0) newstart.setTime(newstart.getTime() - round * 60000);
}
// update event times and display
update_freebusy_dates(newstart, new Date(newstart.getTime() + freebusy_ui.startdate.data('duration') * 1000));
if (me.selected_event.allDay)
render_freebusy_overlay();
}
}).data('isdraggable', true);
}
}
else
overlay.draggable('disable').hide();
}
};
// fetch free-busy information for each attendee from server
var load_freebusy_data = function(from, interval)
{
var start = new Date(from.getTime() - DAY_MS * 2); // start 1 days before event
var end = new Date(start.getTime() + DAY_MS * Math.max(14, freebusy_ui.numdays + 7)); // load min. 14 days
freebusy_ui.numrequired = 0;
freebusy_data.all = [];
freebusy_data.required = [];
// load free-busy information for every attendee
var domid, email;
for (var i=0; i < event_attendees.length; i++) {
if ((email = event_attendees[i].email)) {
domid = String(email).replace(rcmail.identifier_expr, '');
$('#rcmli' + domid).addClass('loading');
freebusy_ui.loading++;
$.ajax({
type: 'GET',
dataType: 'json',
url: rcmail.url('freebusy-times'),
data: { email:email, start:date2unixtime(clone_date(start, 1)), end:date2unixtime(clone_date(end, 2)), interval:interval, _remote:1 },
success: function(data) {
freebusy_ui.loading--;
// find attendee
var attendee = null;
for (var i=0; i < event_attendees.length; i++) {
if (freebusy_ui.attendees[i].email == data.email) {
attendee = freebusy_ui.attendees[i];
break;
}
}
// copy data to member var
var req = attendee.role != 'OPT-PARTICIPANT';
var ts = data.start - 0;
freebusy_data.start = ts;
freebusy_data[data.email] = {};
for (var i=0; i < data.slots.length; i++) {
freebusy_data[data.email][ts] = data.slots[i];
// set totals
if (!freebusy_data.required[ts])
freebusy_data.required[ts] = [0,0,0,0];
if (req)
freebusy_data.required[ts][data.slots[i]]++;
if (!freebusy_data.all[ts])
freebusy_data.all[ts] = [0,0,0,0];
freebusy_data.all[ts][data.slots[i]]++;
ts += data.interval * 60;
}
freebusy_data.end = ts;
freebusy_data.interval = data.interval;
// hide loading indicator
var domid = String(data.email).replace(rcmail.identifier_expr, '');
$('#rcmli' + domid).removeClass('loading');
// update display
update_freebusy_display(data.email);
}
});
// count required attendees
if (freebusy_ui.attendees[i].role != 'OPT-PARTICIPANT')
freebusy_ui.numrequired++;
}
}
};
// re-calculate total status after role change
var compute_freebusy_totals = function()
{
freebusy_ui.numrequired = 0;
freebusy_data.all = [];
freebusy_data.required = [];
var email, req, status;
for (var i=0; i < event_attendees.length; i++) {
if (!(email = event_attendees[i].email))
continue;
req = freebusy_ui.attendees[i].role != 'OPT-PARTICIPANT';
if (req)
freebusy_ui.numrequired++;
for (var ts in freebusy_data[email]) {
if (!freebusy_data.required[ts])
freebusy_data.required[ts] = [0,0,0,0];
if (!freebusy_data.all[ts])
freebusy_data.all[ts] = [0,0,0,0];
status = freebusy_data[email][ts];
freebusy_data.all[ts][status]++;
if (req)
freebusy_data.required[ts][status]++;
}
}
};
// update free-busy grid with status loaded from server
var update_freebusy_display = function(email)
{
var status_classes = ['unknown','free','busy','tentative','out-of-office'];
var domid = String(email).replace(rcmail.identifier_expr, '');
var row = $('#fbrow' + domid);
var rowall = $('#fbrowall').children();
var ts = date2unixtime(freebusy_ui.start);
var fbdata = freebusy_data[email];
if (fbdata && fbdata[ts] !== undefined && row.length) {
row.children().each(function(i, cell){
cell.className = cell.className.replace('unknown', fbdata[ts] ? status_classes[fbdata[ts]] : 'unknown');
// also update total row if all data was loaded
if (freebusy_ui.loading == 0 && freebusy_data.all[ts] && (cell = rowall.get(i))) {
var workinghours = cell.className.indexOf('workinghours') >= 0;
var all_status = freebusy_data.all[ts][2] ? 'busy' : 'unknown';
req_status = freebusy_data.required[ts][2] ? 'busy' : 'free';
for (var j=1; j < status_classes.length; j++) {
if (freebusy_ui.numrequired && freebusy_data.required[ts][j] >= freebusy_ui.numrequired)
req_status = status_classes[j];
if (freebusy_data.all[ts][j] == event_attendees.length)
all_status = status_classes[j];
}
cell.className = (workinghours ? 'workinghours ' : 'offhours ') + req_status + ' all-' + all_status;
}
ts += freebusy_ui.interval * 60;
});
}
};
// write changed event date/times back to form fields
var update_freebusy_dates = function(start, end)
{
if (me.selected_event.allDay) {
start.setHours(12);
start.setMinutes(0);
end.setHours(13);
end.setMinutes(0);
}
me.selected_event.start = start;
me.selected_event.end = end;
freebusy_ui.startdate.val($.fullCalendar.formatDate(start, settings['date_format']));
freebusy_ui.starttime.val($.fullCalendar.formatDate(start, settings['time_format']));
freebusy_ui.enddate.val($.fullCalendar.formatDate(end, settings['date_format']));
freebusy_ui.endtime.val($.fullCalendar.formatDate(end, settings['time_format']));
freebusy_ui.needsupdate = true;
};
// attempt to find a time slot where all attemdees are available
var freebusy_find_slot = function(dir)
{
var event = me.selected_event,
eventstart = date2unixtime(event.start), // calculate with unixtimes
eventend = date2unixtime(event.end),
duration = eventend - eventstart,
sinterval = freebusy_data.interval * 60,
intvlslots = 1,
numslots = Math.ceil(duration / sinterval),
checkdate, slotend, email, curdate;
// shift event times to next possible slot
eventstart += sinterval * intvlslots * dir;
eventend += sinterval * intvlslots * dir;
// iterate through free-busy slots and find candidates
var candidatecount = 0, candidatestart = candidateend = success = false;
for (var slot = dir > 0 ? freebusy_data.start : freebusy_data.end - sinterval; (dir > 0 && slot < freebusy_data.end) || (dir < 0 && slot >= freebusy_data.start); slot += sinterval * dir) {
slotend = slot + sinterval;
if ((dir > 0 && slotend <= eventstart) || (dir < 0 && slot >= eventend)) // skip
continue;
// respect workingours setting
if (freebusy_ui.workinhoursonly) {
curdate = fromunixtime(dir > 0 || !candidateend ? slot : (candidateend - duration));
if (is_weekend(curdate) || (freebusy_data.interval <= 60 && !is_workinghour(curdate))) { // skip off-hours
candidatestart = candidateend = false;
candidatecount = 0;
continue;
}
}
if (!candidatestart)
candidatestart = slot;
// check freebusy data for all attendees
for (var i=0; i < event_attendees.length; i++) {
if (freebusy_ui.attendees[i].role != 'OPT-PARTICIPANT' && (email = freebusy_ui.attendees[i].email) && freebusy_data[email] && freebusy_data[email][slot] > 1) {
candidatestart = candidateend = false;
break;
}
}
// occupied slot
if (!candidatestart) {
slot += Math.max(0, intvlslots - candidatecount - 1) * sinterval * dir;
candidatecount = 0;
continue;
}
// set candidate end to slot end time
candidatecount++;
if (dir < 0 && !candidateend)
candidateend = slotend;
// if candidate is big enough, this is it!
if (candidatecount == numslots) {
if (dir > 0) {
event.start = fromunixtime(candidatestart);
event.end = fromunixtime(candidatestart + duration);
}
else {
event.end = fromunixtime(candidateend);
event.start = fromunixtime(candidateend - duration);
}
success = true;
break;
}
}
// update event date/time display
if (success) {
update_freebusy_dates(event.start, event.end);
// move freebusy grid if necessary
var offset = Math.ceil((event.start.getTime() - freebusy_ui.end.getTime()) / DAY_MS);
if (event.start.getTime() >= freebusy_ui.end.getTime())
render_freebusy_grid(Math.max(1, offset));
else if (event.end.getTime() <= freebusy_ui.start.getTime())
render_freebusy_grid(Math.min(-1, offset));
else
render_freebusy_overlay();
var now = new Date();
$('#shedule-find-prev').button('option', 'disabled', (event.start.getTime() < now.getTime()));
}
else {
alert(rcmail.gettext('noslotfound','calendar'));
}
};
// update event properties and attendees availability if event times have changed
var event_times_changed = function()
{
if (me.selected_event) {
var allday = $('#edit-allday').get(0);
me.selected_event.allDay = allday.checked;
me.selected_event.start = parse_datetime(allday.checked ? '12:00' : $('#edit-starttime').val(), $('#edit-startdate').val());
me.selected_event.end = parse_datetime(allday.checked ? '13:00' : $('#edit-endtime').val(), $('#edit-enddate').val());
if (event_attendees)
freebusy_ui.needsupdate = true;
$('#edit-startdate').data('duration', Math.round((me.selected_event.end.getTime() - me.selected_event.start.getTime()) / 1000));
}
};
// add the given list of participants
var add_attendees = function(names)
{
names = explode_quoted_string(names.replace(/,\s*$/, ''), ',');
// parse name/email pairs
var item, email, name, success = false;
for (var i=0; i < names.length; i++) {
email = name = '';
item = $.trim(names[i]);
if (!item.length) {
continue;
} // address in brackets without name (do nothing)
else if (item.match(/^<[^@]+@[^>]+>$/)) {
email = item.replace(/[<>]/g, '');
} // address without brackets and without name (add brackets)
else if (rcube_check_email(item)) {
email = item;
} // address with name
else if (item.match(/([^\s<@]+@[^>]+)>*$/)) {
email = RegExp.$1;
name = item.replace(email, '').replace(/^["\s<>]+/, '').replace(/["\s<>]+$/, '');
}
if (email) {
add_attendee({ email:email, name:name, role:'REQ-PARTICIPANT', status:'NEEDS-ACTION' });
success = true;
}
else {
alert(rcmail.gettext('noemailwarning'));
}
}
return success;
};
// add the given attendee to the list
var add_attendee = function(data, readonly)
{
// check for dupes...
var exists = false;
$.each(event_attendees, function(i, v){ exists |= (v.email == data.email); });
if (exists)
return false;
var dispname = Q(data.name || data.email);
if (data.email)
dispname = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink">' + dispname + '</a>';
// role selection
var organizer = data.role == 'ORGANIZER';
var opts = {};
if (organizer)
opts.ORGANIZER = rcmail.gettext('calendar.roleorganizer');
opts['REQ-PARTICIPANT'] = rcmail.gettext('calendar.rolerequired');
opts['OPT-PARTICIPANT'] = rcmail.gettext('calendar.roleoptional');
opts['CHAIR'] = rcmail.gettext('calendar.roleresource');
var select = '<select class="edit-attendee-role"' + (organizer || readonly ? ' disabled="true"' : '') + '>';
for (var r in opts)
select += '<option value="'+ r +'" class="' + r.toLowerCase() + '"' + (data.role == r ? ' selected="selected"' : '') +'>' + Q(opts[r]) + '</option>';
select += '</select>';
// availability
var avail = data.email ? 'loading' : 'unknown';
// delete icon
var icon = rcmail.env.deleteicon ? '<img src="' + rcmail.env.deleteicon + '" alt="" />' : rcmail.gettext('delete');
var dellink = '<a href="#delete" class="deletelink" title="' + Q(rcmail.gettext('delete')) + '">' + icon + '</a>';
var html = '<td class="role">' + select + '</td>' +
'<td class="name">' + dispname + '</td>' +
'<td class="availability"><img src="./program/blank.gif" class="availabilityicon ' + avail + '" /></td>' +
'<td class="confirmstate"><span class="' + String(data.status).toLowerCase() + '">' + Q(data.status) + '</span></td>' +
'<td class="options">' + (organizer || readonly ? '' : dellink) + '</td>';
var tr = $('<tr>')
.addClass(String(data.role).toLowerCase())
.html(html)
.appendTo(attendees_list);
tr.find('a.deletelink').click({ id:(data.email || data.name) }, function(e) { remove_attendee(this, e.data.id); return false; });
tr.find('a.mailtolink').click(function(e) { rcmail.redirect(rcmail.url('mail/compose', { _to:this.href.substr(7) })); return false; });
// check free-busy status
if (avail == 'loading') {
check_freebusy_status(tr.find('img.availabilityicon'), data.email, me.selected_event);
}
event_attendees.push(data);
};
// iterate over all attendees and update their free-busy status display
var update_freebusy_status = function(event)
{
var icons = attendees_list.find('img.availabilityicon');
for (var i=0; i < event_attendees.length; i++) {
if (icons.get(i) && event_attendees[i].email)
check_freebusy_status(icons.get(i), event_attendees[i].email, event);
}
freebusy_ui.needsupdate = false;
};
// load free-busy status from server and update icon accordingly
var check_freebusy_status = function(icon, email, event)
{
var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { freebusy:false };
if (!calendar.freebusy) {
$(icon).removeClass().addClass('availabilityicon unknown');
return;
}
icon = $(icon).removeClass().addClass('availabilityicon loading');
$.ajax({
type: 'GET',
dataType: 'html',
url: rcmail.url('freebusy-status'),
data: { email:email, start:date2unixtime(clone_date(event.start, event.allDay?1:0)), end:date2unixtime(clone_date(event.end, event.allDay?2:0)), _remote: 1 },
success: function(status){
icon.removeClass('loading').addClass(String(status).toLowerCase());
},
error: function(){
icon.removeClass('loading').addClass('unknown');
}
});
};
// remove an attendee from the list
var remove_attendee = function(elem, id)
{
$(elem).closest('tr').remove();
event_attendees = $.grep(event_attendees, function(data){ return (data.name != id && data.email != id) });
};
// when the user accepts or declines an event invitation
var event_rsvp = function(response)
{
if (me.selected_event && me.selected_event.attendees && response) {
// update attendee status
for (var data, i=0; i < me.selected_event.attendees.length; i++) {
data = me.selected_event.attendees[i];
if (settings.identity.emails.indexOf(';'+data.email) >= 0)
data.status = response.toUpperCase();
}
event_show_dialog(me.selected_event);
// submit status change to server
me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
rcmail.http_post('event', { action:'rsvp', e:me.selected_event, status:response });
}
}
// post the given event data to server
var update_event = function(action, data)
{
me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
rcmail.http_post('calendar/event', { action:action, e:data });
// render event temporarily into the calendar
if ((data.start && data.end) || data.id) {
var event = data.id ? $.extend(fc.fullCalendar('clientEvents', data.id)[0], data) : data;
if (data.start)
event.start = fromunixtime(data.start);
if (data.end)
event.end = fromunixtime(data.end);
if (data.allday !== undefined)
event.allDay = data.allday;
event.editable = false;
event.temp = true;
event.className = 'fc-event-cal-'+data.calendar+' fc-event-temp';
fc.fullCalendar(data.id ? 'updateEvent' : 'renderEvent', event);
}
};
// mouse-click handler to check if the show dialog is still open and prevent default action
var dialog_check = function(e)
{
var showd = $("#eventshow");
if (showd.is(':visible') && !$(e.target).closest('.ui-dialog').length) {
showd.dialog('close');
e.stopImmediatePropagation();
ignore_click = true;
return false;
}
else if (ignore_click) {
window.setTimeout(function(){ ignore_click = false; }, 20);
return false;
}
return true;
};
// display confirm dialog when modifying/deleting an event
var update_event_confirm = function(action, event, data)
{
if (!data) data = event;
var decline = false, notify = false, html = '', cal = me.calendars[event.calendar];
// event has attendees, ask whether to notify them
if (has_attendees(event)) {
if (is_organizer(event)) {
notify = true;
html += '<div class="message">' +
'<label><input class="confirm-attendees-donotify" type="checkbox" checked="checked" value="1" name="notify" />&nbsp;' +
rcmail.gettext((action == 'remove' ? 'sendcancellation' : 'sendnotifications'), 'calendar') +
'</label></div>';
}
else if (action == 'remove' && is_attendee(event)) {
decline = true;
html += '<div class="message">' +
'<label><input class="confirm-attendees-decline" type="checkbox" checked="checked" value="1" name="decline" />&nbsp;' +
rcmail.gettext('itipdeclineevent', 'calendar') +
'</label></div>';
}
else {
html += '<div class="message">' + rcmail.gettext('localchangeswarning', 'calendar') + '</div>';
}
}
// recurring event: user needs to select the savemode
if (event.recurrence) {
html += '<div class="message"><span class="ui-icon ui-icon-alert"></span>' +
rcmail.gettext((action == 'remove' ? 'removerecurringeventwarning' : 'changerecurringeventwarning'), 'calendar') + '</div>' +
'<div class="savemode">' +
'<a href="#current" class="button">' + rcmail.gettext('currentevent', 'calendar') + '</a>' +
'<a href="#future" class="button">' + rcmail.gettext('futurevents', 'calendar') + '</a>' +
'<a href="#all" class="button">' + rcmail.gettext('allevents', 'calendar') + '</a>' +
(action != 'remove' ? '<a href="#new" class="button">' + rcmail.gettext('saveasnew', 'calendar') + '</a>' : '') +
'</div>';
}
// show dialog
if (html) {
var $dialog = $('<div>').html(html);
$dialog.find('a.button').button().click(function(e){
data._savemode = String(this.href).replace(/.+#/, '');
if ($dialog.find('input.confirm-attendees-donotify').get(0))
data._notify = notify && $dialog.find('input.confirm-attendees-donotify').get(0).checked ? 1 : 0;
if (decline && $dialog.find('input.confirm-attendees-decline:checked'))
data.decline = 1;
update_event(action, data);
$dialog.dialog("destroy").hide();
return false;
});
var buttons = [{
text: rcmail.gettext('cancel', 'calendar'),
click: function() {
$(this).dialog("close");
}
}];
if (!event.recurrence) {
buttons.push({
text: rcmail.gettext((action == 'remove' ? 'remove' : 'save'), 'calendar'),
click: function() {
data._notify = notify && $dialog.find('input.confirm-attendees-donotify').get(0).checked ? 1 : 0;
data.decline = decline && $dialog.find('input.confirm-attendees-decline:checked').length ? 1 : 0;
update_event(action, data);
$(this).dialog("close");
}
});
}
$dialog.dialog({
modal: true,
width: 460,
dialogClass: 'warning',
title: rcmail.gettext((action == 'remove' ? 'removeeventconfirm' : 'changeeventconfirm'), 'calendar'),
buttons: buttons,
close: function(){
$dialog.dialog("destroy").hide();
if (!rcmail.busy)
fc.fullCalendar('refetchEvents');
}
}).addClass('event-update-confirm').show();
return false;
}
// show regular confirm box when deleting
else if (action == 'remove' && !cal.undelete) {
if (!confirm(rcmail.gettext('deleteventconfirm', 'calendar')))
return false;
}
// do update
update_event(action, data);
return true;
};
var update_agenda_toolbar = function()
{
$('#agenda-listrange').val(fc.fullCalendar('option', 'listRange'));
$('#agenda-listsections').val(fc.fullCalendar('option', 'listSections'));
}
/*** fullcalendar event handlers ***/
var fc_event_render = function(event, element, view) {
if (view.name != 'list' && view.name != 'table') {
var prefix = event.sensitivity != 0 ? String(sensitivitylabels[event.sensitivity]).toUpperCase()+': ' : '';
element.attr('title', prefix + event.title);
}
if (view.name != 'month') {
if (event.location) {
element.find('div.fc-event-title').after('<div class="fc-event-location">@&nbsp;' + Q(event.location) + '</div>');
}
if (event.sensitivity != 0)
element.find('div.fc-event-time').append('<i class="fc-icon-sensitive"></i>');
if (event.recurrence)
element.find('div.fc-event-time').append('<i class="fc-icon-recurring"></i>');
if (event.alarms)
element.find('div.fc-event-time').append('<i class="fc-icon-alarms"></i>');
}
};
/*** public methods ***/
// opens calendar day-view in a popup
this.fisheye_view = function(date)
{
$('#fish-eye-view').dialog('close');
// create list of active event sources
var src, cals = {}, sources = [];
for (var id in this.calendars) {
src = $.extend({}, this.calendars[id]);
src.editable = false;
src.url = null;
src.events = [];
if (src.active) {
cals[id] = src;
sources.push(src);
}
}
// copy events already loaded
var events = fc.fullCalendar('clientEvents');
for (var event, i=0; i< events.length; i++) {
event = events[i];
if (event.source && (src = cals[event.source.id])) {
src.events.push(event);
}
}
var h = $(window).height() - 50;
var dialog = $('<div>')
.attr('id', 'fish-eye-view')
.dialog({
modal: true,
width: 680,
height: h,
title: $.fullCalendar.formatDate(date, 'dddd ' + settings['date_long']),
close: function(){
dialog.dialog("destroy");
me.fisheye_date = null;
}
})
.fullCalendar({
header: { left: '', center: '', right: '' },
height: h - 50,
defaultView: 'agendaDay',
date: date.getDate(),
month: date.getMonth(),
year: date.getFullYear(),
ignoreTimezone: true, // will treat the given date strings as in local (browser's) timezone
eventSources: sources,
monthNames : settings['months'],
monthNamesShort : settings['months_short'],
dayNames : settings['days'],
dayNamesShort : settings['days_short'],
firstDay : settings['first_day'],
firstHour : settings['first_hour'],
slotMinutes : 60/settings['timeslots'],
timeFormat: { '': settings['time_format'] },
axisFormat : settings['time_format'],
columnFormat: { day: 'dddd ' + settings['date_short'] },
titleFormat: { day: 'dddd ' + settings['date_long'] },
allDayText: rcmail.gettext('all-day', 'calendar'),
currentTimeIndicator: settings.time_indicator,
eventRender: fc_event_render,
eventClick: function(event) {
event_show_dialog(event);
}
});
this.fisheye_date = date;
};
//public method to show the print dialog.
this.print_calendars = function(view)
{
if (!view) view = fc.fullCalendar('getView').name;
var date = fc.fullCalendar('getDate') || new Date();
var range = fc.fullCalendar('option', 'listRange');
var sections = fc.fullCalendar('option', 'listSections');
var printwin = window.open(rcmail.url('print', { view: view, date: date2unixtime(date), range: range, sections: sections, search: this.search_query }), "rc_print_calendars", "toolbar=no,location=yes,menubar=yes,resizable=yes,scrollbars=yes,width=800");
window.setTimeout(function(){ printwin.focus() }, 50);
};
// public method to bring up the new event dialog
this.add_event = function(templ) {
if (this.selected_calendar) {
var now = new Date();
var date = fc.fullCalendar('getDate');
if (typeof date != 'Date')
date = now;
date.setHours(now.getHours()+1);
date.setMinutes(0);
var end = new Date(date.getTime());
end.setHours(date.getHours()+1);
event_edit_dialog('new', $.extend({ start:date, end:end, allDay:false, calendar:this.selected_calendar }, templ || {}));
}
};
// delete the given event after showing a confirmation dialog
this.delete_event = function(event) {
// show confirm dialog for recurring events, use jquery UI dialog
return update_event_confirm('remove', event, { id:event.id, calendar:event.calendar, attendees:event.attendees });
};
// opens a jquery UI dialog with event properties (or empty for creating a new calendar)
this.calendar_edit_dialog = function(calendar)
{
// close show dialog first
var $dialog = $("#calendarform").dialog('close');
if (!calendar)
calendar = { name:'', color:'cc0000', editable:true, showalarms:true };
var form, name, color, alarms;
$dialog.html(rcmail.get_label('loading'));
$.ajax({
type: 'GET',
dataType: 'html',
url: rcmail.url('calendar'),
data: { action:(calendar.id ? 'form-edit' : 'form-new'), c:{ id:calendar.id } },
success: function(data) {
$dialog.html(data);
// resize and reposition dialog window
form = $('#calendarpropform');
me.dialog_resize('#calendarform', form.height(), form.width());
name = $('#calendar-name').prop('disabled', !calendar.editable).val(calendar.editname || calendar.name);
color = $('#calendar-color').val(calendar.color).miniColors({ value: calendar.color, colorValues:rcmail.env.mscolors });
alarms = $('#calendar-showalarms').prop('checked', calendar.showalarms).get(0);
name.select();
}
});
// dialog buttons
var buttons = {};
buttons[rcmail.gettext('save', 'calendar')] = function() {
// form is not loaded
if (!form || !form.length)
return;
// TODO: do some input validation
if (!name.val() || name.val().length < 2) {
alert(rcmail.gettext('invalidcalendarproperties', 'calendar'));
name.select();
return;
}
// post data to server
var data = form.serializeJSON();
if (data.color)
data.color = data.color.replace(/^#/, '');
if (calendar.id)
data.id = calendar.id;
if (alarms)
data.showalarms = alarms.checked ? 1 : 0;
me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
rcmail.http_post('calendar', { action:(calendar.id ? 'edit' : 'new'), c:data });
$dialog.dialog("close");
};
buttons[rcmail.gettext('cancel', 'calendar')] = function() {
$dialog.dialog("close");
};
// open jquery UI dialog
$dialog.dialog({
modal: true,
resizable: true,
closeOnEscape: false,
title: rcmail.gettext((calendar.id ? 'editcalendar' : 'createcalendar'), 'calendar'),
close: function() {
$dialog.html('').dialog("destroy").hide();
},
buttons: buttons,
minWidth: 400,
width: 420
}).show();
};
this.calendar_remove = function(calendar)
{
if (confirm(rcmail.gettext('deletecalendarconfirm', 'calendar'))) {
rcmail.http_post('calendar', { action:'remove', c:{ id:calendar.id } });
return true;
}
return false;
};
this.calendar_destroy_source = function(id)
{
if (this.calendars[id]) {
fc.fullCalendar('removeEventSource', this.calendars[id]);
$(rcmail.get_folder_li(id, 'rcmlical')).remove();
$('#edit-calendar option[value="'+id+'"]').remove();
delete this.calendars[id];
}
};
// open a dialog to upload an .ics file with events to be imported
this.import_events = function(calendar)
{
// close show dialog first
var $dialog = $("#eventsimport").dialog('close');
var form = rcmail.gui_objects.importform;
$('#event-import-calendar').val(calendar.id);
var buttons = {};
buttons[rcmail.gettext('import', 'calendar')] = function() {
if (form && form.elements._data.value) {
rcmail.async_upload_form(form, 'import_events', function(e) {
rcmail.set_busy(false, null, me.saving_lock);
});
// display upload indicator
me.saving_lock = rcmail.set_busy(true, 'uploading');
}
};
buttons[rcmail.gettext('cancel', 'calendar')] = function() {
$dialog.dialog("close");
};
// open jquery UI dialog
$dialog.dialog({
modal: true,
resizable: false,
closeOnEscape: false,
title: rcmail.gettext('importevents', 'calendar'),
close: function() {
$dialog.dialog("destroy").hide();
},
buttons: buttons,
width: 520
}).show();
};
// callback from server if import succeeded
this.import_success = function(p)
{
$("#eventsimport").dialog('close');
rcmail.set_busy(false, null, me.saving_lock);
rcmail.gui_objects.importform.reset();
if (p.refetch)
this.refresh(p);
};
// show URL of the given calendar in a dialog box
this.showurl = function(calendar)
{
var $dialog = $('#calendarurlbox').dialog('close');
if (calendar.feedurl) {
$dialog.dialog({
resizable: true,
closeOnEscape: true,
title: rcmail.gettext('showurl', 'calendar'),
close: function() {
$dialog.dialog("destroy").hide();
},
width: 520
}).show();
$('#calfeedurl').val(calendar.feedurl).select();
}
};
// refresh the calendar view after saving event data
this.refresh = function(p)
{
var source = me.calendars[p.source];
if (source && (p.refetch || (p.update && !source.active))) {
// activate event source if new event was added to an invisible calendar
if (!source.active) {
source.active = true;
fc.fullCalendar('addEventSource', source);
$('#' + rcmail.get_folder_li(source.id, 'rcmlical').id + ' input').prop('checked', true);
}
else
fc.fullCalendar('refetchEvents', source);
}
// add/update single event object
else if (source && p.update) {
var event = p.update;
event.temp = false;
event.editable = source.editable;
var existing = fc.fullCalendar('clientEvents', event.id);
if (existing.length) {
$.extend(existing[0], event);
fc.fullCalendar('updateEvent', existing[0]);
}
else {
event.source = source; // link with source
fc.fullCalendar('renderEvent', event);
}
// refresh fish-eye view
if (me.fisheye_date)
me.fisheye_view(me.fisheye_date);
}
// remove temp events
fc.fullCalendar('removeEvents', function(e){ return e.temp; });
};
/*** event searching ***/
// execute search
this.quicksearch = function()
{
if (rcmail.gui_objects.qsearchbox) {
var q = rcmail.gui_objects.qsearchbox.value;
if (q != '') {
var id = 'search-'+q;
var sources = [];
if (this._search_message)
rcmail.hide_message(this._search_message);
for (var sid in this.calendars) {
if (this.calendars[sid]) {
this.calendars[sid].url = this.calendars[sid].url.replace(/&q=.+/, '') + '&q='+escape(q);
sources.push(sid);
}
}
id += '@'+sources.join(',');
// ignore if query didn't change
if (this.search_request == id) {
return;
}
// remember current view
else if (!this.search_request) {
this.default_view = fc.fullCalendar('getView').name;
}
this.search_request = id;
this.search_query = q;
// change to list view
fc.fullCalendar('option', 'listSections', 'month')
.fullCalendar('option', 'listRange', Math.max(60, settings['agenda_range']))
.fullCalendar('changeView', 'table');
update_agenda_toolbar();
// refetch events with new url (if not already triggered by changeView)
if (!this.is_loading)
fc.fullCalendar('refetchEvents');
}
else // empty search input equals reset
this.reset_quicksearch();
}
};
// reset search and get back to normal event listing
this.reset_quicksearch = function()
{
$(rcmail.gui_objects.qsearchbox).val('');
if (this._search_message)
rcmail.hide_message(this._search_message);
if (this.search_request) {
// hide bottom links of agenda view
fc.find('.fc-list-content > .fc-listappend').hide();
// restore original event sources and view mode from fullcalendar
fc.fullCalendar('option', 'listSections', settings['agenda_sections'])
.fullCalendar('option', 'listRange', settings['agenda_range']);
update_agenda_toolbar();
for (var sid in this.calendars) {
if (this.calendars[sid])
this.calendars[sid].url = this.calendars[sid].url.replace(/&q=.+/, '');
}
if (this.default_view)
fc.fullCalendar('changeView', this.default_view);
if (!this.is_loading)
fc.fullCalendar('refetchEvents');
this.search_request = this.search_query = null;
}
};
// callback if all sources have been fetched from server
this.events_loaded = function(count)
{
var addlinks, append = '';
// enhance list view when searching
if (this.search_request) {
if (!count) {
this._search_message = rcmail.display_message(rcmail.gettext('searchnoresults', 'calendar'), 'notice');
append = '<div class="message">' + rcmail.gettext('searchnoresults', 'calendar') + '</div>';
}
append += '<div class="fc-bottomlinks formlinks"></div>';
addlinks = true;
}
if (fc.fullCalendar('getView').name == 'table') {
var container = fc.find('.fc-list-content > .fc-listappend');
if (append) {
if (!container.length)
container = $('<div class="fc-listappend"></div>').appendTo(fc.find('.fc-list-content'));
container.html(append).show();
}
else if (container.length)
container.hide();
// add links to adjust search date range
if (addlinks) {
var lc = container.find('.fc-bottomlinks');
$('<a>').attr('href', '#').html(rcmail.gettext('searchearlierdates', 'calendar')).appendTo(lc).click(function(){
fc.fullCalendar('incrementDate', 0, -1, 0);
});
lc.append(" ");
$('<a>').attr('href', '#').html(rcmail.gettext('searchlaterdates', 'calendar')).appendTo(lc).click(function(){
var range = fc.fullCalendar('option', 'listRange');
if (range < 90) {
fc.fullCalendar('option', 'listRange', fc.fullCalendar('option', 'listRange') + 30).fullCalendar('render');
update_agenda_toolbar();
}
else
fc.fullCalendar('incrementDate', 0, 1, 0);
});
}
}
if (this.fisheye_date)
this.fisheye_view(this.fisheye_date);
};
// resize and reposition (center) the dialog window
this.dialog_resize = function(id, height, width)
{
var win = $(window), w = win.width(), h = win.height();
$(id).dialog('option', { height: Math.min(h-20, height+130), width: Math.min(w-20, width+50) })
.dialog('option', 'position', ['center', 'center']); // only works in a separate call (!?)
};
// adjust calendar view size
this.view_resize = function()
{
- var footer = fc.fullCalendar('getView').name == 'table' ? $('#agendaoptions').height() + 2 : 0;
+ var footer = fc.fullCalendar('getView').name == 'table' ? $('#agendaoptions').outerHeight() : 0;
fc.fullCalendar('option', 'height', $('#calendar').height() - footer);
};
/*** startup code ***/
// create list of event sources AKA calendars
this.calendars = {};
var li, cal, active, event_sources = [];
for (var id in rcmail.env.calendars) {
cal = rcmail.env.calendars[id];
this.calendars[id] = $.extend({
url: "./?_task=calendar&_action=load_events&source="+escape(id),
editable: !cal.readonly,
className: 'fc-event-cal-'+id,
id: id
}, cal);
this.calendars[id].color = settings.event_coloring % 2 ? '' : '#' + cal.color;
if ((active = cal.active || false)) {
event_sources.push(this.calendars[id]);
}
// init event handler on calendar list checkbox
if ((li = rcmail.get_folder_li(id, 'rcmlical'))) {
$('#'+li.id+' input').click(function(e){
var id = $(this).data('id');
if (me.calendars[id]) { // add or remove event source on click
var action;
if (this.checked) {
action = 'addEventSource';
me.calendars[id].active = true;
}
else {
action = 'removeEventSource';
me.calendars[id].active = false;
}
// add/remove event source
fc.fullCalendar(action, me.calendars[id]);
rcmail.http_post('calendar', { action:'subscribe', c:{ id:id, active:me.calendars[id].active?1:0 } });
}
}).data('id', id).get(0).checked = active;
$(li).click(function(e){
var id = $(this).data('id');
rcmail.select_folder(id, 'rcmlical');
rcmail.enable_command('calendar-edit', true);
- rcmail.enable_command('calendar-remove', 'events-import', 'calendar-showurl', !me.calendars[id].readonly);
+ rcmail.enable_command('calendar-remove', 'events-import', 'calendar-showurl', true);
me.selected_calendar = id;
})
.dblclick(function(){ me.calendar_edit_dialog(me.calendars[me.selected_calendar]); })
.data('id', id);
}
if (!cal.readonly && !this.selected_calendar) {
this.selected_calendar = id;
rcmail.enable_command('addevent', true);
}
}
// select default calendar
if (settings.default_calendar && this.calendars[settings.default_calendar] && !this.calendars[settings.default_calendar].readonly)
this.selected_calendar = settings.default_calendar;
var viewdate = new Date();
if (rcmail.env.date)
viewdate.setTime(fromunixtime(rcmail.env.date));
// initalize the fullCalendar plugin
var fc = $('#calendar').fullCalendar({
header: {
left: 'prev,next today',
center: 'title',
right: 'agendaDay,agendaWeek,month,table'
},
aspectRatio: 1,
date: viewdate.getDate(),
month: viewdate.getMonth(),
year: viewdate.getFullYear(),
ignoreTimezone: true, // will treat the given date strings as in local (browser's) timezone
height: $('#calendar').height(),
eventSources: event_sources,
monthNames : settings['months'],
monthNamesShort : settings['months_short'],
dayNames : settings['days'],
dayNamesShort : settings['days_short'],
firstDay : settings['first_day'],
firstHour : settings['first_hour'],
slotMinutes : 60/settings['timeslots'],
timeFormat: {
'': settings['time_format'],
agenda: settings['time_format'] + '{ - ' + settings['time_format'] + '}',
list: settings['time_format'] + '{ - ' + settings['time_format'] + '}',
table: settings['time_format'] + '{ - ' + settings['time_format'] + '}'
},
axisFormat : settings['time_format'],
columnFormat: {
month: 'ddd', // Mon
week: 'ddd ' + settings['date_short'], // Mon 9/7
day: 'dddd ' + settings['date_short'], // Monday 9/7
table: settings['date_agenda']
},
titleFormat: {
month: 'MMMM yyyy',
week: settings['dates_long'],
day: 'dddd ' + settings['date_long'],
table: settings['dates_long']
},
listPage: 1, // advance one day in agenda view
listRange: settings['agenda_range'],
listSections: settings['agenda_sections'],
tableCols: ['handle', 'date', 'time', 'title', 'location'],
defaultView: rcmail.env.view || settings['default_view'],
allDayText: rcmail.gettext('all-day', 'calendar'),
buttonText: {
prev: (bw.ie6 ? '&nbsp;&lt;&lt;&nbsp;' : '&nbsp;&#9668;&nbsp;'),
next: (bw.ie6 ? '&nbsp;&gt;&gt;&nbsp;' : '&nbsp;&#9658;&nbsp;'),
today: settings['today'],
day: rcmail.gettext('day', 'calendar'),
week: rcmail.gettext('week', 'calendar'),
month: rcmail.gettext('month', 'calendar'),
table: rcmail.gettext('agenda', 'calendar')
},
listTexts: {
until: rcmail.gettext('until', 'calendar'),
past: rcmail.gettext('pastevents', 'calendar'),
today: rcmail.gettext('today', 'calendar'),
tomorrow: rcmail.gettext('tomorrow', 'calendar'),
thisWeek: rcmail.gettext('thisweek', 'calendar'),
nextWeek: rcmail.gettext('nextweek', 'calendar'),
thisMonth: rcmail.gettext('thismonth', 'calendar'),
nextMonth: rcmail.gettext('nextmonth', 'calendar'),
future: rcmail.gettext('futureevents', 'calendar'),
week: rcmail.gettext('weekofyear', 'calendar')
},
selectable: true,
selectHelper: false,
currentTimeIndicator: settings.time_indicator,
loading: function(isLoading) {
me.is_loading = isLoading;
this._rc_loading = rcmail.set_busy(isLoading, 'loading', this._rc_loading);
// trigger callback
if (!isLoading)
me.events_loaded($(this).fullCalendar('clientEvents').length);
},
// event rendering
eventRender: fc_event_render,
// render element indicating more (invisible) events
overflowRender: function(data, element) {
element.html(rcmail.gettext('andnmore', 'calendar').replace('$nr', data.count))
.click(function(e){ me.fisheye_view(data.date); });
},
// callback for date range selection
select: function(start, end, allDay, e, view) {
var range_select = (!allDay || start.getDate() != end.getDate())
if (dialog_check(e) && range_select)
event_edit_dialog('new', { start:start, end:end, allDay:allDay, calendar:me.selected_calendar });
if (range_select || ignore_click)
view.calendar.unselect();
},
// callback for clicks in all-day box
dayClick: function(date, allDay, e, view) {
var now = new Date().getTime();
if (now - day_clicked_ts < 400 && day_clicked == date.getTime()) { // emulate double-click on day
var enddate = new Date(); enddate.setTime(date.getTime() + DAY_MS - 60000);
return event_edit_dialog('new', { start:date, end:enddate, allDay:allDay, calendar:me.selected_calendar });
}
if (!ignore_click) {
view.calendar.gotoDate(date);
if (day_clicked && new Date(day_clicked).getMonth() != date.getMonth())
view.calendar.select(date, date, allDay);
}
day_clicked = date.getTime();
day_clicked_ts = now;
},
// callback when a specific event is clicked
eventClick: function(event) {
if (!event.temp)
event_show_dialog(event);
},
// callback when an event was dragged and finally dropped
eventDrop: function(event, dayDelta, minuteDelta, allDay, revertFunc) {
if (event.end == null || event.end.getTime() < event.start.getTime()) {
event.end = new Date(event.start.getTime() + (allDay ? DAY_MS : HOUR_MS));
}
// moved to all-day section: set times to 12:00 - 13:00
if (allDay && !event.allday) {
event.start.setHours(12);
event.start.setMinutes(0);
event.start.setSeconds(0);
event.end.setHours(13);
event.end.setMinutes(0);
event.end.setSeconds(0);
}
// moved from all-day section: set times to working hours
else if (event.allday && !allDay) {
var newstart = event.start.getTime();
revertFunc(); // revert to get original duration
var numdays = Math.max(1, Math.round((event.end.getTime() - event.start.getTime()) / DAY_MS)) - 1;
event.start = new Date(newstart);
event.end = new Date(newstart + numdays * DAY_MS);
event.end.setHours(settings['work_end'] || 18);
event.end.setMinutes(0);
if (event.end.getTime() < event.start.getTime())
event.end = new Date(newstart + HOUR_MS);
}
// send move request to server
var data = {
id: event.id,
calendar: event.calendar,
start: date2unixtime(event.start),
end: date2unixtime(event.end),
allday: allDay?1:0
};
update_event_confirm('move', event, data);
},
// callback for event resizing
eventResize: function(event, delta) {
// sanitize event dates
if (event.allDay)
event.start.setHours(12);
if (!event.end || event.end.getTime() < event.start.getTime())
event.end = new Date(event.start.getTime() + HOUR_MS);
// send resize request to server
var data = {
id: event.id,
calendar: event.calendar,
start: date2unixtime(event.start),
end: date2unixtime(event.end)
};
update_event_confirm('resize', event, data);
},
viewDisplay: function(view) {
$('#agendaoptions')[view.name == 'table' ? 'show' : 'hide']();
if (minical) {
window.setTimeout(function(){ minical.datepicker('setDate', fc.fullCalendar('getDate')); }, exec_deferred);
if (view.name != current_view)
me.view_resize();
current_view = view.name;
}
},
viewRender: function(view) {
if (view.name == 'month')
fc.fullCalendar('option', 'maxHeight', Math.floor((view.element.parent().height()-18) / 6) - 35);
}
});
// format time string
var formattime = function(hour, minutes, start) {
var time, diff, unit, duration = '', d = new Date();
d.setHours(hour);
d.setMinutes(minutes);
time = $.fullCalendar.formatDate(d, settings['time_format']);
if (start) {
diff = Math.floor((d.getTime() - start.getTime()) / 60000);
if (diff > 0) {
unit = 'm';
if (diff >= 60) {
unit = 'h';
diff = Math.round(diff / 3) / 20;
}
duration = ' (' + diff + unit + ')';
}
}
return [time, duration];
};
var autocomplete_times = function(p, callback) {
/* Time completions */
var result = [];
var now = new Date();
var st, start = (this.element.attr('id').indexOf('endtime') > 0
&& (st = $('#edit-starttime').val())
&& $('#edit-startdate').val() == $('#edit-enddate').val())
? parse_datetime(st, '') : null;
var full = p.term - 1 > 0 || p.term.length > 1;
var hours = start ? start.getHours() :
(full ? parse_datetime(p.term, '') : now).getHours();
var step = 15;
var minutes = hours * 60 + (full ? 0 : now.getMinutes());
var min = Math.ceil(minutes / step) * step % 60;
var hour = Math.floor(Math.ceil(minutes / step) * step / 60);
// list hours from 0:00 till now
for (var h = start ? start.getHours() : 0; h < hours; h++)
result.push(formattime(h, 0, start));
// list 15min steps for the next two hours
for (; h < hour + 2 && h < 24; h++) {
while (min < 60) {
result.push(formattime(h, min, start));
min += step;
}
min = 0;
}
// list the remaining hours till 23:00
while (h < 24)
result.push(formattime((h++), 0, start));
return callback(result);
};
var autocomplete_open = function(event, ui) {
// scroll to current time
var $this = $(this);
var widget = $this.autocomplete('widget');
var menu = $this.data('autocomplete').menu;
var amregex = /^(.+)(a[.m]*)/i;
var pmregex = /^(.+)(a[.m]*)/i;
var val = $(this).val().replace(amregex, '0:$1').replace(pmregex, '1:$1');
var li, html;
widget.css('width', '10em');
widget.children().each(function(){
li = $(this);
html = li.children().first().html().replace(/\s+\(.+\)$/, '').replace(amregex, '0:$1').replace(pmregex, '1:$1');
if (html == val)
menu.activate($.Event({ type:'keypress' }), li);
});
};
// if start date is changed, shift end date according to initial duration
var shift_enddate = function(dateText) {
var newstart = parse_datetime('0', dateText);
var newend = new Date(newstart.getTime() + $('#edit-startdate').data('duration') * 1000);
$('#edit-enddate').val($.fullCalendar.formatDate(newend, me.settings['date_format']));
event_times_changed();
};
var minical;
var init_calendar_ui = function()
{
// initialize small calendar widget using jQuery UI datepicker
minical = $('#datepicker').datepicker($.extend(datepicker_settings, {
inline: true,
showWeek: true,
changeMonth: false, // maybe enable?
changeYear: false, // maybe enable?
onSelect: function(dateText, inst) {
ignore_click = true;
var d = minical.datepicker('getDate'); //parse_datetime('0:0', dateText);
fc.fullCalendar('gotoDate', d).fullCalendar('select', d, d, true);
},
onChangeMonthYear: function(year, month, inst) {
minical.data('year', year).data('month', month);
},
beforeShowDay: function(date) {
var view = fc.fullCalendar('getView');
var active = view.visStart && date.getTime() >= view.visStart.getTime() && date.getTime() < view.visEnd.getTime();
return [ true, (active ? 'ui-datepicker-activerange ui-datepicker-active-' + view.name : ''), ''];
}
})) // set event handler for clicks on calendar week cell of the datepicker widget
.click(function(e) {
var cell = $(e.target);
if (e.target.tagName == 'TD' && cell.hasClass('ui-datepicker-week-col')) {
var base_date = minical.datepicker('getDate');
if (minical.data('month'))
base_date.setMonth(minical.data('month')-1);
if (minical.data('year'))
base_date.setYear(minical.data('year'));
base_date.setHours(12);
var day_off = base_date.getDay() - 1;
if (day_off < 0) day_off = 6;
var base_kw = $.datepicker.iso8601Week(base_date);
var kw = parseInt(cell.html());
var diff = (kw - base_kw) * 7 * DAY_MS;
// select monday of the chosen calendar week
var date = new Date(base_date.getTime() - day_off * DAY_MS + diff);
fc.fullCalendar('gotoDate', date).fullCalendar('setDate', date).fullCalendar('changeView', 'agendaWeek');
minical.datepicker('setDate', date);
}
});
// init event dialog
$('#eventtabs').tabs({
show: function(event, ui) {
if (ui.panel.id == 'event-tab-3') {
$('#edit-attendee-name').select();
// update free-busy status if needed
if (freebusy_ui.needsupdate && me.selected_event)
update_freebusy_status(me.selected_event);
// add current user as organizer if non added yet
if (!event_attendees.length) {
add_attendee($.extend({ role:'ORGANIZER' }, settings.identity));
$('#edit-attendees-form .attendees-invitebox').show();
}
}
}
});
$('#edit-enddate, input.edit-alarm-date').datepicker(datepicker_settings);
$('#edit-startdate').datepicker(datepicker_settings).datepicker('option', 'onSelect', shift_enddate).change(function(){ shift_enddate(this.value); });
$('#edit-enddate').datepicker('option', 'onSelect', event_times_changed).change(event_times_changed);
$('#edit-allday').click(function(){ $('#edit-starttime, #edit-endtime')[(this.checked?'hide':'show')](); event_times_changed(); });
// configure drop-down menu on time input fields based on jquery UI autocomplete
$('#edit-starttime, #edit-endtime, input.edit-alarm-time')
.attr('autocomplete', "off")
.autocomplete({
delay: 100,
minLength: 1,
source: autocomplete_times,
open: autocomplete_open,
change: event_times_changed,
select: function(event, ui) {
$(this).val(ui.item[0]);
return false;
}
})
.click(function() { // show drop-down upon clicks
$(this).autocomplete('search', $(this).val() ? $(this).val().replace(/\D.*/, "") : " ");
}).each(function(){
$(this).data('autocomplete')._renderItem = function(ul, item) {
return $('<li>')
.data('item.autocomplete', item)
.append('<a>' + item[0] + item[1] + '</a>')
.appendTo(ul);
};
});
// register events on alarm fields
$('select.edit-alarm-type').change(function(){
$(this).parent().find('span.edit-alarm-values')[(this.selectedIndex>0?'show':'hide')]();
});
$('select.edit-alarm-offset').change(function(){
var mode = $(this).val() == '@' ? 'show' : 'hide';
$(this).parent().find('.edit-alarm-date, .edit-alarm-time')[mode]();
$(this).parent().find('.edit-alarm-value').prop('disabled', mode == 'show');
});
// toggle recurrence frequency forms
$('#edit-recurrence-frequency').change(function(e){
var freq = $(this).val().toLowerCase();
$('.recurrence-form').hide();
if (freq)
$('#recurrence-form-'+freq+', #recurrence-form-until').show();
});
$('#edit-recurrence-enddate').datepicker(datepicker_settings).click(function(){ $("#edit-recurrence-repeat-until").prop('checked', true) });
$('#edit-recurrence-repeat-times').change(function(e){ $('#edit-recurrence-repeat-count').prop('checked', true); });
// init attendees autocompletion
var ac_props;
// parallel autocompletion
if (rcmail.env.autocomplete_threads > 0) {
ac_props = {
threads: rcmail.env.autocomplete_threads,
sources: rcmail.env.autocomplete_sources
};
}
rcmail.init_address_input_events($('#edit-attendee-name'), ac_props);
rcmail.addEventListener('autocomplete_insert', function(e){ $('#edit-attendee-add').click(); });
$('#edit-attendee-add').click(function(){
var input = $('#edit-attendee-name');
rcmail.ksearch_blur();
if (add_attendees(input.val())) {
input.val('');
}
});
// keep these two checkboxes in sync
$('#edit-attendees-donotify, #edit-attendees-invite').click(function(){
$('#edit-attendees-donotify, #edit-attendees-invite').prop('checked', this.checked);
});
$('#edit-attendee-schedule').click(function(){
event_freebusy_dialog();
});
$('#shedule-freebusy-prev').html(bw.ie6 ? '&lt;&lt;' : '&#9668;').button().click(function(){ render_freebusy_grid(-1); });
$('#shedule-freebusy-next').html(bw.ie6 ? '&gt;&gt;' : '&#9658;').button().click(function(){ render_freebusy_grid(1); }).parent().buttonset();
$('#shedule-find-prev').button().click(function(){ freebusy_find_slot(-1); });
$('#shedule-find-next').button().click(function(){ freebusy_find_slot(1); });
$('#schedule-freebusy-workinghours').click(function(){
freebusy_ui.workinhoursonly = this.checked;
$('#workinghourscss').remove();
if (this.checked)
$('<style type="text/css" id="workinghourscss"> td.offhours { opacity:0.3; filter:alpha(opacity=30) } </style>').appendTo('head');
});
$('#event-rsvp input.button').click(function(){
event_rsvp($(this).attr('rel'))
})
$('#agenda-listrange').change(function(e){
settings['agenda_range'] = parseInt($(this).val());
fc.fullCalendar('option', 'listRange', settings['agenda_range']).fullCalendar('render');
// TODO: save new settings in prefs
}).val(settings['agenda_range']);
$('#agenda-listsections').change(function(e){
settings['agenda_sections'] = $(this).val();
fc.fullCalendar('option', 'listSections', settings['agenda_sections']).fullCalendar('render');
// TODO: save new settings in prefs
}).val(fc.fullCalendar('option', 'listSections'));
// hide event dialog when clicking somewhere into document
$(document).bind('mousedown', dialog_check);
rcmail.set_busy(false, 'loading', ui_loading);
}
// initialize more UI elements (deferred)
window.setTimeout(init_calendar_ui, exec_deferred);
// add proprietary css styles if not IE
if (!bw.ie)
$('div.fc-content').addClass('rcube-fc-content');
// IE supresses 2nd click event when double-clicking
if (bw.ie && bw.vendver < 9) {
$('div.fc-content').bind('dblclick', function(e){
if (!$(this).hasClass('fc-widget-header') && fc.fullCalendar('getView').name != 'table') {
var date = fc.fullCalendar('getDate');
var enddate = new Date(); enddate.setTime(date.getTime() + DAY_MS - 60000);
event_edit_dialog('new', { start:date, end:enddate, allDay:true, calendar:me.selected_calendar });
}
});
}
} // end rcube_calendar class
/* calendar plugin initialization */
window.rcmail && rcmail.addEventListener('init', function(evt) {
// configure toolbar buttons
rcmail.register_command('addevent', function(){ cal.add_event(); }, true);
rcmail.register_command('print', function(){ cal.print_calendars(); }, true);
// configure list operations
rcmail.register_command('calendar-create', function(){ cal.calendar_edit_dialog(null); }, true);
rcmail.register_command('calendar-edit', function(){ cal.calendar_edit_dialog(cal.calendars[cal.selected_calendar]); }, false);
rcmail.register_command('calendar-remove', function(){ cal.calendar_remove(cal.calendars[cal.selected_calendar]); }, false);
rcmail.register_command('events-import', function(){ cal.import_events(cal.calendars[cal.selected_calendar]); }, false);
rcmail.register_command('calendar-showurl', function(){ cal.showurl(cal.calendars[cal.selected_calendar]); }, false);
// search and export events
rcmail.register_command('export', function(){ rcmail.goto_url('export_events', { source:cal.selected_calendar }); }, true);
rcmail.register_command('search', function(){ cal.quicksearch(); }, true);
rcmail.register_command('reset-search', function(){ cal.reset_quicksearch(); }, true);
// register callback commands
rcmail.addEventListener('plugin.display_alarms', function(alarms){ cal.display_alarms(alarms); });
rcmail.addEventListener('plugin.destroy_source', function(p){ cal.calendar_destroy_source(p.id); });
rcmail.addEventListener('plugin.unlock_saving', function(p){ rcmail.set_busy(false, null, cal.saving_lock); });
rcmail.addEventListener('plugin.refresh_calendar', function(p){ cal.refresh(p); });
rcmail.addEventListener('plugin.import_success', function(p){ cal.import_success(p); });
// let's go
var cal = new rcube_calendar_ui(rcmail.env.calendar_settings);
$(window).resize(function(e) {
// check target due to bugs in jquery
// http://bugs.jqueryui.com/ticket/7514
// http://bugs.jquery.com/ticket/9841
if (e.target == window) {
cal.view_resize();
}
}).resize();
// show calendars list when ready
$('#calendars').css('visibility', 'inherit');
// show toolbar
$('#toolbar').show();
});
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index a70ddfef..adca9623 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -1,1226 +1,1229 @@
<?php
/**
* Kolab driver for the Calendar plugin
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2011, 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/>.
*/
require_once(dirname(__FILE__) . '/kolab_calendar.php');
class kolab_driver extends calendar_driver
{
// features this backend supports
public $alarms = true;
public $attendees = true;
public $freebusy = true;
public $attachments = true;
public $undelete = true;
public $categoriesimmutable = true;
private $rc;
private $cal;
private $calendars;
private $has_writeable = false;
/**
* Default constructor
*/
public function __construct($cal)
{
$this->cal = $cal;
$this->rc = $cal->rc;
$this->_read_calendars();
$this->cal->register_action('push-freebusy', array($this, 'push_freebusy'));
$this->cal->register_action('calendar-acl', array($this, 'calendar_acl'));
}
/**
* Read available calendars from server
*/
private function _read_calendars()
{
// already read sources
if (isset($this->calendars))
return $this->calendars;
// get all folders that have "event" type
$folders = rcube_kolab::get_folders('event');
$this->calendars = array();
if (PEAR::isError($folders)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed to list calendar folders from Kolab server:" . $folders->getMessage()),
true, false);
}
else {
// convert to UTF8 and sort
$names = array();
foreach ($folders as $folder)
$names[$folder->name] = rcube_charset::convert($folder->name, 'UTF7-IMAP');
asort($names, SORT_LOCALE_STRING);
foreach ($names as $utf7name => $name) {
$calendar = new kolab_calendar($utf7name, $this->cal);
$this->calendars[$calendar->id] = $calendar;
if (!$calendar->readonly)
$this->has_writeable = true;
}
}
return $this->calendars;
}
/**
* Get a list of available calendars from this source
*/
public function list_calendars()
{
// attempt to create a default calendar for this user
if (!$this->has_writeable) {
if ($this->create_calendar(array('name' => 'Calendar', 'color' => 'cc0000'))) {
unset($this->calendars);
$this->_read_calendars();
}
}
$calendars = $names = array();
foreach ($this->calendars as $id => $cal) {
if ($cal->ready) {
$name = $origname = $cal->get_name();
// find folder prefix to truncate (the same code as in kolab_addressbook plugin)
for ($i = count($names)-1; $i >= 0; $i--) {
if (strpos($name, $names[$i].' &raquo; ') === 0) {
$length = strlen($names[$i].' &raquo; ');
$prefix = substr($name, 0, $length);
$count = count(explode(' &raquo; ', $prefix));
$name = str_repeat('&nbsp;&nbsp;', $count-1) . '&raquo; ' . substr($name, $length);
break;
}
}
$names[] = $origname;
$calendars[$cal->id] = array(
'id' => $cal->id,
'name' => $name,
'editname' => $cal->get_foldername(),
'color' => $cal->get_color(),
'readonly' => $cal->readonly,
'showalarms' => $cal->alarms,
'class_name' => $cal->get_namespace(),
'active' => rcube_kolab::is_subscribed($cal->get_realname()),
);
}
}
return $calendars;
}
/**
* Create a new calendar assigned to the current user
*
* @param array Hash array with calendar properties
* name: Calendar name
* color: The color of the calendar
* @return mixed ID of the calendar on success, False on error
*/
public function create_calendar($prop)
{
$folder = $this->folder_update($prop);
if ($folder === false) {
return false;
}
// subscribe to new calendar by default
$storage = $this->rc->get_storage();
$storage->subscribe($folder);
// create ID
$id = rcube_kolab::folder_id($folder);
// save color in user prefs (temp. solution)
$prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
if (isset($prop['color']))
$prefs['kolab_calendars'][$id]['color'] = $prop['color'];
if (isset($prop['showalarms']))
$prefs['kolab_calendars'][$id]['showalarms'] = $prop['showalarms'] ? true : false;
if ($prefs['kolab_calendars'][$id])
$this->rc->user->save_prefs($prefs);
return $id;
}
/**
* Update properties of an existing calendar
*
* @see calendar_driver::edit_calendar()
*/
public function edit_calendar($prop)
{
if ($prop['id'] && ($cal = $this->calendars[$prop['id']])) {
$oldfolder = $cal->get_realname();
$newfolder = $this->folder_update($prop);
if ($newfolder === false) {
return false;
}
// create ID
$id = rcube_kolab::folder_id($newfolder);
// fallback to local prefs
$prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
unset($prefs['kolab_calendars'][$prop['id']]);
if (isset($prop['color']))
$prefs['kolab_calendars'][$id]['color'] = $prop['color'];
if (isset($prop['showalarms']))
$prefs['kolab_calendars'][$id]['showalarms'] = $prop['showalarms'] ? true : false;
if ($prefs['kolab_calendars'][$id])
$this->rc->user->save_prefs($prefs);
return true;
}
return false;
}
/**
* Set active/subscribed state of a calendar
*
* @see calendar_driver::subscribe_calendar()
*/
public function subscribe_calendar($prop)
{
if ($prop['id'] && ($cal = $this->calendars[$prop['id']])) {
$storage = $this->rc->get_storage();
if ($prop['active'])
return $storage->subscribe($cal->get_realname());
else
return $storage->unsubscribe($cal->get_realname());
}
return false;
}
/**
* Rename or Create a new IMAP folder
*
* @param array Hash array with calendar properties
*
* @return mixed New folder name or False on failure
*/
private function folder_update(&$prop)
{
$folder = rcube_charset::convert($prop['name'], RCMAIL_CHARSET, 'UTF7-IMAP');
$oldfolder = $prop['oldname']; // UTF7
$parent = $prop['parent']; // UTF7
$storage = $this->rc->get_storage();
$delimiter = $storage->get_hierarchy_delimiter();
if (strlen($oldfolder)) {
$options = $storage->folder_info($oldfolder);
}
if (!empty($options) && ($options['norename'] || $options['protected'])) {
}
// sanity checks (from steps/settings/save_folder.inc)
else if (!strlen($folder)) {
$this->last_error = 'Invalid folder name';
return false;
}
else if (strlen($folder) > 128) {
$this->last_error = 'Folder name too long';
return false;
}
else {
// these characters are problematic e.g. when used in LIST/LSUB
foreach (array($delimiter, '%', '*') as $char) {
if (strpos($folder, $delimiter) !== false) {
$this->last_error = 'Invalid folder name';
return false;
}
}
}
if (!empty($options) && ($options['protected'] || $options['norename'])) {
$folder = $oldfolder;
}
else if (strlen($parent)) {
$folder = $parent . $delimiter . $folder;
}
else {
// add namespace prefix (when needed)
$folder = $storage->mod_folder($folder, 'in');
}
// Check access rights to the parent folder
if (strlen($parent) && (!strlen($oldfolder) || $oldfolder != $folder)) {
$parent_opts = $storage->folder_info($parent);
if ($parent_opts['namespace'] != 'personal'
&& (empty($parent_opts['rights']) || !preg_match('/[ck]/', implode($parent_opts)))
) {
$this->last_error = 'No permission to create folder';
return false;
}
}
// update the folder name
if (strlen($oldfolder)) {
if ($oldfolder != $folder) {
if (!($result = rcube_kolab::folder_rename($oldfolder, $folder)))
$this->last_error = rcube_kolab::$last_error;
}
else
$result = true;
}
// create new folder
else {
if (!($result = rcube_kolab::folder_create($folder, 'event', false)))
$this->last_error = rcube_kolab::$last_error;
}
// save color in METADATA
// TODO: also save 'showalarams' and other properties here
if ($result && $prop['color']) {
if (!($meta_saved = $storage->set_metadata($folder, array('/shared/vendor/kolab/color' => $prop['color'])))) // try in shared namespace
$meta_saved = $storage->set_metadata($folder, array('/private/vendor/kolab/color' => $prop['color'])); // try in private namespace
if ($meta_saved)
unset($prop['color']); // unsetting will prevent fallback to local user prefs
}
return $result ? $folder : false;
}
/**
* Delete the given calendar with all its contents
*
* @see calendar_driver::remove_calendar()
*/
public function remove_calendar($prop)
{
if ($prop['id'] && ($cal = $this->calendars[$prop['id']])) {
$folder = $cal->get_realname();
if (rcube_kolab::folder_delete($folder)) {
// remove color in user prefs (temp. solution)
$prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
unset($prefs['kolab_calendars'][$prop['id']]);
$this->rc->user->save_prefs($prefs);
return true;
}
else
$this->last_error = rcube_kolab::$last_error;
}
return false;
}
/**
* Fetch a single event
*
* @see calendar_driver::get_event()
* @return array Hash array with event properties, false if not found
*/
public function get_event($event, $writeable = null)
{
if (is_array($event)) {
$id = $event['id'] ? $event['id'] : $event['uid'];
$cal = $event['calendar'];
}
else {
$id = $event;
}
if ($cal && ($storage = $this->calendars[$cal])) {
return $storage->get_event($id);
}
// iterate over all calendar folders and search for the event ID
else if (!$cal) {
foreach ($this->calendars as $storage) {
if ($writeable && $storage->readonly)
continue;
if ($result = $storage->get_event($id)) {
return $result;
}
}
}
return false;
}
/**
* Add a single event to the database
*
* @see calendar_driver::new_event()
*/
public function new_event($event)
{
$cid = $event['calendar'] ? $event['calendar'] : reset(array_keys($this->calendars));
if ($storage = $this->calendars[$cid]) {
// handle attachments to add
if (!empty($event['attachments'])) {
foreach ($event['attachments'] as $idx => $attachment) {
// we'll read file contacts into memory, Horde/Kolab classes does the same
// So we cannot save memory, rcube_imap class can do this better
$event['attachments'][$idx]['content'] = $attachment['data'] ? $attachment['data'] : file_get_contents($attachment['path']);
}
}
$GLOBALS['conf']['kolab']['no_triggering'] = true;
$success = $storage->insert_event($event);
if ($success)
$this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id));
return $success;
}
return false;
}
/**
* Update an event entry with the given data
*
* @see calendar_driver::new_event()
* @return boolean True on success, False on error
*/
public function edit_event($event)
{
return $this->update_event($event);
}
/**
* Move a single event
*
* @see calendar_driver::move_event()
* @return boolean True on success, False on error
*/
public function move_event($event)
{
if (($storage = $this->calendars[$event['calendar']]) && ($ev = $storage->get_event($event['id'])))
return $this->update_event($event + $ev);
return false;
}
/**
* Resize a single event
*
* @see calendar_driver::resize_event()
* @return boolean True on success, False on error
*/
public function resize_event($event)
{
if (($storage = $this->calendars[$event['calendar']]) && ($ev = $storage->get_event($event['id'])))
return $this->update_event($event + $ev);
return false;
}
/**
* Remove a single event
*
* @param array Hash array with event properties:
* id: Event identifier
* @param boolean Remove record(s) irreversible (mark as deleted otherwise)
*
* @return boolean True on success, False on error
*/
public function remove_event($event, $force = true)
{
$success = false;
$_savemode = $event['_savemode'];
if (($storage = $this->calendars[$event['calendar']]) && ($event = $storage->get_event($event['id']))) {
$event['_savemode'] = $_savemode;
$savemode = 'all';
$master = $event;
$this->rc->session->remove('calendar_restore_event_data');
$GLOBALS['conf']['kolab']['no_triggering'] = true;
// read master if deleting a recurring event
if ($event['recurrence'] || $event['recurrence_id']) {
$master = $event['recurrence_id'] ? $storage->get_event($event['recurrence_id']) : $event;
$savemode = $event['_savemode'];
}
switch ($savemode) {
case 'current':
$_SESSION['calendar_restore_event_data'] = $master;
// removing the first instance => just move to next occurence
if ($master['id'] == $event['id']) {
$recurring = reset($storage->_get_recurring_events($event, $event['start'], $event['end'] + 86400 * 370, $event['id'].'-1'));
$master['start'] = $recurring['start'];
$master['end'] = $recurring['end'];
if ($master['recurrence']['COUNT'])
$master['recurrence']['COUNT']--;
}
else { // add exception to master event
$master['recurrence']['EXDATE'][] = $event['start'];
}
$success = $storage->update_event($master);
break;
case 'future':
if ($master['id'] != $event['id']) {
$_SESSION['calendar_restore_event_data'] = $master;
// set until-date on master event
$master['recurrence']['UNTIL'] = $event['start'] - 86400;
unset($master['recurrence']['COUNT']);
$success = $storage->update_event($master);
break;
}
default: // 'all' is default
$success = $storage->delete_event($master, $force);
break;
}
}
if ($success)
$this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id));
return $success;
}
/**
* Restore a single deleted event
*
* @param array Hash array with event properties:
* id: Event identifier
* @return boolean True on success, False on error
*/
public function restore_event($event)
{
if ($storage = $this->calendars[$event['calendar']]) {
if (!empty($_SESSION['calendar_restore_event_data']))
$success = $storage->update_event($_SESSION['calendar_restore_event_data']);
else
$success = $storage->restore_event($event);
if ($success)
$this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id));
return $success;
}
return false;
}
/**
* Wrapper to update an event object depending on the given savemode
*/
private function update_event($event)
{
if (!($storage = $this->calendars[$event['calendar']]))
return false;
// move event to another folder/calendar
if ($event['_fromcalendar'] && $event['_fromcalendar'] != $event['calendar']) {
if (!($fromcalendar = $this->calendars[$event['_fromcalendar']]))
return false;
if ($event['_savemode'] != 'new') {
if (!$fromcalendar->storage->move($event['id'], $storage->get_realname()))
return false;
$fromcalendar = $storage;
$storage->storage->synchronize();
}
}
else
$fromcalendar = $storage;
$success = false;
$savemode = 'all';
$attachments = array();
$old = $master = $fromcalendar->get_event($event['id']);
// delete existing attachment(s)
if (!empty($event['deleted_attachments'])) {
foreach ($event['deleted_attachments'] as $attachment) {
if (!empty($old['attachments'])) {
foreach ($old['attachments'] as $idx => $att) {
if ($att['id'] == $attachment) {
unset($old['attachments'][$idx]);
}
}
}
}
}
// handle attachments to add
if (!empty($event['attachments'])) {
foreach ($event['attachments'] as $attachment) {
// skip entries without content (could be existing ones)
if (!$attachment['data'] && !$attachment['path'])
continue;
// we'll read file contacts into memory, Horde/Kolab classes does the same
// So we cannot save memory, rcube_imap class can do this better
$attachments[] = array(
'name' => $attachment['name'],
'type' => $attachment['mimetype'],
'content' => $attachment['data'] ? $attachment['data'] : file_get_contents($attachment['path']),
);
}
}
$event['attachments'] = array_merge((array)$old['attachments'], $attachments);
// modify a recurring event, check submitted savemode to do the right things
if ($old['recurrence'] || $old['recurrence_id']) {
$master = $old['recurrence_id'] ? $fromcalendar->get_event($old['recurrence_id']) : $old;
$savemode = $event['_savemode'];
}
// keep saved exceptions (not submitted by the client)
if ($old['recurrence']['EXDATE'])
$event['recurrence']['EXDATE'] = $old['recurrence']['EXDATE'];
$GLOBALS['conf']['kolab']['no_triggering'] = true;
switch ($savemode) {
case 'new':
// save submitted data as new (non-recurring) event
$event['recurrence'] = array();
$event['uid'] = $this->cal->generate_uid();
// copy attachment data to new event
foreach ((array)$event['attachments'] as $idx => $attachment) {
if (!$attachment['data'])
$attachment['data'] = $fromcalendar->get_attachment_body($attachment['id']);
}
$success = $storage->insert_event($event);
break;
case 'current':
// add exception to master event
$master['recurrence']['EXDATE'][] = $old['start'];
$storage->update_event($master);
// insert new event for this occurence
$event += $old;
$event['recurrence'] = array();
$event['uid'] = $this->cal->generate_uid();
$success = $storage->insert_event($event);
break;
case 'future':
if ($master['id'] != $event['id']) {
// set until-date on master event
$master['recurrence']['UNTIL'] = $old['start'] - 86400;
unset($master['recurrence']['COUNT']);
$storage->update_event($master);
// save this instance as new recurring event
$event += $old;
$event['uid'] = $this->cal->generate_uid();
// if recurrence COUNT, update value to the correct number of future occurences
if ($event['recurrence']['COUNT']) {
$event['recurrence']['COUNT'] -= $old['_instance'];
}
// remove fixed weekday, will be re-set to the new weekday in kolab_calendar::insert_event()
if (strlen($event['recurrence']['BYDAY']) == 2)
unset($event['recurrence']['BYDAY']);
if ($master['recurrence']['BYMONTH'] == gmdate('n', $master['start']))
unset($event['recurrence']['BYMONTH']);
$success = $storage->insert_event($event);
break;
}
default: // 'all' is default
$event['id'] = $master['id'];
$event['uid'] = $master['uid'];
// use start date from master but try to be smart on time or duration changes
$old_start_date = date('Y-m-d', $old['start']);
$old_start_time = date('H:i', $old['start']);
$old_duration = $old['end'] - $old['start'];
$new_start_date = date('Y-m-d', $event['start']);
$new_start_time = date('H:i', $event['start']);
$new_duration = $event['end'] - $event['start'];
$diff = $old_start_date != $new_start_date || $old_start_time != $new_start_time || $old_duration != $new_duration;
// shifted or resized
if ($diff && ($old_start_date == $new_start_date || $old_duration == $new_duration)) {
$event['start'] = $master['start'] + ($event['start'] - $old['start']);
$event['end'] = $event['start'] + $new_duration;
// remove fixed weekday, will be re-set to the new weekday in kolab_calendar::update_event()
if ($old_start_date != $new_start_date) {
if (strlen($event['recurrence']['BYDAY']) == 2)
unset($event['recurrence']['BYDAY']);
if ($old['recurrence']['BYMONTH'] == gmdate('n', $old['start']))
unset($event['recurrence']['BYMONTH']);
}
}
$success = $storage->update_event($event);
break;
}
if ($success)
$this->rc->output->command('plugin.ping_url', array('action' => 'calendar/push-freebusy', 'source' => $storage->id));
return $success;
}
/**
* Get events from source.
*
* @param integer Event's new start (unix timestamp)
* @param integer Event's new end (unix timestamp)
* @param string Search query (optional)
* @param mixed List of calendar IDs to load events from (either as array or comma-separated string)
* @param boolean Strip virtual events (optional)
* @return array A list of event records
*/
public function load_events($start, $end, $search = null, $calendars = null, $virtual = 1)
{
if ($calendars && is_string($calendars))
$calendars = explode(',', $calendars);
$events = $categories = array();
foreach ($this->calendars as $cid => $calendar) {
if ($calendars && !in_array($cid, $calendars))
continue;
$events = array_merge($events, $this->calendars[$cid]->list_events($start, $end, $search, $virtual));
$categories += $this->calendars[$cid]->categories;
}
// add new categories to user prefs
$old_categories = $this->rc->config->get('calendar_categories', array());
if ($newcats = array_diff(array_map('strtolower', array_keys($categories)), array_map('strtolower', array_keys($old_categories)))) {
foreach ($newcats as $category)
$old_categories[$category] = ''; // no color set yet
$this->rc->user->save_prefs(array('calendar_categories' => $old_categories));
}
return $events;
}
/**
* Get a list of pending alarms to be displayed to the user
*
* @see calendar_driver::pending_alarms()
*/
public function pending_alarms($time, $calendars = null)
{
$interval = 300;
$time -= $time % 60;
$slot = $time;
$slot -= $slot % $interval;
$last = $time - max(60, $this->rc->session->get_keep_alive());
$last -= $last % $interval;
// only check for alerts once in 5 minutes
if ($last == $slot)
return false;
if ($calendars && is_string($calendars))
$calendars = explode(',', $calendars);
$time = $slot + $interval;
$events = array();
foreach ($this->calendars as $cid => $calendar) {
// skip calendars with alarms disabled
if (!$calendar->alarms || ($calendars && !in_array($cid, $calendars)))
continue;
foreach ($calendar->list_events($time, $time + 86400 * 365) as $e) {
// add to list if alarm is set
if ($e['_alarm'] && ($notifyat = $e['start'] - $e['_alarm'] * 60) <= $time) {
$id = $e['id'];
$events[$id] = $e;
$events[$id]['notifyat'] = $notifyat;
}
}
}
// get alarm information stored in local database
if (!empty($events)) {
$event_ids = array_map(array($this->rc->db, 'quote'), array_keys($events));
$result = $this->rc->db->query(sprintf(
"SELECT * FROM kolab_alarms
WHERE event_id IN (%s)",
join(',', $event_ids),
$this->rc->db->now()
));
while ($result && ($e = $this->rc->db->fetch_assoc($result))) {
$dbdata[$e['event_id']] = $e;
}
}
$alarms = array();
foreach ($events as $id => $e) {
// skip dismissed
if ($dbdata[$id]['dismissed'])
continue;
// snooze function may have shifted alarm time
$notifyat = $dbdata[$id]['notifyat'] ? strtotime($dbdata[$id]['notifyat']) : $e['notifyat'];
if ($notifyat <= $time)
$alarms[] = $e;
}
return $alarms;
}
/**
* Feedback after showing/sending an alarm notification
*
* @see calendar_driver::dismiss_alarm()
*/
public function dismiss_alarm($event_id, $snooze = 0)
{
// set new notifyat time or unset if not snoozed
$notifyat = $snooze > 0 ? date('Y-m-d H:i:s', time() + $snooze) : null;
$query = $this->rc->db->query(
"REPLACE INTO kolab_alarms
(event_id, dismissed, notifyat)
VALUES(?, ?, ?)",
$event_id,
$snooze > 0 ? 0 : 1,
$notifyat
);
return $this->rc->db->affected_rows($query);
}
/**
* List attachments from the given event
*/
public function list_attachments($event)
{
if (!($storage = $this->calendars[$event['calendar']]))
return false;
$event = $storage->get_event($event['id']);
return $event['attachments'];
}
/**
* Get attachment properties
*/
public function get_attachment($id, $event)
{
if (!($storage = $this->calendars[$event['calendar']]))
return false;
$event = $storage->get_event($event['id']);
if ($event && !empty($event['attachments'])) {
foreach ($event['attachments'] as $att) {
if ($att['id'] == $id) {
return $att;
}
}
}
return null;
}
/**
* Get attachment body
*/
public function get_attachment_body($id, $event)
{
if (!($storage = $this->calendars[$event['calendar']]))
return false;
return $storage->get_attachment_body($id);
}
/**
* List availabale categories
* The default implementation reads them from config/user prefs
*/
public function list_categories()
{
// FIXME: complete list with categories saved in config objects (KEP:12)
return $this->rc->config->get('calendar_categories', array());
}
/**
* Fetch free/busy information from a person within the given range
*/
public function get_freebusy_list($email, $start, $end)
{
require_once('Horde/iCalendar.php');
if (empty($email)/* || $end < time()*/)
return false;
// map vcalendar fbtypes to internal values
$fbtypemap = array(
'FREE' => calendar::FREEBUSY_FREE,
'BUSY-TENTATIVE' => calendar::FREEBUSY_TENTATIVE,
'X-OUT-OF-OFFICE' => calendar::FREEBUSY_OOF,
'OOF' => calendar::FREEBUSY_OOF);
// ask kolab server first
$fbdata = @file_get_contents(rcube_kolab::get_freebusy_url($email));
// get free-busy url from contacts
if (!$fbdata) {
$fburl = null;
foreach ((array)$this->rc->config->get('autocomplete_addressbooks', 'sql') as $book) {
$abook = $this->rc->get_address_book($book);
if ($result = $abook->search(array('email'), $email, true, true, true/*, 'freebusyurl'*/)) {
while ($contact = $result->iterate()) {
if ($fburl = $contact['freebusyurl']) {
$fbdata = @file_get_contents($fburl);
break;
}
}
}
if ($fbdata)
break;
}
}
// parse free-busy information using Horde classes
if ($fbdata) {
$fbcal = new Horde_iCalendar;
$fbcal->parsevCalendar($fbdata);
if ($fb = $fbcal->findComponent('vfreebusy')) {
$result = array();
$params = $fb->getExtraParams();
foreach ($fb->getBusyPeriods() as $from => $to) {
if ($to == null) // no information, assume free
break;
$type = $params[$from]['FBTYPE'];
$result[] = array($from, $to, isset($fbtypemap[$type]) ? $fbtypemap[$type] : calendar::FREEBUSY_BUSY);
}
// set period from $start till the begin of the free-busy information as 'unknown'
if (($fbstart = $fb->getStart()) && $start < $fbstart) {
array_unshift($result, array($start, $fbstart, calendar::FREEBUSY_UNKNOWN));
}
// pad period till $end with status 'unknown'
if (($fbend = $fb->getEnd()) && $fbend < $end) {
$result[] = array($fbend, $end, calendar::FREEBUSY_UNKNOWN);
}
return $result;
}
}
return false;
}
/**
* Handler to push folder triggers when sent from client.
* Used to push free-busy changes asynchronously after updating an event
*/
public function push_freebusy()
{
// make shure triggering completes
set_time_limit(0);
ignore_user_abort(true);
$cal = get_input_value('source', RCUBE_INPUT_GPC);
if (!($storage = $this->calendars[$cal]))
return false;
// trigger updates on folder
$folder = $storage->get_folder();
$trigger = $folder->trigger();
if (is_object($trigger) && is_a($trigger, 'PEAR_Error')) {
raise_error(array(
'code' => 900, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed triggering folder. Error was " . $trigger->getMessage()),
true, false);
}
exit;
}
/**
* Callback function to produce driver-specific calendar create/edit form
*
* @param string Request action 'form-edit|form-new'
* @param array Calendar properties (e.g. id, color)
* @param array Edit form fields
*
* @return string HTML content of the form
*/
public function calendar_form($action, $calendar, $formfields)
{
if ($calendar['id'] && ($cal = $this->calendars[$calendar['id']])) {
$folder = $cal->get_realname(); // UTF7
$color = $cal->get_color();
}
else {
$folder = '';
$color = '';
}
$hidden_fields[] = array('name' => 'oldname', 'value' => $folder);
$storage = $this->rc->get_storage();
$delim = $storage->get_hierarchy_delimiter();
$form = array();
if (strlen($folder)) {
$path_imap = explode($delim, $folder);
array_pop($path_imap); // pop off name part
$path_imap = implode($path_imap, $delim);
$options = $storage->folder_info($folder);
}
else {
$path_imap = '';
}
// General tab
$form['props'] = array(
'name' => $this->rc->gettext('properties'),
);
// Disable folder name input
if (!empty($options) && ($options['norename'] || $options['protected'])) {
$input_name = new html_hiddenfield(array('name' => 'name', 'id' => 'calendar-name'));
$formfields['name']['value'] = Q(str_replace($delimiter, ' &raquo; ', rcube_kolab::object_name($folder)))
. $input_name->show($folder);
}
// calendar name (default field)
$form['props']['fieldsets']['location'] = array(
'name' => $this->rc->gettext('location'),
'content' => array(
'name' => $formfields['name']
),
);
if (!empty($options) && ($options['norename'] || $options['protected'])) {
// prevent user from moving folder
$hidden_fields[] = array('name' => 'parent', 'value' => $path_imap);
}
else {
$select = rcube_kolab::folder_selector('event', array('name' => 'parent'), $folder);
$form['props']['fieldsets']['location']['content']['path'] = array(
'label' => $this->cal->gettext('parentcalendar'),
'value' => $select->show(strlen($folder) ? $path_imap : ''),
);
}
// calendar color (default field)
$form['props']['fieldsets']['settings'] = array(
'name' => $this->rc->gettext('settings'),
'content' => array(
'color' => $formfields['color'],
'showalarms' => $formfields['showalarms'],
),
);
if ($action != 'form-new') {
$form['sharing'] = array(
'name' => Q($this->cal->gettext('tabsharing')),
'content' => html::tag('iframe', array(
'src' => $this->cal->rc->url(array('_action' => 'calendar-acl', 'id' => $calendar['id'], 'framed' => 1)),
'width' => '100%',
'height' => 350,
'border' => 0,
'style' => 'border:0'),
''),
);
}
$this->form_html = '';
if (is_array($hidden_fields)) {
foreach ($hidden_fields as $field) {
$hiddenfield = new html_hiddenfield($field);
$this->form_html .= $hiddenfield->show() . "\n";
}
}
// Create form output
foreach ($form as $tab) {
if (!empty($tab['fieldsets']) && is_array($tab['fieldsets'])) {
$content = '';
foreach ($tab['fieldsets'] as $fieldset) {
$subcontent = $this->get_form_part($fieldset);
if ($subcontent) {
$content .= html::tag('fieldset', null, html::tag('legend', null, Q($fieldset['name'])) . $subcontent) ."\n";
}
}
}
else {
$content = $this->get_form_part($tab);
}
if ($content) {
$this->form_html .= html::tag('fieldset', null, html::tag('legend', null, Q($tab['name'])) . $content) ."\n";
}
}
// Parse form template for skin-dependent stuff
$this->rc->output->add_handler('calendarform', array($this, 'calendar_form_html'));
return $this->rc->output->parse('calendar.kolabform', false, false);
}
/**
* Handler for template object
*/
public function calendar_form_html()
{
return $this->form_html;
}
/**
* Helper function used in calendar_form_content(). Creates a part of the form.
*/
private function get_form_part($form)
{
$content = '';
if (is_array($form['content']) && !empty($form['content'])) {
$table = new html_table(array('cols' => 2));
foreach ($form['content'] as $col => $colprop) {
$colprop['id'] = '_'.$col;
$label = !empty($colprop['label']) ? $colprop['label'] : rcube_label($col);
$table->add('title', sprintf('<label for="%s">%s</label>', $colprop['id'], Q($label)));
$table->add(null, $colprop['value']);
}
$content = $table->show();
}
else {
$content = $form['content'];
}
return $content;
}
/**
* Handler to render ACL form for a calendar folder
*/
public function calendar_acl()
{
$this->rc->output->add_handler('folderacl', array($this, 'calendar_acl_form'));
$this->rc->output->send('calendar.kolabacl');
}
/**
* Handler for ACL form template object
*/
public function calendar_acl_form()
{
$calid = get_input_value('_id', RCUBE_INPUT_GPC);
if ($calid && ($cal = $this->calendars[$calid])) {
$folder = $cal->get_realname(); // UTF7
$color = $cal->get_color();
}
else {
$folder = '';
$color = '';
}
$hidden_fields[] = array('name' => 'oldname', 'value' => $folder);
$storage = $this->rc->get_storage();
$delim = $storage->get_hierarchy_delimiter();
$form = array();
if (strlen($folder)) {
$path_imap = explode($delim, $folder);
array_pop($path_imap); // pop off name part
$path_imap = implode($path_imap, $delim);
$options = $storage->folder_info($folder);
// Allow plugins to modify the form content (e.g. with ACL form)
$plugin = $this->rc->plugins->exec_hook('calendar_form_kolab',
array('form' => $form, 'options' => $options, 'name' => $folder));
}
+ if (!$plugin['form']['sharing']['content'])
+ $plugin['form']['sharing']['content'] = html::div('hint', $this->cal->gettext('aclnorights'));
+
return $plugin['form']['sharing']['content'];
}
/**
* Return a (limited) list of color values to be used for calendar and category coloring
*
* @return mixed List for colors as hex values or false if no presets should be shown
*/
public function get_color_values()
{
// selection from http://msdn.microsoft.com/en-us/library/aa358802%28v=VS.85%29.aspx
return array('000000','006400','2F4F4F','800000','808000','008000',
'008080','000080','800080','4B0082','191970','8B0000','008B8B',
'00008B','8B008B','556B2F','8B4513','228B22','6B8E23','2E8B57',
'B8860B','483D8B','A0522D','0000CD','A52A2A','00CED1','696969',
'20B2AA','9400D3','B22222','C71585','3CB371','D2691E','DC143C',
'DAA520','00FA9A','4682B4','7CFC00','9932CC','FF0000','FF4500',
'FF8C00','FFA500','FFD700','FFFF00','9ACD32','32CD32','00FF00',
'00FF7F','00FFFF','5F9EA0','00BFFF','0000FF','FF00FF','808080',
'708090','CD853F','8A2BE2','778899','FF1493','48D1CC','1E90FF',
'40E0D0','4169E1','6A5ACD','BDB76B','BA55D3','CD5C5C','ADFF2F',
'66CDAA','FF6347','8FBC8B','DA70D6','BC8F8F','9370DB','DB7093',
'FF7F50','6495ED','A9A9A9','F4A460','7B68EE','D2B48C','E9967A',
'DEB887','FF69B4','FA8072','F08080','EE82EE','87CEEB','FFA07A',
'F0E68C','DDA0DD','90EE90','7FFFD4','C0C0C0','87CEFA','B0C4DE',
'98FB98','ADD8E6','B0E0E6','D8BFD8','EEE8AA','AFEEEE','D3D3D3',
'FFDEAD');
}
}
diff --git a/plugins/calendar/lib/calendar_itip.php b/plugins/calendar/lib/calendar_itip.php
index 8b1a74b6..6507b513 100644
--- a/plugins/calendar/lib/calendar_itip.php
+++ b/plugins/calendar/lib/calendar_itip.php
@@ -1,324 +1,324 @@
<?php
/**
* iTIP functions for the Calendar plugin
*
* Class providing functionality to manage iTIP invitations
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @package @package_name@
*
* Copyright (C) 2011, 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_itip
{
private $rc;
private $cal;
private $event;
function __construct($cal)
{
$this->cal = $cal;
$this->rc = $cal->rc;
$this->sender = $this->rc->user->get_identity();
}
/**
* Send an iTip mail message
*
* @param array Event object to send
* @param string iTip method (REQUEST|REPLY|CANCEL)
* @param array Hash array with recipient data (name, email)
* @param string Mail subject
* @param string Mail body text label
* @param object Mail_mime object with message data
* @return boolean True on success, false on failure
*/
public function send_itip_message($event, $method, $recipient, $subject, $bodytext, $message = null)
{
if (!$this->sender['name'])
$this->sender['name'] = $this->sender['email'];
if (!$message)
$message = $this->compose_itip_message($event, $method);
$mailto = rcube_idn_to_ascii($recipient['email']);
$headers = $message->headers();
$headers['To'] = format_email_recipient($mailto, $recipient['name']);
$headers['Subject'] = $this->cal->gettext(array(
'name' => $subject,
'vars' => array('title' => $event['title'], 'name' => $this->sender['name'])
));
// compose a list of all event attendees
$attendees_list = array();
foreach ((array)$event['attendees'] as $attendee) {
$attendees_list[] = ($attendee['name'] && $attendee['email']) ?
$attendee['name'] . ' <' . $attendee['email'] . '>' :
($attendee['name'] ? $attendee['name'] : $attendee['email']);
}
$mailbody = $this->cal->gettext(array(
'name' => $bodytext,
'vars' => array(
'title' => $event['title'],
'date' => $this->cal->event_date_text($event, true),
'attendees' => join(', ', $attendees_list),
'sender' => $this->sender['name'],
'organizer' => $this->sender['name'],
)
));
// append links for direct invitation replies
if ($method == 'REQUEST' && ($token = $this->store_invitation($event, $recipient['email']))) {
$mailbody .= "\n\n" . $this->cal->gettext(array(
'name' => 'invitationattendlinks',
'vars' => array('url' => $this->cal->get_url(array('action' => 'attend', 't' => $token))),
));
}
else if ($method == 'CANCEL') {
$this->cancel_itip_invitation($event);
}
$message->headers($headers, true);
- $message->setTXTBody(rcube_message::format_flowed($mailbody, 79));
-
+ $message->setTXTBody(rcube_mime::format_flowed($mailbody, 79));
+
// finally send the message
return rcmail_deliver_message($message, $headers['X-Sender'], $mailto, $smtp_error);
}
/**
* Helper function to build a Mail_mime object to send an iTip message
*
* @param array Event object to send
* @param string iTip method (REQUEST|REPLY|CANCEL)
* @return object Mail_mime object with message data
*/
public function compose_itip_message($event, $method)
{
$from = rcube_idn_to_ascii($this->sender['email']);
$sender = format_email_recipient($from, $this->sender['name']);
// compose multipart message using PEAR:Mail_Mime
$message = new Mail_mime("\r\n");
$message->setParam('text_encoding', 'quoted-printable');
$message->setParam('head_encoding', 'quoted-printable');
$message->setParam('head_charset', RCMAIL_CHARSET);
$message->setParam('text_charset', RCMAIL_CHARSET . ";\r\n format=flowed");
// compose common headers array
$headers = array(
'From' => $sender,
'Date' => rcmail_user_date(),
'Message-ID' => rcmail_gen_message_id(),
'X-Sender' => $from,
);
if ($agent = $this->rc->config->get('useragent'))
$headers['User-Agent'] = $agent;
$message->headers($headers);
// attach ics file for this event
$ical = $this->cal->get_ical();
$ics = $ical->export(array($event), $method);
$message->addAttachment($ics, 'text/calendar', 'event.ics', false, '8bit', 'attachment', RCMAIL_CHARSET . "; method=" . $method);
return $message;
}
/**
* Find invitation record by token
*
* @param string Invitation token
* @return mixed Invitation record as hash array or False if not found
*/
public function get_invitation($token)
{
if ($parts = $this->decode_token($token)) {
$result = $this->rc->db->query("SELECT * FROM itipinvitations WHERE token=?", $parts['base']);
if ($result && ($rec = $this->rc->db->fetch_assoc($result))) {
$rec['event'] = unserialize($rec['event']);
$rec['attendee'] = $parts['attendee'];
return $rec;
}
}
return false;
}
/**
* Update the attendee status of the given invitation record
*
* @param array Invitation record as fetched with calendar_itip::get_invitation()
* @param string Attendee email address
* @param string New attendee status
*/
public function update_invitation($invitation, $email, $newstatus)
{
if (is_string($invitation))
$invitation = $this->get_invitation($invitation);
if ($invitation['token'] && $invitation['event']) {
// update attendee record in event data
foreach ($invitation['event']['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
$organizer = $attendee;
}
else if ($attendee['email'] == $email) {
// nothing to be done here
if ($attendee['status'] == $newstatus)
return true;
$invitation['event']['attendees'][$i]['status'] = $newstatus;
$this->sender = $attendee;
}
}
$invitation['event']['changed'] = time();
// send iTIP REPLY message to organizer
if ($organizer) {
$status = strtolower($newstatus);
if ($this->send_itip_message($invitation['event'], 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
$this->rc->output->command('display_message', $this->cal->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->cal->gettext('itipresponseerror'), 'error');
}
// update record in DB
$query = $this->rc->db->query(
"UPDATE itipinvitations
SET event=?
WHERE token=?",
self::serialize_event($invitation['event']),
$invitation['token']
);
if ($this->rc->db->affected_rows($query))
return true;
}
return false;
}
/**
* Create iTIP invitation token for later replies via URL
*
* @param array Hash array with event properties
* @param string Attendee email address
* @return string Invitation token
*/
public function store_invitation($event, $attendee)
{
static $stored = array();
if (!$event['uid'] || !$attendee)
return false;
// generate token for this invitation
$token = $this->generate_token($event, $attendee);
$base = substr($token, 0, 40);
// already stored this
if ($stored[$base])
return $token;
// @TODO: REPLACE works only with MySQL
$query = $this->rc->db->query(
"REPLACE INTO itipinvitations
(token, event_uid, user_id, event, expires)
VALUES(?, ?, ?, ?, ?)",
$base,
$event['uid'],
$this->rc->user->ID,
self::serialize_event($event),
date('Y-m-d H:i:s', $event['end'] + 86400 * 2)
);
if ($this->rc->db->affected_rows($query)) {
$stored[$base] = 1;
return $token;
}
return false;
}
/**
* Mark invitations for the given event as cancelled
*
* @param array Hash array with event properties
*/
public function cancel_itip_invitation($event)
{
// flag invitation record as cancelled
$this->rc->db->query(
"UPDATE itipinvitations
SET cancelled=1
WHERE event_uid=? AND user_id=?",
$event['uid'],
$this->rc->user->ID
);
}
/**
* Generate an invitation request token for the given event and attendee
*
* @param array Event hash array
* @param string Attendee email address
*/
public function generate_token($event, $attendee)
{
$base = sha1($event['uid'] . ';' . $this->rc->user->ID);
$mail = base64_encode($attendee);
$hash = substr(md5($base . $mail . $this->rc->config->get('des_key')), 0, 6);
return "$base.$mail.$hash";
}
/**
* Decode the given iTIP request token and return its parts
*
* @param string Request token to decode
* @return mixed Hash array with parts or False if invalid
*/
public function decode_token($token)
{
list($base, $mail, $hash) = explode('.', $token);
// validate and return parts
if ($mail && $hash && $hash == substr(md5($base . $mail . $this->rc->config->get('des_key')), 0, 6)) {
return array('base' => $base, 'attendee' => base64_decode($mail));
}
return false;
}
/**
* Helper method to serialize the given event for storing in invitations table
*/
private static function serialize_event($event)
{
$ev = $event;
$ev['description'] = abbreviate_string($ev['description'], 100);
unset($ev['attachments']);
return serialize($ev);
}
}
diff --git a/plugins/calendar/localization/de_CH.inc b/plugins/calendar/localization/de_CH.inc
index aceeccb1..ee8383df 100644
--- a/plugins/calendar/localization/de_CH.inc
+++ b/plugins/calendar/localization/de_CH.inc
@@ -1,250 +1,250 @@
<?php
$labels = array();
// preferences
$labels['default_view'] = 'Ansicht';
$labels['time_format'] = 'Zeitformatierung';
$labels['timeslots'] = 'Zeitraster pro Stunde';
$labels['first_day'] = 'Erster Wochentag';
$labels['first_hour'] = 'Erste angezeigte Stunde';
$labels['workinghours'] = 'Arbeitszeiten';
$labels['add_category'] = 'Katgorie hinzufügen';
$labels['remove_category'] = 'Katgorie entfernen';
$labels['defaultcalendar'] = 'Neue Termine erstellen in';
$labels['eventcoloring'] = 'Färbung der Termine';
$labels['coloringmode0'] = 'Farbe des Kalenders';
$labels['coloringmode1'] = 'Farbe der Kategorie';
$labels['coloringmode2'] = 'Kalenderfarbe aussen, Kategoriefarbe innen';
$labels['coloringmode3'] = 'Kategoriefarbe aussen, Kalenderfarbe innen';
// calendar
$labels['calendar'] = 'Kalender';
$labels['calendars'] = 'Kalender';
$labels['category'] = 'Kategorie';
$labels['categories'] = 'Kategorien';
$labels['createcalendar'] = 'Neuen Kalender erstellen';
$labels['editcalendar'] = 'Kalendereigenschaften bearbeiten';
$labels['name'] = 'Name';
$labels['color'] = 'Farbe';
$labels['day'] = 'Tag';
$labels['week'] = 'Woche';
$labels['month'] = 'Monat';
$labels['agenda'] = 'Agenda';
$labels['new'] = 'Neu';
$labels['new_event'] = 'Neuer Termin';
$labels['edit_event'] = 'Termin bearbeiten';
$labels['edit'] = 'Bearbeiten';
$labels['save'] = 'Speichern';
$labels['remove'] = 'Entfernen';
$labels['cancel'] = 'Abbrechen';
$labels['select'] = 'Auswählen';
$labels['print'] = 'Drucken';
$labels['printtitle'] = 'Kalender drucken';
$labels['title'] = 'Titel';
$labels['description'] = 'Beschrieb';
$labels['all-day'] = 'ganztägig';
$labels['export'] = 'Exportieren';
$labels['exporttitle'] = 'Kalender als iCalendar exportieren';
$labels['location'] = 'Ort';
$labels['date'] = 'Datum';
$labels['start'] = 'Beginn';
$labels['end'] = 'Ende';
$labels['selectdate'] = 'Datum auswählen';
$labels['freebusy'] = 'Zeige mich als';
$labels['free'] = 'Frei';
$labels['busy'] = 'Gebucht';
$labels['outofoffice'] = 'Abwesend';
$labels['tentative'] = 'Mit Vorbehalt';
$labels['priority'] = 'Priorität';
$labels['sensitivity'] = 'Sichtbarkeit';
$labels['public'] = 'öffentlich';
$labels['private'] = 'privat';
$labels['confidential'] = 'vertraulich';
$labels['alarms'] = 'Erinnerung';
$labels['generated'] = 'erstellt am';
$labels['printdescriptions'] = 'Beschrieb drucken';
-$labels['parentcalendar'] = 'Übergeordneter Kalender';
+$labels['parentcalendar'] = 'Erstellen in';
$labels['searchearlierdates'] = '« Frühere Termine suchen';
$labels['searchlaterdates'] = 'Spätere Termine suchen »';
$labels['andnmore'] = '$nr weitere...';
$labels['showmore'] = 'Mehr anzeigen...';
$labels['togglerole'] = 'Klick zum Ändern der Rolle';
$labels['createfrommail'] = 'Als Termin speichern';
$labels['importevents'] = 'Termine importieren';
$labels['importrange'] = 'Termine ab';
$labels['onemonthback'] = '1 Monat zurück';
$labels['nmonthsback'] = '$nr Monate zurück';
$labels['showurl'] = 'URL anzeigen';
$labels['showurldescription'] = 'Über die folgende Adresse können Sie mit einem beliebigen Kalenderprogramm auf Ihren Kalender zugreifen, sofern dieses das iCal-Format unterstützt.';
// agenda view
$labels['listrange'] = 'Angezeigter Bereich:';
$labels['listsections'] = 'Unterteilung:';
$labels['smartsections'] = 'Intelligent';
$labels['until'] = 'bis';
$labels['today'] = 'Heute';
$labels['tomorrow'] = 'Morgen';
$labels['thisweek'] = 'Diese Woche';
$labels['nextweek'] = 'Nächste Woche';
$labels['thismonth'] = 'Diesen Monat';
$labels['nextmonth'] = 'Nächsten Monat';
$labels['weekofyear'] = 'KW';
$labels['pastevents'] = 'Vergangene';
$labels['futureevents'] = 'Zukünftige';
// alarm/reminder settings
$labels['showalarms'] = 'Erinnerungen anzeigen';
$labels['alarmemail'] = 'E-Mail senden';
$labels['alarmdisplay'] = 'Nachricht anzeigen';
$labels['alarmdisplayoption'] = 'Nachricht';
$labels['alarmemailoption'] = 'E-Mail';
$labels['alarmat'] = 'um $datetime';
$labels['trigger@'] = 'genau um';
$labels['trigger-M'] = 'Minuten davor';
$labels['trigger-H'] = 'Stunden davor';
$labels['trigger-D'] = 'Tage davor';
$labels['trigger+M'] = 'Minuten danach';
$labels['trigger+H'] = 'Stunden danach';
$labels['trigger+D'] = 'Tage danach';
$labels['addalarm'] = 'Erinnerung hinzufügen';
$labels['defaultalarmtype'] = 'Standard-Erinnerungseinstellung';
$labels['defaultalarmoffset'] = 'Standard-Erinnerungszeit';
$labels['dismissall'] = 'Alle ignorieren';
$labels['dismiss'] = 'Ignorieren';
$labels['snooze'] = 'Später erinnern';
$labels['repeatinmin'] = 'Wiederholung in $min Minuten';
$labels['repeatinhr'] = 'Wiederholung in 1 Stunde';
$labels['repeatinhrs'] = 'Wiederholung in $hrs Stunden';
$labels['repeattomorrow'] = 'Wiederholung morgen';
$labels['repeatinweek'] = 'Wiederholung in einer Woche';
$labels['alarmtitle'] = 'Anstehende Termine';
// attendees
$labels['attendee'] = 'Teilnehmer';
$labels['role'] = 'Rolle';
$labels['availability'] = 'Verfüg.';
$labels['confirmstate'] = 'Status';
$labels['addattendee'] = 'Hinzufügen';
$labels['roleorganizer'] = 'Organisator';
$labels['rolerequired'] = 'Erforderlich';
$labels['roleoptional'] = 'Optional';
$labels['roleresource'] = 'Ressource';
$labels['availfree'] = 'Frei';
$labels['availbusy'] = 'Gebucht';
$labels['availunknown'] = 'Unbekannt';
$labels['availtentative'] = 'Mit Vorbehalt';
$labels['availoutofoffice'] = 'Abwesend';
$labels['scheduletime'] = 'Verfügbarkeit anzeigen';
$labels['sendinvitations'] = 'Einladungen versenden';
$labels['sendnotifications'] = 'Teilnehmer über die Änderungen informieren';
$labels['sendcancellation'] = 'Teilnehmer über die Terminabsage informieren';
$labels['onlyworkinghours'] = 'Verfügbarkeit innerhalb meiner Arbeitszeiten suchen';
$labels['reqallattendees'] = 'Erforderliche/alle Teilnehmer';
$labels['prevslot'] = 'Vorheriger Vorschlag';
$labels['nextslot'] = 'Nächster Vorschlag';
$labels['noslotfound'] = 'Es konnten keine freien Zeiten gefunden werden';
$labels['invitationsubject'] = 'Sie wurden zu "$title" eingeladen';
$labels['invitationmailbody'] = "*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees\n\nIm Anhang finden Sie eine iCalendar-Datei mit allen Details des Termins. Diese können Sie in Ihre Kalenderanwendung importieren.";
$labels['invitationattendlinks'] = "Falls Ihr E-Mail-Programm keine iTip-Anfragen unterstützt, können Sie den folgenden Link verwenden, um den Termin zu bestätigen oder abzulehnen:\n\$url";
$labels['eventupdatesubject'] = '"$title" wurde aktualisiert';
$labels['eventupdatesubjectempty'] = 'Termin wurde aktualisiert';
$labels['eventupdatemailbody'] = "*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees\n\nIm Anhang finden Sie eine iCalendar-Datei mit den aktualisiereten Termindaten. Diese können Sie in Ihre Kalenderanwendung importieren.";
$labels['eventcancelsubject'] = '"$title" wurde abgesagt';
$labels['eventcancelmailbody'] = "*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees\n\nDer Termin wurde von \$organizer abgesagt.\n\nIm Anhang finden Sie eine iCalendar-Datei mit den Termindaten.";
// invitation handling
$labels['itipinvitation'] = 'Einladung zu';
$labels['itipreply'] = 'Antwort zu';
$labels['itipupdate'] = 'Aktialisiert:';
$labels['itipcancellation'] = 'Abgesagt:';
$labels['itipaccepted'] = 'Akzeptieren';
$labels['itiptentative'] = 'Mit Vorbehalt';
$labels['itipdeclined'] = 'Ablehnen';
$labels['itipsubjectaccepted'] = 'Einladung zu "$title" wurde von $name angenommen';
$labels['itipsubjecttentative'] = 'Einladung zu "$title" wurde von $name mit Vorbehalt angenommen';
$labels['itipsubjectdeclined'] = 'Einladung zu "$title" wurde von $name abgelehnt';
$labels['itipmailbodyaccepted'] = "\$sender hat die Einladung zum folgenden Termin angenommen:\n\n*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees";
$labels['itipmailbodytentative'] = "\$sender hat die Einladung mit Vorbehalt zum folgenden Termin angenommen:\n\n*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees";
$labels['itipmailbodydeclined'] = "\$sender hat die Einladung zum folgenden Termin abgelehnt:\n\n*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees";
$labels['itipdeclineevent'] = 'Möchten Sie die Einladung zu diesem Termin ablehnen?';
$labels['importtocalendar'] = 'In Kalender übernehmen';
$labels['removefromcalendar'] = 'Aus meinem Kalender löschen';
$labels['updateattendeestatus'] = 'Teilnehmerstatus aktualisieren';
$labels['acceptinvitation'] = 'Möchten Sie die Einladung zu diesem Termin annehmen?';
$labels['youhaveaccepted'] = 'Sie haben die Einladung angenommen';
$labels['youhavetentative'] = 'Sie haben die Einladung mit Vorbehalt angenommen';
$labels['youhavedeclined'] = 'Sie haben die Einladung abgelehnt';
$labels['notanattendee'] = 'Sie sind nicht in der Liste der Teilnehmer aufgeführt';
$labels['eventcancelled'] = 'Der Termin wurde vom Organisator abgesagt';
$labels['saveincalendar'] = 'speichern in';
// event dialog tabs
$labels['tabsummary'] = 'Übersicht';
$labels['tabrecurrence'] = 'Wiederholung';
$labels['tabattendees'] = 'Teilnehmer';
$labels['tabattachments'] = 'Anhänge';
$labels['tabsharing'] = 'Freigabe';
// messages
$labels['deleteventconfirm'] = 'Möchten Sie diesen Termin wirklich löschen?';
$labels['deletecalendarconfirm'] = 'Möchten Sie diesen Kalender mit allen Terminen wirklich löschen?';
$labels['savingdata'] = 'Speichere Daten...';
$labels['errorsaving'] = 'Fehler beim Speichern.';
$labels['operationfailed'] = 'Die Aktion ist fehlgeschlagen.';
$labels['invalideventdates'] = 'Ungültige Daten eingegeben! Bitte überprüfen Sie die Eingaben.';
$labels['invalidcalendarproperties'] = 'Ungültige Kalenderinformationen! Bitte geben Sie einen Namen ein.';
$labels['searchnoresults'] = 'Keine Termine in den gewählten Kalendern gefunden.';
$labels['successremoval'] = 'Der Termin wurde erfolgreich gelöscht.';
$labels['successrestore'] = 'Der Termin wurde erfolgreich wieder hergestellt.';
$labels['errornotifying'] = 'Benachrichtigung an die Teilnehmer konnten nicht gesendet werden';
$labels['errorimportingevent'] = 'Fehler beim Importieren';
$labels['newerversionexists'] = 'Eine neuere Version dieses Termins exisitert bereits! Import abgebrochen.';
$labels['nowritecalendarfound'] = 'Kein Kalender zum Speichern gefunden';
$labels['importedsuccessfully'] = 'Der Termin wurde erfolgreich in \'$calendar\' gespeichert';
$labels['attendeupdateesuccess'] = 'Teilnehmerstatus erfolgreich aktualisiert';
$labels['itipresponseerror'] = 'Die Antwort auf diese Einladung konnte nicht versendet werden';
$labels['sentresponseto'] = 'Antwort auf diese Einladung erfolgreich an $mailto gesendet';
$labels['localchangeswarning'] = 'Die Änderungen an diesem Termin können nur in Ihrem persönlichen Kalender gespeichert werden.';
$labels['importsuccess'] = 'Es wurden $nr Termine erfolgreich importiert';
$labels['importnone'] = 'Keine Termine zum Importieren gefunden';
$labels['importerror'] = 'Fehler beim Importieren';
// recurrence form
$labels['repeat'] = 'Wiederholung';
$labels['frequency'] = 'Wiederholung';
$labels['never'] = 'nie';
$labels['daily'] = 'täglich';
$labels['weekly'] = 'wöchentlich';
$labels['monthly'] = 'monatlich';
$labels['yearly'] = 'jährlich';
$labels['every'] = 'Alle';
$labels['days'] = 'Tag(e)';
$labels['weeks'] = 'Woche(n)';
$labels['months'] = 'Monat(e)';
$labels['years'] = 'Jahre(e) im:';
$labels['bydays'] = 'Am';
$labels['untildate'] = 'am';
$labels['each'] = 'Jeden';
$labels['onevery'] = 'An jedem';
$labels['onsamedate'] = 'Am gleichen Tag';
$labels['forever'] = 'unendlich';
$labels['recurrencend'] = 'bis';
$labels['forntimes'] = '$nr Wiederholungen';
$labels['first'] = 'erster';
$labels['second'] = 'zweiter';
$labels['third'] = 'dritter';
$labels['fourth'] = 'vierter';
$labels['last'] = 'letzter';
$labels['dayofmonth'] = 'Tag des Montats';
$labels['changeeventconfirm'] = 'Termin ändern';
$labels['removeeventconfirm'] = 'Termin löschen';
$labels['changerecurringeventwarning'] = 'Dies ist eine Terminreihe. Möchten Sie nur den aktuellen, diesen und alle zukünftigen oder alle Termine bearbeiten oder die Änderungen als neuen Termin speichern?';
$labels['removerecurringeventwarning'] = 'Dies ist eine Terminreihe. Möchten Sie nur den aktuellen, diesen und alle zukünftigen oder alle Termine löschen?';
$labels['currentevent'] = 'Aktuellen';
$labels['futurevents'] = 'Zukünftige';
$labels['allevents'] = 'Alle';
$labels['saveasnew'] = 'Als neu speichern';
?>
diff --git a/plugins/calendar/localization/de_DE.inc b/plugins/calendar/localization/de_DE.inc
index a3e6510b..3c3a4bdf 100644
--- a/plugins/calendar/localization/de_DE.inc
+++ b/plugins/calendar/localization/de_DE.inc
@@ -1,250 +1,250 @@
<?php
$labels = array();
// preferences
$labels['default_view'] = 'Ansicht';
$labels['time_format'] = 'Zeitformatierung';
$labels['timeslots'] = 'Zeitraster pro Stunde';
$labels['first_day'] = 'Erster Wochentag';
$labels['first_hour'] = 'Erste angezeigte Stunde';
$labels['workinghours'] = 'Arbeitszeiten';
$labels['add_category'] = 'Katgorie hinzufügen';
$labels['remove_category'] = 'Katgorie entfernen';
$labels['defaultcalendar'] = 'Neue Termine erstellen in';
$labels['eventcoloring'] = 'Färbung der Termine';
$labels['coloringmode0'] = 'Farbe des Kalenders';
$labels['coloringmode1'] = 'Farbe der Kategorie';
$labels['coloringmode2'] = 'Kalenderfarbe außen, Kategoriefarbe innen';
$labels['coloringmode3'] = 'Kategoriefarbe außen, Kalenderfarbe innen';
// calendar
$labels['calendar'] = 'Kalender';
$labels['calendars'] = 'Kalender';
$labels['category'] = 'Kategorie';
$labels['categories'] = 'Kategorien';
$labels['createcalendar'] = 'Neuen Kalender erstellen';
$labels['editcalendar'] = 'Kalendereigenschaften bearbeiten';
$labels['name'] = 'Name';
$labels['color'] = 'Farbe';
$labels['day'] = 'Tag';
$labels['week'] = 'Woche';
$labels['month'] = 'Monat';
$labels['agenda'] = 'Agenda';
$labels['new'] = 'Neu';
$labels['new_event'] = 'Neuer Termin';
$labels['edit_event'] = 'Termin bearbeiten';
$labels['edit'] = 'Bearbeiten';
$labels['save'] = 'Speichern';
$labels['remove'] = 'Entfernen';
$labels['cancel'] = 'Abbrechen';
$labels['select'] = 'Auswählen';
$labels['print'] = 'Drucken';
$labels['printtitle'] = 'Kalender drucken';
$labels['title'] = 'Titel';
$labels['description'] = 'Beschrieb';
$labels['all-day'] = 'ganztägig';
$labels['export'] = 'Exportieren';
$labels['exporttitle'] = 'Kalender als iCalendar exportieren';
$labels['location'] = 'Ort';
$labels['date'] = 'Datum';
$labels['start'] = 'Beginn';
$labels['end'] = 'Ende';
$labels['selectdate'] = 'Datum auswählen';
$labels['freebusy'] = 'Zeige mich als';
$labels['free'] = 'Frei';
$labels['busy'] = 'Gebucht';
$labels['outofoffice'] = 'Abwesend';
$labels['tentative'] = 'Mit Vorbehalt';
$labels['priority'] = 'Priorität';
$labels['sensitivity'] = 'Sichtbarkeit';
$labels['public'] = 'öffentlich';
$labels['private'] = 'privat';
$labels['confidential'] = 'vertraulich';
$labels['alarms'] = 'Erinnerung';
$labels['generated'] = 'erstellt am';
$labels['printdescriptions'] = 'Beschrieb drucken';
-$labels['parentcalendar'] = 'Übergeordneter Kalender';
+$labels['parentcalendar'] = 'Erstellen in';
$labels['searchearlierdates'] = '« Frühere Termine suchen';
$labels['searchlaterdates'] = 'Spätere Termine suchen »';
$labels['andnmore'] = '$nr weitere...';
$labels['showmore'] = 'Mehr anzeigen...';
$labels['togglerole'] = 'Klick zum Ändern der Rolle';
$labels['createfrommail'] = 'Als Termin speichern';
$labels['importevents'] = 'Termine importieren';
$labels['importrange'] = 'Termine ab';
$labels['onemonthback'] = '1 Monat zurück';
$labels['nmonthsback'] = '$nr Monate zurück';
$labels['showurl'] = 'URL anzeigen';
$labels['showurldescription'] = 'Über die folgende Adresse können Sie mit einem beliebigen Kalenderprogramm auf Ihren Kalender zugreifen, sofern dieses das iCal-Format unterstützt.';
// agenda view
$labels['listrange'] = 'Angezeigter Bereich:';
$labels['listsections'] = 'Unterteilung:';
$labels['smartsections'] = 'Intelligent';
$labels['until'] = 'bis';
$labels['today'] = 'Heute';
$labels['tomorrow'] = 'Morgen';
$labels['thisweek'] = 'Diese Woche';
$labels['nextweek'] = 'Nächste Woche';
$labels['thismonth'] = 'Diesen Monat';
$labels['nextmonth'] = 'Nächsten Monat';
$labels['weekofyear'] = 'Woche';
$labels['pastevents'] = 'Vergangene';
$labels['futureevents'] = 'Zukünftige';
// alarm/reminder settings
$labels['showalarms'] = 'Erinnerungen anzeigen';
$labels['alarmemail'] = 'E-Mail senden';
$labels['alarmdisplay'] = 'Nachricht anzeigen';
$labels['alarmdisplayoption'] = 'Nachricht';
$labels['alarmemailoption'] = 'E-Mail';
$labels['alarmat'] = 'um $datetime';
$labels['trigger@'] = 'genau um';
$labels['trigger-M'] = 'Minuten davor';
$labels['trigger-H'] = 'Stunden davor';
$labels['trigger-D'] = 'Tage davor';
$labels['trigger+M'] = 'Minuten danach';
$labels['trigger+H'] = 'Stunden danach';
$labels['trigger+D'] = 'Tage danach';
$labels['addalarm'] = 'Erinnerung hinzufügen';
$labels['defaultalarmtype'] = 'Standard-Erinnerungseinstellung';
$labels['defaultalarmoffset'] = 'Standard-Erinnerungszeit';
$labels['dismissall'] = 'Alle ignorieren';
$labels['dismiss'] = 'Ignorieren';
$labels['snooze'] = 'Später erinnern';
$labels['repeatinmin'] = 'Wiederholung in $min Minuten';
$labels['repeatinhr'] = 'Wiederholung in 1 Stunde';
$labels['repeatinhrs'] = 'Wiederholung in $hrs Stunden';
$labels['repeattomorrow'] = 'Wiederholung morgen';
$labels['repeatinweek'] = 'Wiederholung in einer Woche';
$labels['alarmtitle'] = 'Anstehende Termine';
// attendees
$labels['attendee'] = 'Teilnehmer';
$labels['role'] = 'Rolle';
$labels['availability'] = 'Verfüg.';
$labels['confirmstate'] = 'Status';
$labels['addattendee'] = 'Hinzufügen';
$labels['roleorganizer'] = 'Organisator';
$labels['rolerequired'] = 'Erforderlich';
$labels['roleoptional'] = 'Optional';
$labels['roleresource'] = 'Ressource';
$labels['availfree'] = 'Frei';
$labels['availbusy'] = 'Gebucht';
$labels['availunknown'] = 'Unbekannt';
$labels['availtentative'] = 'Mit Vorbehalt';
$labels['availoutofoffice'] = 'Abwesend';
$labels['scheduletime'] = 'Verfügbarkeit anzeigen';
$labels['sendinvitations'] = 'Einladungen versenden';
$labels['sendnotifications'] = 'Teilnehmer über die Änderungen informieren';
$labels['sendcancellation'] = 'Teilnehmer über die Terminabsage informieren';
$labels['onlyworkinghours'] = 'Verfügbarkeit innerhalb meiner Arbeitszeiten suchen';
$labels['reqallattendees'] = 'Erforderliche/alle Teilnehmer';
$labels['prevslot'] = 'Vorheriger Vorschlag';
$labels['nextslot'] = 'Nächster Vorschlag';
$labels['noslotfound'] = 'Es konnten keine freien Zeiten gefunden werden';
$labels['invitationsubject'] = 'Sie wurden zu "$title" eingeladen';
$labels['invitationmailbody'] = "*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees\n\nIm Anhang finden Sie eine iCalendar-Datei mit allen Details des Termins. Diese können Sie in Ihre Kalenderanwendung importieren.";
$labels['invitationattendlinks'] = "Falls Ihr E-Mail-Programm keine iTip-Anfragen unterstützt, können Sie den folgenden Link verwenden, um den Termin zu bestätigen oder abzulehnen:\n\$url";
$labels['eventupdatesubject'] = '"$title" wurde aktualisiert';
$labels['eventupdatesubjectempty'] = 'Termin wurde aktualisiert';
$labels['eventupdatemailbody'] = "*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees\n\nIm Anhang finden Sie eine iCalendar-Datei mit den aktualisiereten Termindaten. Diese können Sie in Ihre Kalenderanwendung importieren.";
$labels['eventcancelsubject'] = '"$title" wurde abgesagt';
$labels['eventcancelmailbody'] = "*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees\n\nDer Termin wurde von \$organizer abgesagt.\n\nIm Anhang finden Sie eine iCalendar-Datei mit den Termindaten.";
// invitation handling
$labels['itipinvitation'] = 'Einladung zu';
$labels['itipreply'] = 'Antwort zu';
$labels['itipupdate'] = 'Aktialisiert:';
$labels['itipcancellation'] = 'Abgesagt:';
$labels['itipaccepted'] = 'Akzeptieren';
$labels['itiptentative'] = 'Mit Vorbehalt';
$labels['itipdeclined'] = 'Ablehnen';
$labels['itipsubjectaccepted'] = 'Einladung zu "$title" wurde von $name angenommen';
$labels['itipsubjecttentative'] = 'Einladung zu "$title" wurde von $name mit Vorbehalt angenommen';
$labels['itipsubjectdeclined'] = 'Einladung zu "$title" wurde von $name abgelehnt';
$labels['itipmailbodyaccepted'] = "\$sender hat die Einladung zum folgenden Termin angenommen:\n\n*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees";
$labels['itipmailbodytentative'] = "\$sender hat die Einladung mit Vorbehalt zum folgenden Termin angenommen:\n\n*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees";
$labels['itipmailbodydeclined'] = "\$sender hat die Einladung zum folgenden Termin abgelehnt:\n\n*\$title*\n\nWann: \$date\n\nTeilnehmer: \$attendees";
$labels['itipdeclineevent'] = 'Möchten Sie die Einladung zu diesem Termin ablehnen?';
$labels['importtocalendar'] = 'In Kalender übernehmen';
$labels['removefromcalendar'] = 'Aus meinem Kalender löschen';
$labels['updateattendeestatus'] = 'Teilnehmerstatus aktualisieren';
$labels['acceptinvitation'] = 'Möchten Sie die Einladung zu diesem Termin annehmen?';
$labels['youhaveaccepted'] = 'Sie haben die Einladung angenommen';
$labels['youhavetentative'] = 'Sie haben die Einladung mit Vorbehalt angenommen';
$labels['youhavedeclined'] = 'Sie haben die Einladung abgelehnt';
$labels['notanattendee'] = 'Sie sind nicht in der Liste der Teilnehmer aufgeführt';
$labels['eventcancelled'] = 'Der Termin wurde vom Organisator abgesagt';
$labels['saveincalendar'] = 'speichern in';
// event dialog tabs
$labels['tabsummary'] = 'Übersicht';
$labels['tabrecurrence'] = 'Wiederholung';
$labels['tabattendees'] = 'Teilnehmer';
$labels['tabattachments'] = 'Anhänge';
$labels['tabsharing'] = 'Freigabe';
// messages
$labels['deleteventconfirm'] = 'Möchten Sie diesen Termin wirklich löschen?';
$labels['deletecalendarconfirm'] = 'Möchten Sie diesen Kalender mit allen Terminen wirklich löschen?';
$labels['savingdata'] = 'Speichere Daten...';
$labels['errorsaving'] = 'Fehler beim Speichern.';
$labels['operationfailed'] = 'Die Aktion ist fehlgeschlagen.';
$labels['invalideventdates'] = 'Ungültige Daten eingegeben! Bitte überprüfen Sie die Eingaben.';
$labels['invalidcalendarproperties'] = 'Ungültige Kalenderinformationen! Bitte geben Sie einen Namen ein.';
$labels['searchnoresults'] = 'Keine Termine in den gewählten Kalendern gefunden.';
$labels['successremoval'] = 'Der Termin wurde erfolgreich gelöscht.';
$labels['successrestore'] = 'Der Termin wurde erfolgreich wieder hergestellt.';
$labels['errornotifying'] = 'Benachrichtigung an die Teilnehmer konnten nicht gesendet werden';
$labels['errorimportingevent'] = 'Fehler beim Importieren';
$labels['newerversionexists'] = 'Eine neuere Version dieses Termins exisitert bereits! Import abgebrochen.';
$labels['nowritecalendarfound'] = 'Kein Kalender zum Speichern gefunden';
$labels['importedsuccessfully'] = 'Der Termin wurde erfolgreich in \'$calendar\' gespeichert';
$labels['attendeupdateesuccess'] = 'Teilnehmerstatus erfolgreich aktualisiert';
$labels['itipresponseerror'] = 'Die Antwort auf diese Einladung konnte nicht versendet werden';
$labels['sentresponseto'] = 'Antwort auf diese Einladung erfolgreich an $mailto gesendet';
$labels['localchangeswarning'] = 'Die Änderungen an diesem Termin können nur in Ihrem persönlichen Kalender gespeichert werden.';
$labels['importsuccess'] = 'Es wurden $nr Termine erfolgreich importiert';
$labels['importnone'] = 'Keine Termine zum Importieren gefunden';
$labels['importerror'] = 'Fehler beim Importieren';
// recurrence form
$labels['repeat'] = 'Wiederholung';
$labels['frequency'] = 'Wiederholung';
$labels['never'] = 'nie';
$labels['daily'] = 'täglich';
$labels['weekly'] = 'wöchentlich';
$labels['monthly'] = 'monatlich';
$labels['yearly'] = 'jährlich';
$labels['every'] = 'Alle';
$labels['days'] = 'Tag(e)';
$labels['weeks'] = 'Woche(n)';
$labels['months'] = 'Monat(e)';
$labels['years'] = 'Jahre(e) im:';
$labels['bydays'] = 'Am';
$labels['untildate'] = 'am';
$labels['each'] = 'Jeden';
$labels['onevery'] = 'An jedem';
$labels['onsamedate'] = 'Am gleichen Tag';
$labels['forever'] = 'unendlich';
$labels['recurrencend'] = 'bis';
$labels['forntimes'] = '$nr Wiederholungen';
$labels['first'] = 'erster';
$labels['second'] = 'zweiter';
$labels['third'] = 'dritter';
$labels['fourth'] = 'vierter';
$labels['last'] = 'letzter';
$labels['dayofmonth'] = 'Tag des Montats';
$labels['changeeventconfirm'] = 'Termin ändern';
$labels['removeeventconfirm'] = 'Termin löschen';
$labels['changerecurringeventwarning'] = 'Dies ist eine Terminreihe. Möchten Sie nur den aktuellen, diesen und alle zukünftigen oder alle Termine bearbeiten oder die Änderungen als neuen Termin speichern?';
$labels['removerecurringeventwarning'] = 'Dies ist eine Terminreihe. Möchten Sie nur den aktuellen, diesen und alle zukünftigen oder alle Termine löschen?';
$labels['currentevent'] = 'Aktuellen';
$labels['futurevents'] = 'Zukünftige';
$labels['allevents'] = 'Alle';
$labels['saveasnew'] = 'Als neu speichern';
?>
diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc
index a5e48d5e..e13cd88b 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -1,251 +1,252 @@
<?php
$labels = array();
// preferences
$labels['default_view'] = 'Default view';
$labels['time_format'] = 'Time format';
$labels['timeslots'] = 'Timeslots 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['location'] = 'Location';
$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['generated'] = 'generated at';
$labels['printdescriptions'] = 'Print descriptions';
-$labels['parentcalendar'] = 'Superior calendar';
+$labels['parentcalendar'] = 'Insert inside';
$labels['searchearlierdates'] = '« Search for earlier events';
$labels['searchlaterdates'] = 'Search for later events »';
$labels['andnmore'] = '$nr more...';
$labels['showmore'] = 'Show more...';
$labels['togglerole'] = 'Click to toggle role';
$labels['createfrommail'] = 'Save as event';
$labels['importevents'] = 'Import events';
$labels['importrange'] = 'Events from';
$labels['onemonthback'] = '1 month back';
$labels['nmonthsback'] = '$nr months back';
$labels['showurl'] = 'Show calendar URL';
$labels['showurldescription'] = 'Use the following address to access your calendar from other applications. You can copy and paste this into any calendar software that supports the iCal format.';
// 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 alarms';
$labels['alarmemail'] = 'Send Email';
$labels['alarmdisplay'] = 'Show message';
$labels['alarmdisplayoption'] = 'Message';
$labels['alarmemailoption'] = 'Email';
$labels['alarmat'] = 'at $datetime';
$labels['trigger@'] = 'on date';
$labels['trigger-M'] = 'minutes before';
$labels['trigger-H'] = 'hours before';
$labels['trigger-D'] = 'days before';
$labels['trigger+M'] = 'minutes after';
$labels['trigger+H'] = 'hours after';
$labels['trigger+D'] = 'days after';
$labels['addalarm'] = 'add alarm';
$labels['defaultalarmtype'] = 'Default reminder setting';
$labels['defaultalarmoffset'] = 'Default reminder time';
$labels['dismissall'] = 'Dismiss all';
$labels['dismiss'] = 'Dismiss';
$labels['snooze'] = 'Snooze';
$labels['repeatinmin'] = 'Repeat in $min minutes';
$labels['repeatinhr'] = 'Repeat in 1 hour';
$labels['repeatinhrs'] = 'Repeat in $hrs hours';
$labels['repeattomorrow'] = 'Repeat tomorrow';
$labels['repeatinweek'] = 'Repeat in a week';
$labels['alarmtitle'] = 'Upcoming events';
// 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['roleresource'] = 'Resource';
$labels['availfree'] = 'Free';
$labels['availbusy'] = 'Busy';
$labels['availunknown'] = 'Unknown';
$labels['availtentative'] = 'Tentative';
$labels['availoutofoffice'] = 'Out of Office';
$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
$labels['itipinvitation'] = 'Invitation to';
$labels['itipupdate'] = 'Update of';
$labels['itipcancellation'] = 'Cancelled:';
$labels['itipreply'] = 'Reply to';
$labels['itipaccepted'] = 'Accept';
$labels['itiptentative'] = 'Maybe';
$labels['itipdeclined'] = 'Decline';
$labels['itipsubjectaccepted'] = '"$title" has been accepted by $name';
$labels['itipsubjecttentative'] = '"$title" has been tentatively accepted by $name';
$labels['itipsubjectdeclined'] = '"$title" has been declined by $name';
$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['itipdeclineevent'] = 'Do you want to decline your invitation to this event?';
$labels['importtocalendar'] = 'Save to my calendar';
$labels['removefromcalendar'] = 'Remove from my calendar';
$labels['updateattendeestatus'] = 'Update the participant\'s status';
$labels['acceptinvitation'] = 'Do you accept this invitation?';
$labels['youhaveaccepted'] = 'You have accepted this invitation';
$labels['youhavetentative'] = 'You have tentatively accepted this invitation';
$labels['youhavedeclined'] = 'You have declined this invitation';
$labels['notanattendee'] = 'You\'re not listed as an attendee of this event';
$labels['eventcancelled'] = 'The event has been cancelled';
$labels['saveincalendar'] = 'save in';
// event dialog tabs
$labels['tabsummary'] = 'Summary';
$labels['tabrecurrence'] = 'Recurrence';
$labels['tabattendees'] = 'Participants';
$labels['tabattachments'] = 'Attachments';
$labels['tabsharing'] = 'Sharing';
// messages
$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['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['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['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 personal calendar';
$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['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['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';
?>
diff --git a/plugins/calendar/skins/default/calendar.css b/plugins/calendar/skins/default/calendar.css
index 23e27507..21dcb1b9 100644
--- a/plugins/calendar/skins/default/calendar.css
+++ b/plugins/calendar/skins/default/calendar.css
@@ -1,1268 +1,1268 @@
/*** 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: 90px;
left: 0;
right: 0;
bottom: 10px;
}
-#sidebar {
+#calendarsidebar {
position: absolute;
top: 37px;
left: 10px;
bottom: 0;
width: 230px;
}
#datepicker {
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;
}
-#sidebartoggle {
+#calendarsidebartoggle {
position: absolute;
left: 244px;
width: 8px;
top: 37px;
bottom: 0;
background: url(images/toggle.gif) 0 48% no-repeat transparent;
cursor: pointer;
}
div.sidebarclosed {
background-position: -8px 48% !important;
}
-#sidebartoggle:hover {
+#calendarsidebartoggle:hover {
background-color: #ddd;
}
#calendar {
position: absolute;
top: 0;
left: 256px;
right: 10px;
bottom: 0;
}
#print {
width: 680px;
}
pre {
font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
}
#calendars {
position: absolute;
top: 220px;
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;
}
#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;
}
#calfeedurl {
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: 45px;
left: 256px;
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.export {
background-position: -128px 0;
}
#calendartoolbar a.exportSel {
background-position: -128px -32px;
}
#quicksearchbar {
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 .buttons {
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.organizer {
background-position: right -80px;
}
/* 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;
}
#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 {
position: relative;
margin-top: 1em;
}
#edit-attendees-form #edit-attendee-schedule {
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;
}
#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 .chair {
background-position: 2px -137px;
}
.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;
}
/* fullcalendar style overrides */
.rcube-fc-content {
position: absolute !important;
top: 37px;
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 col.fc-event-location {
width: 20%;
}
.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 {
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 .rsvp-status,
div.calendar-invitebox .rsvp-buttons {
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 .calendar-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.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;
}
/* 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/default/print.css b/plugins/calendar/skins/default/print.css
index eeb4fadb..3c9bd716 100644
--- a/plugins/calendar/skins/default/print.css
+++ b/plugins/calendar/skins/default/print.css
@@ -1,204 +1,208 @@
/*** Printing styles for Calendar plugin ***/
body {
margin: 0;
color: #000;
background: #fff;
}
body, td, th, div, p, h3, select, input, textarea {
font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
font-size: 8pt;
}
#calendar {
position: relative;
top: 0;
left: 0;
height: auto;
margin: 5em auto 0 auto;
overflow: visible;
}
+#calendar .fc-header-right {
+ padding-right: 0;
+}
+
#printconfig {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 10000;
padding: 0.5em;
background: #ebebeb;
border-bottom: 1px solid #999;
box-shadow: 0 3px 4px #ccc;
-moz-box-shadow: 0 3px 4px #ccc;
-webkit-box-shadow: 0 3px 4px #ccc;
}
#printconfig .prop {
padding-right: 2em;
}
#message {
position: absolute;
top: 5.5em;
left: 1em;
}
#message div.loading {
color: #666;
font-style: italic;
}
#calendarlist {
list-style-type: square;
margin: 2em 0;
padding-left: 1em;
}
#calendarlist li {
float: left;
padding-left: 0;
padding-right: 3em;
margin-left: 0;
font-weight: bold;
}
#calendarlist input {
display: none;
}
.calwidth {
width: 700px;
margin: 0 auto;
}
.rightalign {
float: right;
padding-top: 0.3em;
}
@media print {
.noprint,
.fc-header-right span {
display: none;
}
#calendar {
margin-top: 0;
}
}
/* fullcalendar style overrides */
.fc-view {
overflow: visible;
}
.fc-event-skin,
.fc-event-inner .fc-event-skin {
color: black;
background-color: #fff !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-agenda-slots td div {
height: 1.4em;
}
.fc-widget-header,
.fc-mon, .fc-tue, .fc-wed, .fc-thu, .fc-fri {
background-color: #fff;
}
.fc-widget-header, .fc-widget-content {
border-color: #ccc;
}
.fc-icon-alarms,
.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-view-list, .fc-view-table {
border: 0;
}
.fc-view-list div.fc-list-header,
.fc-view-table td.fc-list-header {
padding: 0.3em;
background: #fff;
font-weight: bold;
font-size: 1.2em;
color: #333;
border-color: #333;
border-style: solid;
border-width: 1px 0;
filter: none;
}
.fc-list-section .fc-event {
cursor: auto;
}
.fc-view-table tr.fc-event td,
.fc-view-table tr.fc-event td.fc-event-handle {
border-color: #999;
padding-top: 0.5em;
padding-bottom: 0.5em;
}
.fc-view-table tr.fc-last td {
border: 0;
}
.fc-view-table tr.fc-event .fc-event-description {
padding-left: 2em;
padding-top: 0em;
}
.fc-event-vert .fc-event-description {
font-size: 90%;
font-style: italic;
}
.fc-view-table col.fc-event-location {
width: 20%;
}
diff --git a/plugins/calendar/skins/default/templates/calendar.html b/plugins/calendar/skins/default/templates/calendar.html
index e6e12711..a22056dd 100644
--- a/plugins/calendar/skins/default/templates/calendar.html
+++ b/plugins/calendar/skins/default/templates/calendar.html
@@ -1,201 +1,201 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title><roundcube:object name="pagetitle" /></title>
<roundcube:include file="/includes/links.html" />
<script type="text/javascript" src="/functions.js"></script>
<!--[if lte IE 7]><link rel="stylesheet" type="text/css" href="./plugins/calendar/skins/default/iehacks.css" /><![endif]-->
</head>
<body class="calendarmain">
<roundcube:include file="/includes/taskbar.html" />
<roundcube:include file="/includes/header.html" />
<div id="main">
- <div id="sidebar">
+ <div id="calendarsidebar">
<div id="datepicker"></div>
<div id="calendars" style="visibility:hidden">
<div class="boxtitle"><roundcube:label name="calendar.calendars" /></div>
<div class="boxlistcontent">
<roundcube:object name="plugin.calendar_list" id="calendarslist" />
</div>
<div class="boxfooter">
<roundcube:button command="calendar-create" type="link" title="calendar.createcalendar" class="buttonPas addgroup" classAct="button addgroup" content=" " />
<roundcube:button name="calendaroptionslink" id="calendaroptionslink" type="link" title="calendaractions" class="button groupactions" onclick="rcmail_ui.show_popup('calendaroptions');return false" content=" " />
</div>
</div>
</div>
- <div id="sidebartoggle"></div>
+ <div id="calendarsidebartoggle"></div>
<div id="calendar">
<roundcube:object name="plugin.angenda_options" class="boxfooter" id="agendaoptions" />
</div>
</div>
<div id="calendaroptionsmenu" class="popupmenu">
<ul>
<li><roundcube:button command="calendar-edit" label="calendar.edit" classAct="active" /></li>
<li><roundcube:button command="calendar-remove" label="calendar.remove" classAct="active" /></li>
<li><roundcube:button command="events-import" label="calendar.importevents" classAct="active" /></li>
<li><roundcube:button command="calendar-showurl" label="calendar.showurl" classAct="active" /></li>
<roundcube:if condition="env:calendar_driver == 'kolab'" />
<li class="separator_above"><roundcube:button command="folders" task="settings" type="link" label="managefolders" classAct="active" /></li>
<roundcube:endif />
</ul>
</div>
<div id="eventshow" class="uidialog">
<h1 id="event-title">Event Title</h1>
<div class="event-section" id="event-location">Location</div>
<div class="event-section" id="event-date">From-To</div>
<div class="event-section" id="event-description">
<h5 class="label"><roundcube:label name="calendar.description" /></h5>
<div class="event-text"></div>
</div>
<div class="event-section" id="event-repeat">
<h5 class="label"><roundcube:label name="calendar.repeat" /></h5>
<div class="event-text"></div>
</div>
<div class="event-section" id="event-alarm">
<h5 class="label"><roundcube:label name="calendar.alarms" /></h5>
<div class="event-text"></div>
</div>
<div class="event-section" id="event-attendees">
<h5 class="label"><roundcube:label name="calendar.tabattendees" /></h5>
<div class="event-text"></div>
</div>
<div class="event-line" id="event-calendar">
<label><roundcube:label name="calendar.calendar" /></label>
<span class="event-text">Default</span>
</div>
<div class="event-line" id="event-category">
<label><roundcube:label name="calendar.category" /></label>
<span class="event-text"></span>
</div>
<div class="event-line" id="event-free-busy">
<label><roundcube:label name="calendar.freebusy" /></label>
<span class="event-text"></span>
</div>
<div class="event-line" id="event-priority">
<label><roundcube:label name="calendar.priority" /></label>
<span class="event-text"></span>
</div>
<div class="event-line" id="event-sensitivity">
<label><roundcube:label name="calendar.sensitivity" /></label>
<span class="event-text"></span>
</div>
<div class="event-section" id="event-attachments">
<label><roundcube:label name="attachments" /></label>
<div class="event-text attachments-list"></div>
</div>
<roundcube:object name="plugin.event_rsvp_buttons" id="event-rsvp" style="display:none" />
</div>
<roundcube:include file="/templates/eventedit.html" />
<div id="eventfreebusy" class="uidialog">
<roundcube:object name="plugin.attendees_freebusy_table" id="attendees-freebusy-table" cellspacing="0" cellpadding="0" border="0" />
<div class="schedule-options">
&nbsp;
<div class="schedule-buttons">
<button id="shedule-freebusy-prev" title="<roundcube:label name='previouspage' />">&#9668;</button><button id="shedule-freebusy-next" title="<roundcube:label name='nextpage' />">&#9658;</button>
</div>
</div>
<div style="float:left; width:28em">
<div class="form-section">
<label for="schedule-startdate"><roundcube:label name="calendar.start" /></label>
<input type="text" name="startdate" size="10" id="schedule-startdate" disabled="true" /> &nbsp;
<input type="text" name="starttime" size="6" id="schedule-starttime" disabled="true" />
</div>
<div class="form-section">
<label for="schedule-enddate"><roundcube:label name="calendar.end" /></label>
<input type="text" name="enddate" size="10" id="schedule-enddate" disabled="true" /> &nbsp;
<input type="text" name="endtime" size="6" id="schedule-endtime" disabled="true" />
</div>
</div>
<div style="float:left">
<div class="schedule-find-buttons">
<button id="shedule-find-prev">&#9668; <roundcube:label name="calendar.prevslot" /></button>
<button id="shedule-find-next"><roundcube:label name="calendar.nextslot" /> &#9658;</button>
</div>
<div class="schedule-options">
<label><input type="checkbox" id="schedule-freebusy-workinghours" value="1" /><roundcube:label name="calendar.onlyworkinghours" /></label>
</div>
</div>
<br style="clear:both;" />
<roundcube:include file="/templates/freebusylegend.html" />
<div class="attendees-list">
<span class="attendee organizer"><roundcube:label name="calendar.roleorganizer" /></span>
<span class="attendee req-participant"><roundcube:label name="calendar.rolerequired" /></span>
<span class="attendee opt-participant"><roundcube:label name="calendar.roleoptional" /></span>
<span class="attendee chair"><roundcube:label name="calendar.roleresource" /></span>
</div>
</div>
<div id="calendarform" class="uidialog">
<roundcube:label name="loading" />
</div>
<div id="eventsimport" class="uidialog">
<roundcube:object name="plugin.events_import_form" id="events-import-form" uploadFieldSize="30" />
</div>
<div id="calendarurlbox" class="uidialog">
<p><roundcube:label name="calendar.showurldescription" /></p>
<textarea id="calfeedurl" rows="2" readonly="readonly"></textarea>
</div>
<div id="alarm-snooze-dropdown" class="popupmenu">
<roundcube:object name="plugin.snooze_select" type="ul" />
</div>
<div id="calendartoolbar">
<roundcube:button command="addevent" type="link" class="buttonPas addevent" classAct="button addevent" classSel="button addeventSel" title="calendar.new_event" content=" " />
<roundcube:button command="print" type="link" class="buttonPas print" classAct="button print" classSel="button printSel" title="calendar.print" content=" " />
<roundcube:button command="export" type="link" class="buttonPas export" classAct="button export" classSel="button exportSel" title="calendar.export" content=" " />
<roundcube:container name="toolbar" id="calendartoolbar" />
</div>
<div id="quicksearchbar">
<roundcube:button name="searchmenulink" id="searchmenulink" image="/images/icons/glass.png" />
<roundcube:object name="plugin.searchform" id="quicksearchbox" />
<roundcube:button command="reset-search" id="searchreset" image="/images/icons/reset.gif" title="resetsearch" />
</div>
<roundcube:object name="plugin.calendar_css" />
<script type="text/javascript">
// use skin functions to handle popup-menus
rcube_init_mail_ui();
rcmail_ui.popups.calendaroptions = { id:'calendaroptionsmenu', above:1, obj:$('#calendaroptionsmenu') };
$(document).ready(function(e){
// initialize sidebar toggle
- $('#sidebartoggle').click(function() {
+ $('#calendarsidebartoggle').click(function() {
var width = $(this).data('sidebarwidth');
var offset = $(this).data('offset');
- var $sidebar = $('#sidebar'), time = 250;
+ var $sidebar = $('#calendarsidebar'), time = 250;
if ($sidebar.is(':visible')) {
- $sidebar.animate({ left:'-'+(width+10)+'px' }, time, function(){ $('#sidebar').hide(); });
- $(this).animate({ left:'8px'}, time, function(){ $('#sidebartoggle').addClass('sidebarclosed') });
+ $sidebar.animate({ left:'-'+(width+10)+'px' }, time, function(){ $('#calendarsidebar').hide(); });
+ $(this).animate({ left:'8px'}, time, function(){ $('#calendarsidebartoggle').addClass('sidebarclosed') });
$('#calendar').animate({ left:'20px'}, time, function(){ $(this).fullCalendar('render'); });
}
else {
$sidebar.show().animate({ left:'10px' }, time);
- $(this).animate({ left:offset+'px'}, time, function(){ $('#sidebartoggle').removeClass('sidebarclosed'); });
+ $(this).animate({ left:offset+'px'}, time, function(){ $('#calendarsidebartoggle').removeClass('sidebarclosed'); });
$('#calendar').animate({ left:(width+16)+'px'}, time, function(){ $(this).fullCalendar('render'); });
}
})
- .data('offset', $('#sidebartoggle').position().left)
- .data('sidebarwidth', $('#sidebar').width() + $('#sidebar').position().left);
+ .data('offset', $('#calendarsidebartoggle').position().left)
+ .data('sidebarwidth', $('#calendarsidebar').width() + $('#calendarsidebar').position().left);
});
</script>
</body>
</html>
diff --git a/plugins/calendar/skins/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index ab9ea521..ad646df1 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -1,1259 +1,1332 @@
/**
* Roundcube Calendar plugin styles for skin "Larry"
*
* Copyright (c) 2012, The Roundcube Dev Team
* 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.
*
* $Id$
*/
body.calendarmain {
overflow: hidden;
}
body.calendarmain #mainscreen {
left: 0;
}
-#sidebar {
+#calendarsidebar {
position: absolute;
top: 0;
left: 10px;
bottom: 0;
width: 240px;
}
#datepicker {
margin-top: 12px;
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);
}
-.ie9 #datepicker .ui-datepicker-activerange a {
- filter: none;
- background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20preserveAspectRatio%3D%22none%22%20version%3D%221.0%22%20width%3D%22100%25%22%20height%3D%22100%25%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22mLG1%22%20x1%3D%220%25%22%20y1%3D%220%25%22%20x2%3D%220%25%22%20y2%3D%22100%25%22%20spreadMethod%3D%22pad%22%3E%3Cstop%20offset%3D%220%25%22%20stop-color%3D%22%23d9f1fb%22%20stop-opacity%3D%221%22/%3E%3Cstop%20offset%3D%22100%25%22%20stop-color%3D%22%23c5e3ee%22%20stop-opacity%3D%221%22/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20style%3D%22fill%3Aurl%28%23mLG1%29%3B%22/%3E%3C/svg%3E) !important;
-}
-
-.ie9 #datepicker .ui-datepicker-activerange a.ui-state-active {
- filter: none;
- background-image: url(data:image/svg+xml,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20preserveAspectRatio%3D%22none%22%20version%3D%221.0%22%20width%3D%22100%25%22%20height%3D%22100%25%22%3E%3Cdefs%3E%3ClinearGradient%20id%3D%22mLG1%22%20x1%3D%220%25%22%20y1%3D%220%25%22%20x2%3D%220%25%22%20y2%3D%22100%25%22%20spreadMethod%3D%22pad%22%3E%3Cstop%20offset%3D%220%25%22%20stop-color%3D%22%2300acd4%22%20stop-opacity%3D%221%22/%3E%3Cstop%20offset%3D%22100%25%22%20stop-color%3D%22%23008fc7%22%20stop-opacity%3D%221%22/%3E%3C/linearGradient%3E%3C/defs%3E%3Crect%20width%3D%22100%25%22%20height%3D%22100%25%22%20style%3D%22fill%3Aurl%28%23mLG1%29%3B%22/%3E%3C/svg%3E) !important;
-}
-
#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;
}
-#sidebartoggle {
+#calendarsidebartoggle {
position: absolute;
left: 254px;
width: 8px;
top: 37px;
bottom: 0;
background: url(images/toggle.gif) 0 48% no-repeat transparent;
cursor: pointer;
}
div.sidebarclosed {
background-position: -8px 48% !important;
}
-#sidebartoggle:hover {
+#calendarsidebartoggle:hover {
background-color: #ddd;
}
#calendar {
position: absolute;
top: 0;
left: 266px;
right: 0;
bottom: 0;
padding-bottom: 28px;
}
+.calendarmain #message.statusbar {
+ border: 1px solid #c3c3c3;
+ border-bottom-color: #ababab;
+}
+
#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;
white-space: nowrap;
}
#calendarslist li label {
display: block;
}
#calendarslist li span.calname {
cursor: default;
background: url(images/calendars.png) 0 -2px no-repeat;
padding-left: 22px;
padding-bottom: 2px;
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: 3px;
right: 5px;
}
#calendarslist li.selected {
background-color: #c7e3ef;
}
#calendarslist li.selected span.calname {
font-weight: bold;
}
#calendarslist li.readonly span.calname {
background-position: 0 -20px;
}
#calendarslist li.other span.calname {
background-position: 0 -38px;
}
#calendarslist li.other.readonly span.calname {
background-position: 0 -56px;
}
#calendarslist li.shared span.calname {
background-position: 0 -74px;
}
#calendarslist li.shared.readonly span.calname {
background-position: 0 -92px;
}
#calfeedurl {
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;
right: 0;
height: 40px;
z-index: 200;
}
#calendartoolbar a {
padding-right: 10px;
}
#quicksearchbar {
right: 4px;
}
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: 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;
}
#edit-attachments {
margin-top: 0.6em;
}
#edit-attachments ul li {
display: block;
color: #333;
font-weight: bold;
padding: 8px 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 {
padding-top: 1.2em;
}
#edit-attachments-form .buttons {
margin: 0.5em 0;
}
#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.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.organizer {
background-position: right -80px;
}
/* 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.8em;
}
#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;
}
#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;
+ border: 1px solid #ffdf0e;
+ background-color: #fef893;
}
#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-form {
position: relative;
margin-top: 1em;
}
#edit-attendees-form #edit-attendee-schedule {
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;
}
#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 .chair {
background-position: 2px -137px;
}
.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 {
border: 0;
color: #999;
font-size: 90%;
text-align: right;
padding-right: 6px;
}
/*
.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;
- top: 37;
+ bottom: 28px;
left: 0;
right: 0;
height: auto;
z-index: 200;
- border: 1px solid #ccc;
- padding: 2px 5px 1px;
- font-size: 90%;
+ background: #d6eaf3;
+ border: 1px solid #c3c3c3;
+ border-top-color: #ddd;
+ padding: 4px 5px;
}
#agendaoptions label {
- color: #444;
- text-shadow: 1px 1px #eee;
+ color: #69939e;
+ text-shadow: 1px 1px #f2f2f2;
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;
+ font-weight: bold;
+ white-space: nowrap;
+ color: #666;
+ padding-right: 10px;
}
+.propform fieldset.tab {
+ background: #efefef;
+ display: block;
+ margin-top: 0.5em;
+ padding: 0.5em 1em;
+}
/* fullcalendar style overrides */
.rcube-fc-content {
overflow: hidden;
border: 0;
border-radius: 4px 4px 0 0;
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;
}
.fc-content {
position: absolute !important;
- top: 37px;
+ top: 38px;
left: 0;
right: 0;
bottom: 28px;
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: #555;
+ text-shadow: 0px 1px 1px #fff;
+ border: 1px solid #a2a2a2;
+ background: #f7f7f7;
+ background: -moz-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f9f9f9), color-stop(100%,#e6e6e6));
+ background: -o-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
+ background: -ms-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
+ background: linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
+ box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3);
+ -o-box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3);
+ -webkit-box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3);
+ -moz-box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3);
+ text-decoration: none;
+}
+
+.calendarmain .fc-state-disabled .fc-button-inner {
+ color: #bbb;
+}
+
+.calendarmain .fc-header .fc-button {
+ margin-left: -1px;
+ margin-right: 0;
+}
+
+.calendarmain .fc-state-down .fc-button-inner {
+ margin: 0;
+ border: 1px solid #a2a2a2;
+ background: #e6e6e6;
+ background: -moz-linear-gradient(top, #e6e6e6 0%, #f9f9f9 100%);
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#e6e6e6), color-stop(100%,#f9f9f9));
+ background: -o-linear-gradient(top, #e6e6e6 0%, #f9f9f9 100%);
+ background: -ms-linear-gradient(top, #e6e6e6 0%, #f9f9f9 100%);
+ background: linear-gradient(top, #e6e6e6 0%, #f9f9f9 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-state-default .fc-button-effect {
+ display: none;
+}
+
+.calendarmain .fc-button-content {
+ height: 2.2em;
+ line-height: 2.2em;
+}
+
#calendar .fc-header-right {
- padding-right: 280px;
+ padding-right: 250px;
}
.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.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,
+
+/*.calendarmain .fc-view-list div.fc-list-header,*/
+.calendarmain .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);
+ color: #69939e;
+ font-size: 11px;
font-weight: bold;
- color: #333;
-}
-*/
-.fc-view-list .fc-event-skin .fc-event-content {
- background: #F6F6F6;
- padding: 2px;
+ 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;
+ padding: 7px;
}
-.fc-view-list .fc-event-skin .fc-event-title,
-.fc-view-list .fc-event-skin .fc-event-location {
- color: #333;
+.calendarmain .fc-view-table tr.fc-event td {
+ border-color: #ddd;
+ padding: 4px 7px;
}
-.fc-view-table col.fc-event-location {
+.calendarmain .fc-view-table col.fc-event-location {
width: 20%;
}
+.calendarmain .fc-view-table tr.fc-event td.fc-event-handle {
+ padding: 5px 10px 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);
+}
+
+
.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 {
/* background-position: 7px -109px; */
}
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 .rsvp-status,
div.calendar-invitebox .rsvp-buttons {
margin-top: 0.5em;
}
#event-rsvp input.button,
div.calendar-invitebox input.button {
font-weight: bold;
margin-right: 0.5em;
}
div.calendar-invitebox .calendar-select {
font-weight: 10px;
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.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;
}
/* 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;
+ background: url(images/invitation.png) 10px 10px no-repeat #fff;
}
.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/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html
index 9e3fbe61..c072e352 100644
--- a/plugins/calendar/skins/larry/templates/calendar.html
+++ b/plugins/calendar/skins/larry/templates/calendar.html
@@ -1,202 +1,202 @@
<roundcube:object name="doctype" value="html5" />
<html>
<head>
<title><roundcube:object name="pagetitle" /></title>
<roundcube:include file="/includes/links.html" />
-<!--[if lte IE 7]><link rel="stylesheet" type="text/css" href="./plugins/calendar/skins/default/iehacks.css" /><![endif]-->
+<!--[if lte IE 7]><link rel="stylesheet" type="text/css" href="./plugins/calendar/$__skin_path/iehacks.css" /><![endif]-->
</head>
<body class="calendarmain noscroll">
<roundcube:include file="/includes/header.html" />
<div id="mainscreen">
- <div id="sidebar">
+ <div id="calendarsidebar">
<div id="quicksearchbar">
<roundcube:object name="plugin.searchform" id="quicksearchbox" />
<a id="searchmenulink" class="iconbutton searchoptions" > </a>
<roundcube:button command="reset-search" id="searchreset" class="iconbutton reset" title="resetsearch" content=" " />
</div>
<div id="datepicker" class="uibox"></div>
<div id="calendars" class="uibox listbox" style="visibility:hidden">
<h2 class="boxtitle"><roundcube:label name="calendar.calendars" /></h2>
<div class="scroller withfooter">
<roundcube:object name="plugin.calendar_list" id="calendarslist" class="listing" />
</div>
<div class="boxfooter">
<roundcube:button command="calendar-create" type="link" title="calendar.createcalendar" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" content="+" /><roundcube:button name="calendaroptionslink" id="calendaroptionsmenulink" type="link" title="calendar.calendaractions" class="listbutton groupactions" onclick="UI.show_popup('calendaroptionsmenu', undefined, { above:true });return false" innerClass="inner" content="&#9881;" />
</div>
</div>
</div>
- <div id="sidebartoggle"></div>
+ <div id="calendarsidebartoggle"></div>
<div id="calendartoolbar" class="toolbar">
<roundcube:button command="addevent" type="link" class="button addevent disabled" classAct="button addevent" classSel="button addevent pressed" label="calendar.new_event" title="calendar.new_event" />
<roundcube:button command="print" type="link" class="button print disabled" classAct="button print" classSel="button print pressed" label="calendar.print" title="calendar.printtitle" />
<roundcube:button command="export" type="link" class="button export disabled" classAct="button export" classSel="button export pressed" label="calendar.export" title="calendar.exporttitle" />
<roundcube:container name="toolbar" id="calendartoolbar" />
</div>
<div id="calendar">
<roundcube:object name="plugin.angenda_options" class="boxfooter" id="agendaoptions" />
<roundcube:object name="message" id="message" class="statusbar" />
</div>
</div>
<div id="calendaroptionsmenu" class="popupmenu">
<ul class="toolbarmenu">
<li><roundcube:button command="calendar-edit" label="calendar.edit" classAct="active" /></li>
<li><roundcube:button command="calendar-remove" label="calendar.remove" classAct="active" /></li>
<li><roundcube:button command="events-import" label="calendar.importevents" classAct="active" /></li>
<li><roundcube:button command="calendar-showurl" label="calendar.showurl" classAct="active" /></li>
<roundcube:if condition="env:calendar_driver == 'kolab'" />
<li class="separator_above"><roundcube:button command="folders" task="settings" type="link" label="managefolders" classAct="active" /></li>
<roundcube:endif />
</ul>
</div>
<div id="eventshow" class="uidialog">
<h1 id="event-title">Event Title</h1>
<div class="event-section" id="event-location">Location</div>
<div class="event-section" id="event-date">From-To</div>
<div class="event-section" id="event-description">
<h5 class="label"><roundcube:label name="calendar.description" /></h5>
<div class="event-text"></div>
</div>
<div class="event-section" id="event-repeat">
<h5 class="label"><roundcube:label name="calendar.repeat" /></h5>
<div class="event-text"></div>
</div>
<div class="event-section" id="event-alarm">
<h5 class="label"><roundcube:label name="calendar.alarms" /></h5>
<div class="event-text"></div>
</div>
<div class="event-section" id="event-attendees">
<h5 class="label"><roundcube:label name="calendar.tabattendees" /></h5>
<div class="event-text"></div>
</div>
<div class="event-line" id="event-calendar">
<label><roundcube:label name="calendar.calendar" /></label>
<span class="event-text">Default</span>
</div>
<div class="event-line" id="event-category">
<label><roundcube:label name="calendar.category" /></label>
<span class="event-text"></span>
</div>
<div class="event-line" id="event-free-busy">
<label><roundcube:label name="calendar.freebusy" /></label>
<span class="event-text"></span>
</div>
<div class="event-line" id="event-priority">
<label><roundcube:label name="calendar.priority" /></label>
<span class="event-text"></span>
</div>
<div class="event-line" id="event-sensitivity">
<label><roundcube:label name="calendar.sensitivity" /></label>
<span class="event-text"></span>
</div>
<div class="event-section" id="event-attachments">
<label><roundcube:label name="attachments" /></label>
<div class="event-text"></div>
</div>
<roundcube:object name="plugin.event_rsvp_buttons" id="event-rsvp" style="display:none" />
</div>
<roundcube:include file="/templates/eventedit.html" />
<div id="eventfreebusy" class="uidialog">
<roundcube:object name="plugin.attendees_freebusy_table" id="attendees-freebusy-table" cellspacing="0" cellpadding="0" border="0" />
<div class="schedule-options">
&nbsp;
<div class="schedule-buttons">
<button id="shedule-freebusy-prev" title="<roundcube:label name='previouspage' />">&#9668;</button><button id="shedule-freebusy-next" title="<roundcube:label name='nextpage' />">&#9658;</button>
</div>
</div>
<div style="float:left; width:28em">
<div class="form-section">
<label for="schedule-startdate"><roundcube:label name="calendar.start" /></label>
<input type="text" name="startdate" size="10" id="schedule-startdate" disabled="true" /> &nbsp;
<input type="text" name="starttime" size="6" id="schedule-starttime" disabled="true" />
</div>
<div class="form-section">
<label for="schedule-enddate"><roundcube:label name="calendar.end" /></label>
<input type="text" name="enddate" size="10" id="schedule-enddate" disabled="true" /> &nbsp;
<input type="text" name="endtime" size="6" id="schedule-endtime" disabled="true" />
</div>
</div>
<div style="float:left">
<div class="schedule-find-buttons">
<button id="shedule-find-prev">&#9668; <roundcube:label name="calendar.prevslot" /></button>
<button id="shedule-find-next"><roundcube:label name="calendar.nextslot" /> &#9658;</button>
</div>
<div class="schedule-options">
<label><input type="checkbox" id="schedule-freebusy-workinghours" value="1" /><roundcube:label name="calendar.onlyworkinghours" /></label>
</div>
</div>
<br style="clear:both;" />
<roundcube:include file="/templates/freebusylegend.html" />
<div class="attendees-list">
<span class="attendee organizer"><roundcube:label name="calendar.roleorganizer" /></span>
<span class="attendee req-participant"><roundcube:label name="calendar.rolerequired" /></span>
<span class="attendee opt-participant"><roundcube:label name="calendar.roleoptional" /></span>
<span class="attendee chair"><roundcube:label name="calendar.roleresource" /></span>
</div>
</div>
<div id="calendarform" class="uidialog">
<roundcube:label name="loading" />
</div>
<div id="eventsimport" class="uidialog">
<roundcube:object name="plugin.events_import_form" id="events-import-form" uploadFieldSize="30" />
</div>
<div id="calendarurlbox" class="uidialog">
<p><roundcube:label name="calendar.showurldescription" /></p>
<textarea id="calfeedurl" rows="2" readonly="readonly"></textarea>
</div>
<div id="alarm-snooze-dropdown" class="popupmenu">
<roundcube:object name="plugin.snooze_select" type="ul" />
</div>
<roundcube:object name="plugin.calendar_css" />
<script type="text/javascript">
// UI startup
var UI = new rcube_mail_ui();
$(document).ready(function(e){
UI.init();
// initialize sidebar toggle
- $('#sidebartoggle').click(function() {
+ $('#calendarsidebartoggle').click(function() {
var width = $(this).data('sidebarwidth');
var offset = $(this).data('offset');
- var $sidebar = $('#sidebar'), time = 250;
+ var $sidebar = $('#calendarsidebar'), time = 250;
if ($sidebar.is(':visible')) {
- $sidebar.animate({ left:'-'+(width+10)+'px' }, time, function(){ $('#sidebar').hide(); });
- $(this).animate({ left:'8px'}, time, function(){ $('#sidebartoggle').addClass('sidebarclosed') });
+ $sidebar.animate({ left:'-'+(width+10)+'px' }, time, function(){ $('#calendarsidebar').hide(); });
+ $(this).animate({ left:'8px'}, time, function(){ $('#calendarsidebartoggle').addClass('sidebarclosed') });
$('#calendar').animate({ left:'20px'}, time, function(){ $(this).fullCalendar('render'); });
}
else {
$sidebar.show().animate({ left:'10px' }, time);
- $(this).animate({ left:offset+'px'}, time, function(){ $('#sidebartoggle').removeClass('sidebarclosed'); });
+ $(this).animate({ left:offset+'px'}, time, function(){ $('#calendarsidebartoggle').removeClass('sidebarclosed'); });
$('#calendar').animate({ left:(width+16)+'px'}, time, function(){ $(this).fullCalendar('render'); });
}
})
- .data('offset', $('#sidebartoggle').position().left)
- .data('sidebarwidth', $('#sidebar').width() + $('#sidebar').position().left);
+ .data('offset', $('#calendarsidebartoggle').position().left)
+ .data('sidebarwidth', $('#calendarsidebar').width() + $('#calendarsidebar').position().left);
});
</script>
</body>
</html>
diff --git a/plugins/calendar/skins/larry/templates/itipattend.html b/plugins/calendar/skins/larry/templates/itipattend.html
index e1c7cc7e..bfbb08f5 100644
--- a/plugins/calendar/skins/larry/templates/itipattend.html
+++ b/plugins/calendar/skins/larry/templates/itipattend.html
@@ -1,36 +1,36 @@
<roundcube:object name="doctype" value="html5" />
<html>
<head>
<title><roundcube:object name="pagetitle" /></title>
<roundcube:include file="/includes/links.html" />
</head>
<body class="extwin calendaritipattend">
<div id="header">
<div id="topline">
<div class="topright">
<a href="#close" class="closelink" onclick="self.close()"><roundcube:label name="close" /></a>
</div>
</div>
<div id="topnav">
<roundcube:object name="logo" src="/images/roundcube_logo.png" id="toplogo" border="0" alt="Logo" />
</div>
<br style="clear:both" />
</div>
<div id="mainscreen">
-<div class="centerbox">
+<div class="centerbox uibox">
<roundcube:object name="plugin.event_inviteform" />
<roundcube:object name="plugin.event_invitebox" class="calendar-invitebox" />
<roundcube:object name="plugin.event_rsvp_buttons" type="submit" iname="rsvp" id="event-rsvp" />
+ <roundcube:object name="message" id="message" />
</form>
</div>
-<roundcube:object name="message" id="message" />
</div>
</body>
</html>
\ No newline at end of file
diff --git a/plugins/calendar/skins/larry/templates/kolabacl.html b/plugins/calendar/skins/larry/templates/kolabacl.html
index f32a92fc..05a279ad 100644
--- a/plugins/calendar/skins/larry/templates/kolabacl.html
+++ b/plugins/calendar/skins/larry/templates/kolabacl.html
@@ -1,12 +1,24 @@
<roundcube:object name="doctype" value="html5" />
<html>
<head>
<title><roundcube:object name="pagetitle" /></title>
<roundcube:include file="/includes/links.html" />
+<style type="text/css" media="screen">
+
+body.aclform {
+ background: #efefef;
+ margin: 0;
+}
+
+body.aclform .hint {
+ margin: 1em;
+}
+
+</style>
</head>
-<body class="iframe" style="background:#fff; margin:0">
+<body class="iframe aclform">
<roundcube:object name="folderacl" />
</body>
</html>
diff --git a/plugins/calendar/skins/larry/templates/kolabform.html b/plugins/calendar/skins/larry/templates/kolabform.html
index 03b023fc..77a1c30e 100644
--- a/plugins/calendar/skins/larry/templates/kolabform.html
+++ b/plugins/calendar/skins/larry/templates/kolabform.html
@@ -1,6 +1,9 @@
-<div id="calendar-kolabform">
+<div id="calendar-kolabform" class="propform tabbed">
<roundcube:object name="calendarform" />
</div>
<style type="text/css">
-#calendarpropform { min-width:680px }
-</style>
\ No newline at end of file
+#calendarpropform { min-width:680px; margin-top:-12px; }
+</style>
+<script type="text/javascript">
+UI.init_tabs('#calendar-kolabform');
+</script>
\ No newline at end of file
diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
index 2ffc7407..531eec70 100644
--- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
+++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
@@ -1,1225 +1,1224 @@
<?php
/**
* Backend class for a custom address book
*
* This part of the Roundcube+Kolab integration and connects the
* rcube_addressbook interface with the rcube_kolab wrapper for Kolab_Storage
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2011, 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/>.
*
* @see rcube_addressbook
*/
class rcube_kolab_contacts extends rcube_addressbook
{
public $primary_key = 'ID';
public $readonly = true;
public $editable = false;
public $undelete = true;
public $groups = true;
public $coltypes = array(
'name' => array('limit' => 1),
'firstname' => array('limit' => 1),
'surname' => array('limit' => 1),
'middlename' => array('limit' => 1),
'prefix' => array('limit' => 1),
'suffix' => array('limit' => 1),
'nickname' => array('limit' => 1),
'jobtitle' => array('limit' => 1),
'organization' => array('limit' => 1),
'department' => array('limit' => 1),
'email' => array('subtypes' => null),
'phone' => array(),
'address' => array('limit' => 2, 'subtypes' => array('home','business')),
'officelocation' => array('type' => 'text', 'size' => 40, 'maxlength' => 50, 'limit' => 1,
'label' => 'kolab_addressbook.officelocation', 'category' => 'main'),
'website' => array('limit' => 1, 'subtypes' => null),
'im' => array('limit' => 1, 'subtypes' => null),
'gender' => array('limit' => 1),
'initials' => array('type' => 'text', 'size' => 6, 'maxlength' => 10, 'limit' => 1,
'label' => 'kolab_addressbook.initials', 'category' => 'personal'),
'birthday' => array('limit' => 1),
'anniversary' => array('limit' => 1),
'profession' => array('type' => 'text', 'size' => 40, 'maxlength' => 80, 'limit' => 1,
'label' => 'kolab_addressbook.profession', 'category' => 'personal'),
'manager' => array('limit' => 1),
'assistant' => array('limit' => 1),
'spouse' => array('limit' => 1),
'children' => array('type' => 'text', 'size' => 40, 'maxlength' => 80, 'limit' => 1,
'label' => 'kolab_addressbook.children', 'category' => 'personal'),
'pgppublickey' => array('type' => 'text', 'size' => 40, 'limit' => 1,
'label' => 'kolab_addressbook.pgppublickey'),
'freebusyurl' => array('type' => 'text', 'size' => 40, 'limit' => 1,
'label' => 'kolab_addressbook.freebusyurl'),
'notes' => array(),
'photo' => array(),
// TODO: define more Kolab-specific fields such as: language, latitude, longitude
);
/**
* vCard additional fields mapping
*/
public $vcard_map = array(
'profession' => 'X-PROFESSION',
'officelocation' => 'X-OFFICE-LOCATION',
'initials' => 'X-INITIALS',
'children' => 'X-CHILDREN',
'freebusyurl' => 'X-FREEBUSY-URL',
'pgppublickey' => 'KEY',
);
private $gid;
private $storagefolder;
private $contactstorage;
private $liststorage;
private $contacts;
private $distlists;
private $groupmembers;
private $id2uid;
private $filter;
private $result;
private $namespace;
private $imap_folder = 'INBOX/Contacts';
private $gender_map = array(0 => 'male', 1 => 'female');
private $phonetypemap = array('home' => 'home1', 'work' => 'business1', 'work2' => 'business2', 'workfax' => 'businessfax');
private $addresstypemap = array('work' => 'business');
private $fieldmap = array(
// kolab => roundcube
'full-name' => 'name',
'given-name' => 'firstname',
'middle-names' => 'middlename',
'last-name' => 'surname',
'prefix' => 'prefix',
'suffix' => 'suffix',
'nick-name' => 'nickname',
'organization' => 'organization',
'department' => 'department',
'job-title' => 'jobtitle',
'initials' => 'initials',
'birthday' => 'birthday',
'anniversary' => 'anniversary',
'im-address' => 'im',
'web-page' => 'website',
'office-location' => 'officelocation',
'profession' => 'profession',
'manager-name' => 'manager',
'assistant' => 'assistant',
'spouse-name' => 'spouse',
'children' => 'children',
'body' => 'notes',
'pgp-publickey' => 'pgppublickey',
'free-busy-url' => 'freebusyurl',
'gender' => 'gender',
);
public function __construct($imap_folder = null)
{
if ($imap_folder) {
$this->imap_folder = $imap_folder;
}
// extend coltypes configuration
$format = rcube_kolab::get_format('contact');
$this->coltypes['phone']['subtypes'] = $format->_phone_types;
$this->coltypes['address']['subtypes'] = $format->_address_types;
// set localized labels for proprietary cols
foreach ($this->coltypes as $col => $prop) {
if (is_string($prop['label']))
$this->coltypes[$col]['label'] = rcube_label($prop['label']);
}
// fetch objects from the given IMAP folder
$this->storagefolder = rcube_kolab::get_folder($this->imap_folder);
$this->ready = !PEAR::isError($this->storagefolder);
// Set readonly and editable flags according to folder permissions
if ($this->ready) {
if ($this->get_owner() == $_SESSION['username']) {
$this->editable = true;
$this->readonly = false;
}
else {
$rights = $this->storagefolder->getMyRights();
if (!PEAR::isError($rights)) {
if (strpos($rights, 'i') !== false)
$this->readonly = false;
if (strpos($rights, 'a') !== false || strpos($rights, 'x') !== false)
$this->editable = true;
}
}
}
}
/**
* Getter for the address book name to be displayed
*
* @return string Name of this address book
*/
public function get_name()
{
$folder = rcube_kolab::object_name($this->imap_folder, $this->namespace);
return $folder;
}
/**
* Getter for the IMAP folder name
*
* @return string Name of the IMAP folder
*/
public function get_realname()
{
return $this->imap_folder;
}
/**
* Getter for the IMAP folder owner
*
* @return string Name of the folder owner
*/
public function get_owner()
{
return $this->storagefolder->getOwner();
}
/**
* Getter for the name of the namespace to which the IMAP folder belongs
*
* @return string Name of the namespace (personal, other, shared)
*/
public function get_namespace()
{
if ($this->namespace === null) {
$this->namespace = rcube_kolab::folder_namespace($this->imap_folder);
}
return $this->namespace;
}
/**
* Setter for the current group
*/
public function set_group($gid)
{
$this->gid = $gid;
}
/**
* Save a search string for future listings
*
* @param mixed Search params to use in listing method, obtained by get_search_set()
*/
public function set_search_set($filter)
{
$this->filter = $filter;
}
/**
* Getter for saved search properties
*
* @return mixed Search properties used by this class
*/
public function get_search_set()
{
return $this->filter;
}
/**
* Reset saved results and search parameters
*/
public function reset()
{
$this->result = null;
$this->filter = null;
}
/**
* List all active contact groups of this source
*
* @param string Optional search string to match group name
* @return array Indexed list of contact groups, each a hash array
*/
function list_groups($search = null)
{
$this->_fetch_groups();
$groups = array();
foreach ((array)$this->distlists as $group) {
if (!$search || strstr(strtolower($group['last-name']), strtolower($search)))
$groups[$group['last-name']] = array('ID' => $group['ID'], 'name' => $group['last-name']);
}
// sort groups
ksort($groups, SORT_LOCALE_STRING);
return array_values($groups);
}
/**
* List the current set of contact records
*
* @param array List of cols to show
* @param int Only return this number of records, use negative values for tail
* @return array Indexed list of contact records, each a hash array
*/
public function list_records($cols=null, $subset=0)
{
$this->result = $this->count();
// list member of the selected group
if ($this->gid) {
$seen = array();
$this->result->count = 0;
foreach ((array)$this->distlists[$this->gid]['member'] as $member) {
// skip member that don't match the search filter
if (is_array($this->filter['ids']) && array_search($member['ID'], $this->filter['ids']) === false)
continue;
if ($this->contacts[$member['ID']] && !$seen[$member['ID']]++)
$this->result->count++;
}
$ids = array_keys($seen);
}
else
$ids = is_array($this->filter['ids']) ? $this->filter['ids'] : array_keys($this->contacts);
// sort data arrays according to desired list sorting
if ($count = count($ids)) {
uasort($this->contacts, array($this, '_sort_contacts_comp'));
// get sorted IDs
if ($count != count($this->contacts))
$ids = array_values(array_intersect(array_keys($this->contacts), $ids));
else
$ids = array_keys($this->contacts);
}
// fill contact data into the current result set
$start_row = $subset < 0 ? $this->result->first + $this->page_size + $subset : $this->result->first;
$last_row = min($subset != 0 ? $start_row + abs($subset) : $this->result->first + $this->page_size, $count);
for ($i = $start_row; $i < $last_row; $i++) {
if ($id = $ids[$i])
$this->result->add($this->contacts[$id]);
}
return $this->result;
}
/**
* Search records
*
* @param mixed $fields The field name of array of field names to search in
* @param mixed $value Search value (or array of values when $fields is array)
* @param int $mode Matching mode:
* 0 - partial (*abc*),
* 1 - strict (=),
* 2 - prefix (abc*)
* @param boolean $select True if results are requested, False if count only
* @param boolean $nocount True to skip the count query (select only)
* @param array $required List of fields that cannot be empty
*
* @return object rcube_result_set List of contact records and 'count' value
*/
public function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array())
{
$this->_fetch_contacts();
// search by ID
if ($fields == $this->primary_key) {
$ids = !is_array($value) ? explode(',', $value) : $value;
$result = new rcube_result_set();
foreach ($ids as $id) {
if ($rec = $this->get_record($id, true)) {
$result->add($rec);
$result->count++;
}
}
return $result;
}
else if ($fields == '*') {
$fields = array_keys($this->coltypes);
}
if (!is_array($fields))
$fields = array($fields);
if (!is_array($required) && !empty($required))
$required = array($required);
// advanced search
if (is_array($value)) {
$advanced = true;
$value = array_map('mb_strtolower', $value);
}
else
$value = mb_strtolower($value);
$scount = count($fields);
// build key name regexp
$regexp = '/^(' . implode($fields, '|') . ')(?:.*)$/';
// save searching conditions
$this->filter = array('fields' => $fields, 'value' => $value, 'mode' => $mode, 'ids' => array());
// search be iterating over all records in memory
foreach ($this->contacts as $id => $contact) {
// check if current contact has required values, otherwise skip it
if ($required) {
foreach ($required as $f)
if (empty($contact[$f]))
continue 2;
}
$found = array();
foreach (preg_grep($regexp, array_keys($contact)) as $col) {
if ($advanced) {
$pos = strpos($col, ':');
$colname = $pos ? substr($col, 0, $pos) : $col;
$search = $value[array_search($colname, $fields)];
}
else {
$search = $value;
}
foreach ((array)$contact[$col] as $val) {
$val = mb_strtolower($val);
switch ($mode) {
case 1:
$got = ($val == $search);
break;
case 2:
$got = ($search == substr($val, 0, strlen($search)));
break;
default:
$got = (strpos($val, $search) !== false);
break;
}
if ($got) {
if (!$advanced) {
$this->filter['ids'][] = $id;
break 2;
}
else {
$found[$colname] = true;
}
}
}
}
if (count($found) >= $scount) // && $advanced
$this->filter['ids'][] = $id;
}
// list records (now limited by $this->filter)
return $this->list_records();
}
/**
* Refresh saved search results after data has changed
*/
public function refresh_search()
{
if ($this->filter)
$this->search($this->filter['fields'], $this->filter['value'], $this->filter['mode']);
return $this->get_search_set();
}
/**
* Count number of available contacts in database
*
* @return rcube_result_set Result set with values for 'count' and 'first'
*/
public function count()
{
$this->_fetch_contacts();
$this->_fetch_groups();
$count = $this->gid ? count($this->distlists[$this->gid]['member']) : (is_array($this->filter['ids']) ? count($this->filter['ids']) : count($this->contacts));
return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
}
/**
* Return the last result set
*
* @return rcube_result_set Current result set or NULL if nothing selected yet
*/
public function get_result()
{
return $this->result;
}
/**
* Get a specific contact record
*
* @param mixed record identifier(s)
* @param boolean True to return record as associative array, otherwise a result set is returned
* @return mixed Result object with all record fields or False if not found
*/
public function get_record($id, $assoc=false)
{
$this->_fetch_contacts();
if ($this->contacts[$id]) {
$this->result = new rcube_result_set(1);
$this->result->add($this->contacts[$id]);
return $assoc ? $this->contacts[$id] : $this->result;
}
return false;
}
/**
* Get group assignments of a specific contact record
*
* @param mixed Record identifier
* @return array List of assigned groups as ID=>Name pairs
*/
public function get_record_groups($id)
{
$out = array();
$this->_fetch_groups();
foreach ((array)$this->groupmembers[$id] as $gid) {
if ($group = $this->distlists[$gid])
$out[$gid] = $group['last-name'];
}
return $out;
}
/**
* Create a new contact record
*
* @param array Assoziative array with save data
* Keys: Field name with optional section in the form FIELD:SECTION
* Values: Field value. Can be either a string or an array of strings for multiple values
* @param boolean True to check for duplicates first
* @return mixed The created record ID on success, False on error
*/
public function insert($save_data, $check=false)
{
if (!is_array($save_data))
return false;
$insert_id = $existing = false;
// check for existing records by e-mail comparison
if ($check) {
foreach ($this->get_col_values('email', $save_data, true) as $email) {
if (($res = $this->search('email', $email, true, false)) && $res->count) {
$existing = true;
break;
}
}
}
if (!$existing) {
$this->_connect();
// generate new Kolab contact item
$object = $this->_from_rcube_contact($save_data);
$object['uid'] = $this->contactstorage->generateUID();
$saved = $this->contactstorage->save($object);
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving contact object to Kolab server:" . $saved->getMessage()),
true, false);
}
else {
$contact = $this->_to_rcube_contact($object);
$id = $contact['ID'];
$this->contacts[$id] = $contact;
$this->id2uid[$id] = $object['uid'];
$insert_id = $id;
}
}
return $insert_id;
}
/**
* Update a specific contact record
*
* @param mixed Record identifier
* @param array Assoziative array with save data
* Keys: Field name with optional section in the form FIELD:SECTION
* Values: Field value. Can be either a string or an array of strings for multiple values
* @return boolean True on success, False on error
*/
public function update($id, $save_data)
{
$updated = false;
$this->_fetch_contacts();
if ($this->contacts[$id] && ($uid = $this->id2uid[$id])) {
$old = $this->contactstorage->getObject($uid);
$object = array_merge($old, $this->_from_rcube_contact($save_data));
$saved = $this->contactstorage->save($object, $uid);
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving contact object to Kolab server:" . $saved->getMessage()),
true, false);
}
else {
$this->contacts[$id] = $this->_to_rcube_contact($object);
$updated = true;
}
}
return $updated;
}
/**
* Mark one or more contact records as deleted
*
* @param array Record identifiers
* @param boolean Remove record(s) irreversible (mark as deleted otherwise)
*
* @return int Number of records deleted
*/
public function delete($ids, $force=true)
{
$this->_fetch_contacts();
$this->_fetch_groups();
if (!is_array($ids))
$ids = explode(',', $ids);
$count = 0;
$imap_uids = array();
foreach ($ids as $id) {
if ($uid = $this->id2uid[$id]) {
$imap_uid = $this->contactstorage->_getStorageId($uid);
$deleted = $this->contactstorage->delete($uid, $force);
if (PEAR::isError($deleted)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error deleting a contact object from the Kolab server:" . $deleted->getMessage()),
true, false);
}
else {
// remove from distribution lists
foreach ((array)$this->groupmembers[$id] as $gid)
$this->remove_from_group($gid, $id);
$imap_uids[$id] = $imap_uid;
// clear internal cache
unset($this->contacts[$id], $this->id2uid[$id], $this->groupmembers[$id]);
$count++;
}
}
}
// store IMAP uids for undelete()
if (!$force) {
$_SESSION['kolab_delete_uids'] = $imap_uids;
}
return $count;
}
/**
* Undelete one or more contact records.
* Only possible just after delete (see 2nd argument of delete() method).
*
* @param array Record identifiers
*
* @return int Number of records restored
*/
public function undelete($ids)
{
if (!is_array($ids))
$ids = explode(',', $ids);
$count = 0;
$uids = array();
$imap_uids = $_SESSION['kolab_delete_uids'];
// convert contact IDs into IMAP UIDs
foreach ($ids as $id)
if ($uid = $imap_uids[$id])
$uids[] = $uid;
if (!empty($uids)) {
$session = &Horde_Kolab_Session::singleton();
$imap = &$session->getImap();
if (is_object($imap) && is_a($imap, 'PEAR_Error')) {
$error = $imap;
}
else {
$result = $imap->select($this->imap_folder);
if (is_object($result) && is_a($result, 'PEAR_Error')) {
$error = $result;
}
else {
$result = $imap->undeleteMessages(implode(',', $uids));
if (is_object($result) && is_a($result, 'PEAR_Error')) {
$error = $result;
}
else {
$this->_connect();
$this->contactstorage->synchronize();
}
}
}
if ($error) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error undeleting a contact object(s) from the Kolab server:" . $error->getMessage()),
true, false);
}
$rcmail = rcmail::get_instance();
$rcmail->session->remove('kolab_delete_uids');
}
return count($uids);
}
/**
* Remove all records from the database
*/
public function delete_all()
{
$this->_connect();
if (!PEAR::isError($this->contactstorage->deleteAll())) {
$this->contacts = array();
$this->id2uid = array();
$this->result = null;
}
}
/**
* Close connection to source
* Called on script shutdown
*/
public function close()
{
}
/**
* Create a contact group with the given name
*
* @param string The group name
* @return mixed False on error, array with record props in success
*/
function create_group($name)
{
$this->_fetch_groups();
$result = false;
$list = array(
'uid' => $this->liststorage->generateUID(),
'last-name' => $name,
'member' => array(),
);
$saved = $this->liststorage->save($list);
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list object to Kolab server:" . $saved->getMessage()),
true, false);
return false;
}
else {
$id = md5($list['uid']);
$this->distlists[$id] = $list;
$result = array('id' => $id, 'name' => $name);
}
return $result;
}
/**
* Delete the given group and all linked group members
*
* @param string Group identifier
* @return boolean True on success, false if no data was changed
*/
function delete_group($gid)
{
$this->_fetch_groups();
$result = false;
if ($list = $this->distlists[$gid])
$deleted = $this->liststorage->delete($list['uid']);
if (PEAR::isError($deleted)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error deleting distribution-list object from the Kolab server:" . $deleted->getMessage()),
true, false);
}
else
$result = true;
return $result;
}
/**
* Rename a specific contact group
*
* @param string Group identifier
* @param string New name to set for this group
* @return boolean New name on success, false if no data was changed
*/
function rename_group($gid, $newname)
{
$this->_fetch_groups();
$list = $this->distlists[$gid];
if ($newname != $list['last-name']) {
$list['last-name'] = $newname;
$saved = $this->liststorage->save($list, $list['uid']);
}
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list object to Kolab server:" . $saved->getMessage()),
true, false);
return false;
}
return $newname;
}
/**
* Add the given contact records the a certain group
*
* @param string Group identifier
* @param array List of contact identifiers to be added
* @return int Number of contacts added
*/
function add_to_group($gid, $ids)
{
if (!is_array($ids))
$ids = explode(',', $ids);
$added = 0;
$exists = array();
$this->_fetch_groups();
$this->_fetch_contacts();
$list = $this->distlists[$gid];
foreach ((array)$list['member'] as $i => $member)
$exists[] = $member['ID'];
// substract existing assignments from list
$ids = array_diff($ids, $exists);
foreach ($ids as $contact_id) {
if ($uid = $this->id2uid[$contact_id]) {
$contact = $this->contacts[$contact_id];
foreach ($this->get_col_values('email', $contact, true) as $email) {
$list['member'][] = array(
'uid' => $uid,
'display-name' => $contact['name'],
'smtp-address' => $email,
);
}
$this->groupmembers[$contact_id][] = $gid;
$added++;
}
}
if ($added)
$saved = $this->liststorage->save($list, $list['uid']);
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list to Kolab server:" . $saved->getMessage()),
true, false);
$added = false;
}
else {
$this->distlists[$gid] = $list;
}
return $added;
}
/**
* Remove the given contact records from a certain group
*
* @param string Group identifier
* @param array List of contact identifiers to be removed
* @return int Number of deleted group members
*/
function remove_from_group($gid, $ids)
{
if (!is_array($ids))
$ids = explode(',', $ids);
$this->_fetch_groups();
if (!($list = $this->distlists[$gid]))
return false;
$new_member = array();
foreach ((array)$list['member'] as $member) {
if (!in_array($member['ID'], $ids))
$new_member[] = $member;
}
// write distribution list back to server
$list['member'] = $new_member;
$saved = $this->liststorage->save($list, $list['uid']);
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list object to Kolab server:" . $saved->getMessage()),
true, false);
}
else {
// remove group assigments in local cache
foreach ($ids as $id) {
$j = array_search($gid, $this->groupmembers[$id]);
unset($this->groupmembers[$id][$j]);
}
$this->distlists[$gid] = $list;
return true;
}
return false;
}
/**
* Check the given data before saving.
* If input not valid, the message to display can be fetched using get_error()
*
* @param array Associative array with contact data to save
*
* @return boolean True if input is valid, False if not.
*/
public function validate($save_data)
{
// validate e-mail addresses
$valid = parent::validate($save_data);
// require at least one e-mail address (syntax check is already done)
if ($valid) {
if (!strlen($save_data['name'])
&& !array_filter($this->get_col_values('email', $save_data, true))
) {
$this->set_error('warning', 'kolab_addressbook.noemailnamewarning');
$valid = false;
}
}
return $valid;
}
/**
* Establishes a connection to the Kolab_Data object for accessing contact data
*/
private function _connect()
{
if (!isset($this->contactstorage)) {
$this->contactstorage = $this->storagefolder->getData(null);
}
}
/**
* Establishes a connection to the Kolab_Data object for accessing groups data
*/
private function _connect_groups()
{
if (!isset($this->liststorage)) {
$this->liststorage = $this->storagefolder->getData('distributionlist');
}
}
/**
* Simply fetch all records and store them in private member vars
*/
private function _fetch_contacts()
{
if (!isset($this->contacts)) {
$this->_connect();
// read contacts
$this->contacts = $this->id2uid = array();
foreach ((array)$this->contactstorage->getObjects() as $record) {
// Because of a bug, sometimes group records are returned
if ($record['__type'] == 'Group')
continue;
$contact = $this->_to_rcube_contact($record);
$id = $contact['ID'];
$this->contacts[$id] = $contact;
$this->id2uid[$id] = $record['uid'];
}
}
}
/**
* Callback function for sorting contacts
*/
private function _sort_contacts_comp($a, $b)
{
- $a_name = $a['name'];
- $b_name = $b['name'];
-
- if (!$a_name) {
- $a_name = join(' ', array_filter(array($a['prefix'], $a['firstname'],
- $a['middlename'], $a['surname'], $a['suffix'])));
- if (!$a_name) {
- $a_name = is_array($a['email']) ? $a['email'][0] : $a['email'];
- }
+ $a_value = $a[$this->sort_col];
+ $b_value = $b[$this->sort_col];
+
+ if (!$a_value && $this->sort_col == 'name') {
+ $a_value = join(' ', array_filter(array($a['firstname'], $a['middlename'], $a['surname'])));
}
- if (!$b_name) {
- $b_name = join(' ', array_filter(array($b['prefix'], $b['firstname'],
- $b['middlename'], $b['surname'], $b['suffix'])));
- if (!$b_name) {
- $b_name = is_array($b['email']) ? $b['email'][0] : $b['email'];
- }
+ if (!$a_value) {
+ $a_value = is_array($a['email']) ? $a['email'][0] : $a['email'];
+ }
+
+ if (!$b_value && $this->sort_col == 'name') {
+ $b_value = join(' ', array_filter(array($b['firstname'], $b['middlename'], $b['surname'])));
+ }
+ if (!$b_value) {
+ $b_value = is_array($b['email']) ? $b['email'][0] : $b['email'];
}
- // return strcasecmp($a_name, $b_name);
+ // return strcasecmp($a_value, $b_value);
// make sorting unicode-safe and locale-dependent
- if ($a_name == $b_name)
+ if ($a_value == $b_value)
return 0;
- $arr = array($a_name, $b_name);
+ $arr = array($a_value, $b_value);
sort($arr, SORT_LOCALE_STRING);
- return $a_name == $arr[0] ? -1 : 1;
+ return $a_value == $arr[0] ? -1 : 1;
}
/**
* Read distribution-lists AKA groups from server
*/
private function _fetch_groups()
{
if (!isset($this->distlists)) {
$this->_connect_groups();
$this->distlists = $this->groupmembers = array();
foreach ((array)$this->liststorage->getObjects() as $record) {
// FIXME: folders without any distribution-list objects return contacts instead ?!
if ($record['__type'] != 'Group')
continue;
$record['ID'] = md5($record['uid']);
foreach ((array)$record['member'] as $i => $member) {
$mid = md5($member['uid']);
$record['member'][$i]['ID'] = $mid;
$this->groupmembers[$mid][] = $record['ID'];
}
$this->distlists[$record['ID']] = $record;
}
}
}
/**
* Map fields from internal Kolab_Format to Roundcube contact format
*/
private function _to_rcube_contact($record)
{
$out = array(
'ID' => md5($record['uid']),
'email' => array(),
'phone' => array(),
);
foreach ($this->fieldmap as $kolab => $rcube) {
if (strlen($record[$kolab]))
$out[$rcube] = $record[$kolab];
}
if (isset($record['gender']))
$out['gender'] = $this->gender_map[$record['gender']];
foreach ((array)$record['email'] as $i => $email)
$out['email'][] = $email['smtp-address'];
if (!$record['email'] && $record['emails'])
$out['email'] = preg_split('/,\s*/', $record['emails']);
foreach ((array)$record['phone'] as $i => $phone)
$out['phone:'.$phone['type']][] = $phone['number'];
if (is_array($record['address'])) {
foreach ($record['address'] as $i => $adr) {
$key = 'address:' . $adr['type'];
$out[$key][] = array(
'street' => $adr['street'],
'locality' => $adr['locality'],
'zipcode' => $adr['postal-code'],
'region' => $adr['region'],
'country' => $adr['country'],
);
}
}
// photo is stored as separate attachment
if ($record['picture'] && ($att = $record['_attachments'][$record['picture']])) {
$out['photo'] = $att['content'] ? $att['content'] : $this->contactstorage->getAttachment($att['key']);
}
// remove empty fields
return array_filter($out);
}
/**
* Map fields from Roundcube format to internal Kolab_Format
*/
private function _from_rcube_contact($contact)
{
$object = array();
foreach (array_flip($this->fieldmap) as $rcube => $kolab) {
if (isset($contact[$rcube]))
$object[$kolab] = is_array($contact[$rcube]) ? $contact[$rcube][0] : $contact[$rcube];
else if ($values = $this->get_col_values($rcube, $contact, true))
$object[$kolab] = is_array($values) ? $values[0] : $values;
}
// format dates
if ($object['birthday'] && ($date = @strtotime($object['birthday'])))
$object['birthday'] = date('Y-m-d', $date);
if ($object['anniversary'] && ($date = @strtotime($object['anniversary'])))
$object['anniversary'] = date('Y-m-d', $date);
$gendermap = array_flip($this->gender_map);
if (isset($object['gender']))
$object['gender'] = $gendermap[$object['gender']];
$emails = $this->get_col_values('email', $contact, true);
$object['emails'] = join(', ', array_filter($emails));
// overwrite 'email' field
$object['email'] = null;
foreach ($this->get_col_values('phone', $contact) as $type => $values) {
if ($this->phonetypemap[$type])
$type = $this->phonetypemap[$type];
foreach ((array)$values as $phone) {
if (!empty($phone)) {
$object['phone-' . $type] = $phone;
$object['phone'][] = array('number' => $phone, 'type' => $type);
}
}
}
$object['address'] = array();
foreach ($this->get_col_values('address', $contact) as $type => $values) {
if ($this->addresstypemap[$type])
$type = $this->addresstypemap[$type];
$updated = false;
$basekey = 'addr-' . $type . '-';
foreach ((array)$values as $adr) {
// skip empty address
$adr = array_filter($adr);
if (empty($adr))
continue;
// switch type if slot is already taken
if (isset($object[$basekey . 'type'])) {
$type = $type == 'home' ? 'business' : 'home';
$basekey = 'addr-' . $type . '-';
}
if (!isset($object[$basekey . 'type'])) {
$object[$basekey . 'type'] = $type;
$object[$basekey . 'street'] = $adr['street'];
$object[$basekey . 'locality'] = $adr['locality'];
$object[$basekey . 'postal-code'] = $adr['zipcode'];
$object[$basekey . 'region'] = $adr['region'];
$object[$basekey . 'country'] = $adr['country'];
// Update existing address entry of this type
foreach($object['address'] as $index => $address) {
if ($address['type'] == $type) {
$object['address'][$index] = $new_address;
$updated = true;
}
}
}
if (!$updated) {
$object['address'][] = array(
'type' => $type,
'street' => $adr['street'],
'locality' => $adr['locality'],
'postal-code' => $adr['zipcode'],
'region' => $adr['region'],
'country' => $adr['country'],
);
}
}
}
// save new photo as attachment
if ($contact['photo']) {
$attkey = 'photo.attachment';
$object['_attachments'][$attkey] = array(
'type' => rc_image_content_type($contact['photo']),
'content' => preg_match('![^a-z0-9/=+-]!i', $contact['photo']) ? $contact['photo'] : base64_decode($contact['photo']),
);
$object['picture'] = $attkey;
}
return $object;
}
}
diff --git a/plugins/kolab_addressbook/localization/de_CH.inc b/plugins/kolab_addressbook/localization/de_CH.inc
new file mode 100644
index 00000000..f91a24ba
--- /dev/null
+++ b/plugins/kolab_addressbook/localization/de_CH.inc
@@ -0,0 +1,46 @@
+<?php
+
+$labels = array();
+$labels['initials'] = 'Initialen';
+$labels['profession'] = 'Berufsbezeichnung';
+$labels['officelocation'] = 'Büro Adresse';
+$labels['children'] = 'Kinder';
+$labels['pgppublickey'] = 'Öffentlicher PGP-Schlüssel';
+$labels['freebusyurl'] = 'Frei/Belegt URL';
+$labels['typebusiness'] = 'Dienstlich';
+$labels['typebusinessfax'] = 'Dienst';
+$labels['typecompany'] = 'Firma';
+$labels['typeprimary'] = 'Primär';
+$labels['typetelex'] = 'Telex';
+$labels['typeradio'] = 'Funk';
+$labels['typeisdn'] = 'ISDN';
+$labels['typettytdd'] = 'Telescrit';
+$labels['typecallback'] = 'Rückruf';
+$labels['settings'] = 'Einstellungen';
+
+$labels['bookcreate'] = 'Adressbuch anlegen';
+$labels['bookedit'] = 'Adressbuch bearbeiten';
+$labels['bookdelete'] = 'Adressbuch löschen';
+$labels['bookproperties'] = 'Eigenschaften des Adressbuchs';
+$labels['bookname'] = 'Name des Buches';
+$labels['parentbook'] = 'Übergeordnetes Buch';
+
+$labels['addressbookprio'] = 'Reihenfolge der Adressbücher';
+$labels['personalfirst'] = 'Private(s) Adressbuch/Adressbücher zuerst';
+$labels['globalfirst'] = 'Globale(s) Adressbuch/Adressbücher zuerst';
+$labels['personalonly'] = 'Nur persönliche(s) Adressbuch/Adressbücher';
+$labels['globalonly'] = 'Nur globale(s) Adressbuch/Adressbücher';
+
+$messages['bookdeleteconfirm'] = 'Soll das gewählte Adressbuch und alle Kontakte darin wirklich gelöscht werden?';
+$messages['bookdeleting'] = 'Adressbuch wird gelöscht...';
+$messages['booksaving'] = 'Adressbuch wird gespeichert...';
+$messages['bookdeleted'] = 'Adressbuch erfolgreich gelöscht.';
+$messages['bookupdated'] = 'Adressbuch erfolgreich aktualisiert.';
+$messages['bookcreated'] = 'Adressbuch erfolgreich angelegt.';
+$messages['bookdeleteerror'] = 'Fehler beim Löschen des Adressbuchs.';
+$messages['bookupdateerror'] = 'Fehler beim Aktualisieren des Adressbuchs.';
+$messages['bookcreateerror'] = 'Fehler beim Anlegen des Adressbuchs.';
+$messages['nobooknamewarning'] = 'Bitte den Namen des Adressbuchs angeben.';
+$messages['noemailnamewarning'] = 'Bitte E-Mail-Adresse oder Namen des Kontakts angeben.';
+
+?>
diff --git a/plugins/kolab_addressbook/localization/de_DE.inc b/plugins/kolab_addressbook/localization/de_DE.inc
new file mode 100644
index 00000000..5fd86b76
--- /dev/null
+++ b/plugins/kolab_addressbook/localization/de_DE.inc
@@ -0,0 +1,46 @@
+<?php
+
+$labels = array();
+$labels['initials'] = 'Initialen';
+$labels['profession'] = 'Berufsbezeichnung';
+$labels['officelocation'] = 'Büro Adresse';
+$labels['children'] = 'Kinder';
+$labels['pgppublickey'] = 'Öffentlicher PGP-Schlüssel';
+$labels['freebusyurl'] = 'Frei/Belegt URL';
+$labels['typebusiness'] = 'Dienstlich';
+$labels['typebusinessfax'] = 'Dienst';
+$labels['typecompany'] = 'Firma';
+$labels['typeprimary'] = 'Primär';
+$labels['typetelex'] = 'Fernschreiber';
+$labels['typeradio'] = 'Funktelefon';
+$labels['typeisdn'] = 'ISDN';
+$labels['typettytdd'] = 'Schreibtelefon';
+$labels['typecallback'] = 'Rückruf';
+$labels['settings'] = 'Einstellungen';
+
+$labels['bookcreate'] = 'Adressbuch anlegen';
+$labels['bookedit'] = 'Adressbuch bearbeiten';
+$labels['bookdelete'] = 'Adressbuch löschen';
+$labels['bookproperties'] = 'Eigenschaften des Adressbuchs';
+$labels['bookname'] = 'Name des Buches';
+$labels['parentbook'] = 'Übergeordnetes Buch';
+
+$labels['addressbookprio'] = 'Reihenfolge der Adressbücher';
+$labels['personalfirst'] = 'Private(s) Adressbuch/Adressbücher zuerst';
+$labels['globalfirst'] = 'Globale(s) Adressbuch/Adressbücher zuerst';
+$labels['personalonly'] = 'Nur persönliche(s) Adressbuch/Adressbücher';
+$labels['globalonly'] = 'Nur globale(s) Adressbuch/Adressbücher';
+
+$messages['bookdeleteconfirm'] = 'Soll das gewählte Adressbuch und alle Kontakte darin wirklich gelöscht werden?';
+$messages['bookdeleting'] = 'Adressbuch wird gelöscht...';
+$messages['booksaving'] = 'Adressbuch wird gespeichert...';
+$messages['bookdeleted'] = 'Adressbuch erfolgreich gelöscht.';
+$messages['bookupdated'] = 'Adressbuch erfolgreich aktualisiert.';
+$messages['bookcreated'] = 'Adressbuch erfolgreich angelegt.';
+$messages['bookdeleteerror'] = 'Fehler beim Löschen des Adressbuchs.';
+$messages['bookupdateerror'] = 'Fehler beim Aktualisieren des Adressbuchs.';
+$messages['bookcreateerror'] = 'Fehler beim Anlegen des Adressbuchs.';
+$messages['nobooknamewarning'] = 'Bitte den Namen des Adressbuchs angeben.';
+$messages['noemailnamewarning'] = 'Bitte E-Mail-Adresse oder Namen des Kontakts angeben.';
+
+?>
diff --git a/plugins/kolab_auth/localization/de_CH.inc b/plugins/kolab_auth/localization/de_CH.inc
new file mode 100644
index 00000000..9cdad33a
--- /dev/null
+++ b/plugins/kolab_auth/localization/de_CH.inc
@@ -0,0 +1,5 @@
+<?php
+
+$labels['loginas'] = 'Anmelden als';
+
+?>
diff --git a/plugins/kolab_auth/localization/de_DE.inc b/plugins/kolab_auth/localization/de_DE.inc
new file mode 100644
index 00000000..9cdad33a
--- /dev/null
+++ b/plugins/kolab_auth/localization/de_DE.inc
@@ -0,0 +1,5 @@
+<?php
+
+$labels['loginas'] = 'Anmelden als';
+
+?>
diff --git a/plugins/kolab_folders/config.inc.php.dist b/plugins/kolab_folders/config.inc.php.dist
index d907a98f..e3936849 100644
--- a/plugins/kolab_folders/config.inc.php.dist
+++ b/plugins/kolab_folders/config.inc.php.dist
@@ -1,34 +1,34 @@
<?php
// Default kolab-specific folders. Set values to non-empty
// strings to create default folders of apropriate type.
// If there is no default folder with specified type in user mailbox,
// it will be created.
// Note: Mail folders will be also subscribed.
// Default Configuration folder
$rcmail_config['kolab_folders_configuration_default'] = '';
// Default Calendar folder
$rcmail_config['kolab_folders_event_default'] = '';
// Default Contacts (Addressbook) folder
$rcmail_config['kolab_folders_contact_default'] = '';
// Default Tasks folder
$rcmail_config['kolab_folders_task_default'] = '';
// Default Notes folder
$rcmail_config['kolab_folders_note_default'] = '';
// Default Journal folder
$rcmail_config['kolab_folders_journal_default'] = '';
// INBOX folder
$rcmail_config['kolab_folders_mail_inbox'] = '';
// Drafts folder
$rcmail_config['kolab_folders_mail_drafts'] = '';
// Sent folder
$rcmail_config['kolab_folders_mail_sentitems'] = '';
// Trash folder
-$rcmail_config['kolab_folders_mail_junkemail'] = '';
+$rcmail_config['kolab_folders_mail_wastebasket'] = '';
// Others folders
$rcmail_config['kolab_folders_mail_outbox'] = '';
-$rcmail_config['kolab_folders_mail_wastebasket'] = '';
+$rcmail_config['kolab_folders_mail_junkemail'] = '';
?>
diff --git a/plugins/kolab_folders/localization/de_CH.inc b/plugins/kolab_folders/localization/de_CH.inc
new file mode 100644
index 00000000..f9f6e169
--- /dev/null
+++ b/plugins/kolab_folders/localization/de_CH.inc
@@ -0,0 +1,24 @@
+<?php
+
+$labels = array();
+
+$labels['folderctype'] = 'Ordnerinhalt';
+$labels['foldertypemail'] = 'E-Mail';
+$labels['foldertypeevent'] = 'Kalender'; // Termine?
+$labels['foldertypejournal'] = 'Journal';
+$labels['foldertypetask'] = 'Aufgaben';
+$labels['foldertypenote'] = 'Notizen';
+$labels['foldertypecontact'] = 'Kontakte';
+$labels['foldertypeconfiguration'] = 'Konfiguration';
+
+$labels['default'] = 'Standard';
+$labels['inbox'] = 'Posteingang';
+$labels['drafts'] = 'Entwürfe';
+$labels['sentitems'] = 'Gesendet';
+$labels['outbox'] = 'Postausgang';
+$labels['wastebasket'] = 'Gelöscht';
+$labels['junkemail'] = 'Spam';
+
+$messages['defaultfolderexists'] = 'Es existiert bereits ein Standardordner für den angegebenen Typ';
+
+?>
diff --git a/plugins/kolab_folders/localization/de_DE.inc b/plugins/kolab_folders/localization/de_DE.inc
new file mode 100644
index 00000000..f9f6e169
--- /dev/null
+++ b/plugins/kolab_folders/localization/de_DE.inc
@@ -0,0 +1,24 @@
+<?php
+
+$labels = array();
+
+$labels['folderctype'] = 'Ordnerinhalt';
+$labels['foldertypemail'] = 'E-Mail';
+$labels['foldertypeevent'] = 'Kalender'; // Termine?
+$labels['foldertypejournal'] = 'Journal';
+$labels['foldertypetask'] = 'Aufgaben';
+$labels['foldertypenote'] = 'Notizen';
+$labels['foldertypecontact'] = 'Kontakte';
+$labels['foldertypeconfiguration'] = 'Konfiguration';
+
+$labels['default'] = 'Standard';
+$labels['inbox'] = 'Posteingang';
+$labels['drafts'] = 'Entwürfe';
+$labels['sentitems'] = 'Gesendet';
+$labels['outbox'] = 'Postausgang';
+$labels['wastebasket'] = 'Gelöscht';
+$labels['junkemail'] = 'Spam';
+
+$messages['defaultfolderexists'] = 'Es existiert bereits ein Standardordner für den angegebenen Typ';
+
+?>
diff --git a/plugins/kolab_zpush/localization/de_CH.inc b/plugins/kolab_zpush/localization/de_CH.inc
index 2f92044d..f0abdbf5 100644
--- a/plugins/kolab_zpush/localization/de_CH.inc
+++ b/plugins/kolab_zpush/localization/de_CH.inc
@@ -1,32 +1,32 @@
<?php
$labels = array();
$labels['tabtitle'] = 'Activesync';
$labels['devices'] = 'Geräte';
$labels['devicealias'] = 'Gerätename';
$labels['syncmode'] = 'Modus';
$labels['modeauto'] = 'Automatisch';
-$labels['modeflat'] = 'Forciere flache Struktur';
-$labels['modefolder'] = 'Forciere Ordnerstruktur';
+$labels['modeflat'] = 'Flache Struktur erzwingen';
+$labels['modefolder'] = 'Ordnerstruktur erzwingen';
$labels['synchronize'] = 'Synchronisieren';
$labels['withalarms'] = 'Mit Erinnerungen';
$labels['syncsettings'] = 'Synchronisationseinstellungen';
$labels['deviceconfig'] = 'Gerätekonfiguration';
$labels['folderstosync'] = 'Order zum Synchronisieren';
$labels['mail'] = 'E-Mail';
-$labels['contact'] = 'Adressbücher';
-$labels['event'] = 'Kalendar';
+$labels['contact'] = 'Kontakte';
+$labels['event'] = 'Kalender';
$labels['task'] = 'Aufgaben';
$labels['note'] = 'Notizen';
$labels['deletedevice'] = 'Gerät löschen';
$labels['imageformat'] = 'Bildformat';
-$labels['laxpiclabel'] = 'Erlaube PNG- und GIF-Bilder';
+$labels['laxpiclabel'] = 'PNG- und GIF-Bilder erlauben';
$labels['introtext'] = 'Wählen Sie das zu konfigurierende Gerät aus';
$labels['nodevices'] = 'Es sind noch keine Geräte registriert.<br/><br/>Um ein neues Gerät anzumelden, verbinden Sie dieses zuerst mit dem Server. Eine Anleitung dazu finden Sie im <a href="http://wiki.kolab.org/Z_push#Clients">Wiki</a>. Anschliessend laden Sie diese Seite neu und das Gerät wird hier aufgelistet.';
$labels['savingdata'] = 'Daten werden gespeichert...';
$labels['savingerror'] = 'Fehler beim Speichern';
$labels['notsupported'] = 'Ihr Server unterstützt keine Activesync-Konfiguration';
$labels['devicedeleteconfirm'] = 'Wollen Sie wirklich alle Einstellungen für dieses Gerät löschen?';
$labels['successfullydeleted'] = 'Die Geräteinstellungen wurden erfolgreich gelöscht';
-?>
\ No newline at end of file
+?>
diff --git a/plugins/kolab_zpush/localization/de_CH.inc b/plugins/kolab_zpush/localization/de_DE.inc
similarity index 86%
copy from plugins/kolab_zpush/localization/de_CH.inc
copy to plugins/kolab_zpush/localization/de_DE.inc
index 2f92044d..f0abdbf5 100644
--- a/plugins/kolab_zpush/localization/de_CH.inc
+++ b/plugins/kolab_zpush/localization/de_DE.inc
@@ -1,32 +1,32 @@
<?php
$labels = array();
$labels['tabtitle'] = 'Activesync';
$labels['devices'] = 'Geräte';
$labels['devicealias'] = 'Gerätename';
$labels['syncmode'] = 'Modus';
$labels['modeauto'] = 'Automatisch';
-$labels['modeflat'] = 'Forciere flache Struktur';
-$labels['modefolder'] = 'Forciere Ordnerstruktur';
+$labels['modeflat'] = 'Flache Struktur erzwingen';
+$labels['modefolder'] = 'Ordnerstruktur erzwingen';
$labels['synchronize'] = 'Synchronisieren';
$labels['withalarms'] = 'Mit Erinnerungen';
$labels['syncsettings'] = 'Synchronisationseinstellungen';
$labels['deviceconfig'] = 'Gerätekonfiguration';
$labels['folderstosync'] = 'Order zum Synchronisieren';
$labels['mail'] = 'E-Mail';
-$labels['contact'] = 'Adressbücher';
-$labels['event'] = 'Kalendar';
+$labels['contact'] = 'Kontakte';
+$labels['event'] = 'Kalender';
$labels['task'] = 'Aufgaben';
$labels['note'] = 'Notizen';
$labels['deletedevice'] = 'Gerät löschen';
$labels['imageformat'] = 'Bildformat';
-$labels['laxpiclabel'] = 'Erlaube PNG- und GIF-Bilder';
+$labels['laxpiclabel'] = 'PNG- und GIF-Bilder erlauben';
$labels['introtext'] = 'Wählen Sie das zu konfigurierende Gerät aus';
$labels['nodevices'] = 'Es sind noch keine Geräte registriert.<br/><br/>Um ein neues Gerät anzumelden, verbinden Sie dieses zuerst mit dem Server. Eine Anleitung dazu finden Sie im <a href="http://wiki.kolab.org/Z_push#Clients">Wiki</a>. Anschliessend laden Sie diese Seite neu und das Gerät wird hier aufgelistet.';
$labels['savingdata'] = 'Daten werden gespeichert...';
$labels['savingerror'] = 'Fehler beim Speichern';
$labels['notsupported'] = 'Ihr Server unterstützt keine Activesync-Konfiguration';
$labels['devicedeleteconfirm'] = 'Wollen Sie wirklich alle Einstellungen für dieses Gerät löschen?';
$labels['successfullydeleted'] = 'Die Geräteinstellungen wurden erfolgreich gelöscht';
-?>
\ No newline at end of file
+?>

File Metadata

Mime Type
text/x-diff
Expires
Mon, Aug 17, 8:27 PM (1 d, 12 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1257661
Default Alt Text
(424 KB)

Event Timeline