Page MenuHomePhorge

No OneTemporary

Size
882 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 39496ca6..312a51ed 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -1,2314 +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 = $this->rc->config->get('timezone');
- $this->dst_active = $this->rc->config->get('dst_active');
- $this->gmt_offset = ($this->timezone + $this->dst_active) * 3600;
- $this->user_timezone = new DateTimeZone($this->timezone ? timezone_name_from_abbr("", $this->gmt_offset, $this->dst_active) : 'GMT');
+ $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' => 'calendarlink active',
- 'class' => 'calendarlink',
+ '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()
+ 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;
+ $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->user_timezone->getName())) {
- $fromto .= ' (' . $tzname . ')';
+ 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;
+ $default_port = 443;
}
$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 31481d32..a2aac7cf 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -1,2707 +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) 2011, Kolab Systems AG <contact@kolabsys.com>
+ * 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++) {
- li = document.createElement('LI');
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 = list[i].name;
+ 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
- $('#event-date').html(Q(me.event_date_text(event))).show();
+ // -> 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: null,
+ 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+110), width: Math.min(w-20, width+50) })
+ $(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;
- fc.fullCalendar('option', 'height', $('#main').height() - footer);
+ 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', !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: $('#main').height(),
+ 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/database/database_driver.php b/plugins/calendar/drivers/database/database_driver.php
index 83843d7f..ba7f1e7c 100644
--- a/plugins/calendar/drivers/database/database_driver.php
+++ b/plugins/calendar/drivers/database/database_driver.php
@@ -1,974 +1,974 @@
<?php
/**
* Database driver 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) 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 database_driver extends calendar_driver
{
// features this backend supports
public $alarms = true;
public $attendees = true;
public $freebusy = false;
public $attachments = true;
public $alarm_types = array('DISPLAY','EMAIL');
private $rc;
private $cal;
private $cache = array();
private $calendars = array();
private $calendar_ids = '';
private $free_busy_map = array('free' => 0, 'busy' => 1, 'out-of-office' => 2, 'outofoffice' => 2, 'tentative' => 3);
private $db_events = 'events';
private $db_calendars = 'calendars';
private $db_attachments = 'attachments';
private $sequence_events = 'event_ids';
private $sequence_calendars = 'calendar_ids';
private $sequence_attachments = 'attachment_ids';
/**
* Default constructor
*/
public function __construct($cal)
{
$this->cal = $cal;
$this->rc = $cal->rc;
// load library classes
require_once($this->cal->home . '/lib/Horde_Date_Recurrence.php');
// read database config
$this->db_events = $this->rc->config->get('db_table_events', $this->db_events);
$this->db_calendars = $this->rc->config->get('db_table_calendars', $this->db_calendars);
$this->db_attachments = $this->rc->config->get('db_table_attachments', $this->db_attachments);
$this->sequence_events = $this->rc->config->get('db_sequence_events', $this->sequence_events);
$this->sequence_calendars = $this->rc->config->get('db_sequence_calendars', $this->sequence_calendars);
$this->sequence_attachments = $this->rc->config->get('db_sequence_attachments', $this->sequence_attachments);
$this->_read_calendars();
}
/**
* Read available calendars for the current user and store them internally
*/
private function _read_calendars()
{
$hidden = array_filter(explode(',', $this->rc->config->get('hidden_calendars', '')));
if (!empty($this->rc->user->ID)) {
$calendar_ids = array();
$result = $this->rc->db->query(
"SELECT *, calendar_id AS id FROM " . $this->db_calendars . "
WHERE user_id=?
ORDER BY name",
$this->rc->user->ID
);
while ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
$arr['showalarms'] = intval($arr['showalarms']);
$arr['active'] = !in_array($arr['id'], $hidden);
$this->calendars[$arr['calendar_id']] = $arr;
$calendar_ids[] = $this->rc->db->quote($arr['calendar_id']);
}
$this->calendar_ids = join(',', $calendar_ids);
}
}
/**
* Get a list of available calendars from this source
*/
public function list_calendars()
{
// attempt to create a default calendar for this user
if (empty($this->calendars)) {
if ($this->create_calendar(array('name' => 'Default', 'color' => 'cc0000')))
$this->_read_calendars();
}
return $this->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)
{
$result = $this->rc->db->query(
"INSERT INTO " . $this->db_calendars . "
(user_id, name, color, showalarms)
VALUES (?, ?, ?, ?)",
$this->rc->user->ID,
$prop['name'],
$prop['color'],
$prop['showalarms']?1:0
);
if ($result)
return $this->rc->db->insert_id($this->sequence_calendars);
return false;
}
/**
* Update properties of an existing calendar
*
* @see calendar_driver::edit_calendar()
*/
public function edit_calendar($prop)
{
$query = $this->rc->db->query(
"UPDATE " . $this->db_calendars . "
SET name=?, color=?, showalarms=?
WHERE calendar_id=?
AND user_id=?",
$prop['name'],
$prop['color'],
$prop['showalarms']?1:0,
$prop['id'],
$this->rc->user->ID
);
return $this->rc->db->affected_rows($query);
}
/**
* Set active/subscribed state of a calendar
* Save a list of hidden calendars in user prefs
*
* @see calendar_driver::subscribe_calendar()
*/
public function subscribe_calendar($prop)
{
$hidden = array_flip(explode(',', $this->rc->config->get('hidden_calendars', '')));
if ($prop['active'])
unset($hidden[$prop['id']]);
else
$hidden[$prop['id']] = 1;
return $this->rc->user->save_prefs(array('hidden_calendars' => join(',', array_keys($hidden))));
}
/**
* Delete the given calendar with all its contents
*
* @see calendar_driver::remove_calendar()
*/
public function remove_calendar($prop)
{
if (!$this->calendars[$prop['id']])
return false;
// events and attachments will be deleted by foreign key cascade
$query = $this->rc->db->query(
"DELETE FROM " . $this->db_calendars . "
WHERE calendar_id=?",
$prop['id']
);
return $this->rc->db->affected_rows($query);
}
/**
* Add a single event to the database
*
* @param array Hash array with event properties
* @see calendar_driver::new_event()
*/
public function new_event($event)
{
if (!empty($this->calendars)) {
if ($event['calendar'] && !$this->calendars[$event['calendar']])
return false;
if (!$event['calendar'])
$event['calendar'] = reset(array_keys($this->calendars));
$event = $this->_save_preprocess($event);
$query = $this->rc->db->query(sprintf(
"INSERT INTO " . $this->db_events . "
(calendar_id, created, changed, uid, start, end, all_day, recurrence, title, description, location, categories, free_busy, priority, sensitivity, attendees, alarms, notifyat)
VALUES (?, %s, %s, ?, %s, %s, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
$this->rc->db->now(),
$this->rc->db->now(),
$this->rc->db->fromunixtime($event['start']),
$this->rc->db->fromunixtime($event['end'])
),
$event['calendar'],
strval($event['uid']),
intval($event['all_day']),
$event['_recurrence'],
strval($event['title']),
strval($event['description']),
strval($event['location']),
strval($event['categories']),
intval($event['free_busy']),
intval($event['priority']),
intval($event['sensitivity']),
$event['attendees'],
$event['alarms'],
$event['notifyat']
);
$event_id = $this->rc->db->insert_id($this->sequence_events);
if ($event_id) {
$event['id'] = $event_id;
// add attachments
if (!empty($event['attachments'])) {
foreach ($event['attachments'] as $attachment) {
$this->add_attachment($attachment, $event_id);
unset($attachment);
}
}
$this->_update_recurring($event);
}
return $event_id;
}
return false;
}
/**
* Update an event entry with the given data
*
* @param array Hash array with event properties
* @see calendar_driver::edit_event()
*/
public function edit_event($event)
{
if (!empty($this->calendars)) {
$update_master = false;
$update_recurring = true;
$old = $this->get_event($event);
// modify a recurring event, check submitted savemode to do the right things
if ($old['recurrence'] || $old['recurrence_id']) {
$master = $old['recurrence_id'] ? $this->get_event(array('id' => $old['recurrence_id'])) : $old;
// keep saved exceptions (not submitted by the client)
if ($old['recurrence']['EXDATE'])
$event['recurrence']['EXDATE'] = $old['recurrence']['EXDATE'];
switch ($event['_savemode']) {
case 'new':
$event['uid'] = $this->cal->generate_uid();
return $this->new_event($event);
case 'current':
// add exception to master event
$master['recurrence']['EXDATE'][] = $old['start'];
$update_master = true;
// just update this occurence (decouple from master)
$update_recurring = false;
$event['recurrence_id'] = 0;
$event['recurrence'] = array();
break;
case 'future':
if ($master['id'] != $event['id']) {
// set until-date on master event, then save this instance as new recurring event
$master['recurrence']['UNTIL'] = $event['start'] - 86400;
unset($master['recurrence']['COUNT']);
$update_master = true;
// if recurrence COUNT, update value to the correct number of future occurences
if ($event['recurrence']['COUNT']) {
$sqlresult = $this->rc->db->query(sprintf(
"SELECT event_id FROM " . $this->db_events . "
WHERE calendar_id IN (%s)
AND start >= %s
AND recurrence_id=?",
$this->calendar_ids,
$this->rc->db->fromunixtime($event['start'])
),
$master['id']);
if ($count = $this->rc->db->num_rows($sqlresult))
$event['recurrence']['COUNT'] = $count;
}
$update_recurring = true;
$event['recurrence_id'] = 0;
break;
}
// else: 'future' == 'all' if modifying the master event
default: // 'all' is default
$event['id'] = $master['id'];
$event['recurrence_id'] = 0;
// 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;
}
break;
}
}
$success = $this->_update_event($event, $update_recurring);
if ($success && $update_master)
$this->_update_event($master, true);
return $success;
}
return false;
}
/**
* Convert save data to be used in SQL statements
*/
private function _save_preprocess($event)
{
// compose vcalendar-style recurrencue rule from structured data
$rrule = $event['recurrence'] ? calendar::to_rrule($event['recurrence']) : '';
$event['_recurrence'] = rtrim($rrule, ';');
$event['free_busy'] = intval($this->free_busy_map[strtolower($event['free_busy'])]);
if (isset($event['allday'])) {
$event['all_day'] = $event['allday'] ? 1 : 0;
}
// compute absolute time to notify the user
$event['notifyat'] = $this->_get_notification($event);
// process event attendees
$_attendees = '';
foreach ((array)$event['attendees'] as $attendee) {
if (!$attendee['name'] && !$attendee['email'])
continue;
$_attendees .= 'NAME="'.addcslashes($attendee['name'], '"') . '"' .
';STATUS=' . $attendee['status'].
';ROLE=' . $attendee['role'] .
';EMAIL=' . $attendee['email'] .
"\n";
}
$event['attendees'] = rtrim($_attendees);
return $event;
}
/**
* Compute absolute time to notify the user
*/
private function _get_notification($event)
{
if ($event['alarms']) {
list($trigger, $action) = explode(':', $event['alarms']);
$notify = calendar::parse_alaram_value($trigger);
if (!empty($notify[1])){ // offset
$mult = 1;
switch ($notify[1]) {
case '-M': $mult = -60; break;
case '+M': $mult = 60; break;
case '-H': $mult = -3600; break;
case '+H': $mult = 3600; break;
case '-D': $mult = -86400; break;
case '+D': $mult = 86400; break;
}
$offset = $notify[0] * $mult;
$refdate = $mult > 0 ? $event['end'] : $event['start'];
$notify_at = $refdate + $offset;
}
else { // absolute timestamp
$notify_at = $notify[0];
}
if ($event['start'] > time())
return date('Y-m-d H:i:s', $notify_at);
}
return null;
}
/**
* Save the given event record to database
*
* @param array Event data, already passed through self::_save_preprocess()
* @param boolean True if recurring events instances should be updated, too
*/
private function _update_event($event, $update_recurring = true)
{
$event = $this->_save_preprocess($event);
$sql_set = array();
$set_cols = array('all_day', 'recurrence_id', 'title', 'description', 'location', 'categories', 'free_busy', 'priority', 'sensitivity', 'attendees', 'alarms', 'notifyat');
foreach ($set_cols as $col) {
if (isset($event[$col]))
$sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($event[$col]);
}
if ($event['_recurrence'])
$sql_set[] = $this->rc->db->quote_identifier('recurrence') . '=' . $this->rc->db->quote($event['_recurrence']);
if ($event['_fromcalendar'] && $event['_fromcalendar'] != $event['calendar'])
$sql_set[] = 'calendar_id=' . $this->rc->db->quote($event['calendar']);
$query = $this->rc->db->query(sprintf(
"UPDATE " . $this->db_events . "
SET changed=%s, start=%s, end=%s %s
WHERE event_id=?
AND calendar_id IN (" . $this->calendar_ids . ")",
$this->rc->db->now(),
$this->rc->db->fromunixtime($event['start']),
$this->rc->db->fromunixtime($event['end']),
($sql_set ? ', ' . join(', ', $sql_set) : '')
),
$event['id']
);
$success = $this->rc->db->affected_rows($query);
// add attachments
if ($success && !empty($event['attachments'])) {
foreach ($event['attachments'] as $attachment) {
$this->add_attachment($attachment, $event['id']);
unset($attachment);
}
}
// remove attachments
if ($success && !empty($event['deleted_attachments'])) {
foreach ($event['deleted_attachments'] as $attachment) {
$this->remove_attachment($attachment, $event['id']);
}
}
if ($success) {
unset($this->cache[$event['id']]);
if ($update_recurring)
$this->_update_recurring($event);
}
return $success;
}
/**
* Insert "fake" entries for recurring occurences of this event
*/
private function _update_recurring($event)
{
if (empty($this->calendars))
return;
// clear existing recurrence copies
$this->rc->db->query(
"DELETE FROM " . $this->db_events . "
WHERE recurrence_id=?
AND calendar_id IN (" . $this->calendar_ids . ")",
$event['id']
);
// create new fake entries
if ($event['recurrence']) {
// include library class
require_once($this->cal->home . '/lib/calendar_recurrence.php');
$recurrence = new calendar_recurrence($this->cal, $event);
$duration = $event['end'] - $event['start'];
while ($next_ts = $recurrence->next_start()) {
$notify_at = $this->_get_notification(array('alarms' => $event['alarms'], 'start' => $next_ts, 'end' => $next_ts + $duration));
$query = $this->rc->db->query(sprintf(
"INSERT INTO " . $this->db_events . "
(calendar_id, recurrence_id, created, changed, uid, start, end, all_day, recurrence, title, description, location, categories, free_busy, priority, sensitivity, alarms, notifyat)
SELECT calendar_id, ?, %s, %s, uid, %s, %s, all_day, recurrence, title, description, location, categories, free_busy, priority, sensitivity, alarms, ?
FROM " . $this->db_events . " WHERE event_id=? AND calendar_id IN (" . $this->calendar_ids . ")",
$this->rc->db->now(),
$this->rc->db->now(),
$this->rc->db->fromunixtime($next_ts),
$this->rc->db->fromunixtime($next_ts + $duration)
),
$event['id'],
$notify_at,
$event['id']
);
if (!$this->rc->db->affected_rows($query))
break;
// stop adding events for inifinite recurrence after 20 years
if (++$count > 999 || (!$recurrence->recurEnd && !$recurrence->recurCount && $next->year > date('Y') + 20))
break;
}
}
}
/**
* Move a single event
*
* @param array Hash array with event properties
* @see calendar_driver::move_event()
*/
public function move_event($event)
{
// let edit_event() do all the magic
return $this->edit_event($event + (array)$this->get_event($event));
}
/**
* Resize a single event
*
* @param array Hash array with event properties
* @see calendar_driver::resize_event()
*/
public function resize_event($event)
{
// let edit_event() do all the magic
return $this->edit_event($event + (array)$this->get_event($event));
}
/**
* Remove a single event from the database
*
* @param array Hash array with event properties
* @param boolean Remove record irreversible (@TODO)
*
* @see calendar_driver::remove_event()
*/
public function remove_event($event, $force = true)
{
if (!empty($this->calendars)) {
$event += (array)$this->get_event($event);
$master = $event;
$update_master = false;
$savemode = 'all';
// read master if deleting a recurring event
if ($event['recurrence'] || $event['recurrence_id']) {
- $master = $event['recurrence_id'] ? $this->get_event(array('id' => $old['recurrence_id'])) : $event;
+ $master = $event['recurrence_id'] ? $this->get_event(array('id' => $event['recurrence_id'])) : $event;
$savemode = $event['_savemode'];
}
switch ($savemode) {
case 'current':
// add exception to master event
$master['recurrence']['EXDATE'][] = $event['start'];
$update_master = true;
// just delete this single occurence
$query = $this->rc->db->query(
"DELETE FROM " . $this->db_events . "
WHERE calendar_id IN (" . $this->calendar_ids . ")
AND event_id=?",
$event['id']
);
break;
case 'future':
if ($master['id'] != $event['id']) {
// set until-date on master event
$master['recurrence']['UNTIL'] = $event['start'] - 86400;
unset($master['recurrence']['COUNT']);
$update_master = true;
// delete this and all future instances
$query = $this->rc->db->query(
"DELETE FROM " . $this->db_events . "
WHERE calendar_id IN (" . $this->calendar_ids . ")
AND start >= " . $this->rc->db->fromunixtime($old['start']) . "
AND recurrence_id=?",
$master['id']
);
break;
}
// else: future == all if modifying the master event
default: // 'all' is default
$query = $this->rc->db->query(
"DELETE FROM " . $this->db_events . "
WHERE (event_id=? OR recurrence_id=?)
AND calendar_id IN (" . $this->calendar_ids . ")",
$master['id'],
$master['id']
);
break;
}
$success = $this->rc->db->affected_rows($query);
if ($success && $update_master)
$this->_update_event($master, true);
return $success;
}
return false;
}
/**
* Return data of a specific event
* @param mixed Hash array with event properties or event UID
* @param boolean Only search in writeable calendars (currently ignored)
* @return array Hash array with event properties
*/
public function get_event($event, $writeable = null)
{
$id = is_array($event) ? ($event['id'] ? $event['id'] : $event['uid']) : $event;
$col = $event['id'] && is_numeric($event['id']) ? 'event_id' : 'uid';
if ($this->cache[$id])
return $this->cache[$id];
$result = $this->rc->db->query(sprintf(
"SELECT e.*, COUNT(a.attachment_id) AS _attachments FROM " . $this->db_events . " AS e
LEFT JOIN " . $this->db_attachments . " AS a ON (a.event_id = e.event_id OR a.event_id = e.recurrence_id)
WHERE e.calendar_id IN (%s)
AND e.$col=?",
$this->calendar_ids
),
$id);
if ($result && ($event = $this->rc->db->fetch_assoc($result)) && $event['event_id']) {
$this->cache[$id] = $this->_read_postprocess($event);
return $this->cache[$id];
}
return false;
}
/**
* Get event data
*
* @see calendar_driver::load_events()
*/
public function load_events($start, $end, $query = null, $calendars = null)
{
if (empty($calendars))
$calendars = array_keys($this->calendars);
else if (is_string($calendars))
$calendars = explode(',', $calendars);
// only allow to select from calendars of this use
$calendar_ids = array_map(array($this->rc->db, 'quote'), array_intersect($calendars, array_keys($this->calendars)));
// compose (slow) SQL query for searching
// FIXME: improve searching using a dedicated col and normalized values
if ($query) {
foreach (array('title','location','description','categories','attendees') as $col)
$sql_query[] = $this->rc->db->ilike($col, '%'.$query.'%');
$sql_add = 'AND (' . join(' OR ', $sql_query) . ')';
}
$events = array();
if (!empty($calendar_ids)) {
$result = $this->rc->db->query(sprintf(
"SELECT e.*, COUNT(a.attachment_id) AS _attachments FROM " . $this->db_events . " AS e
LEFT JOIN " . $this->db_attachments . " AS a ON (a.event_id = e.event_id OR a.event_id = e.recurrence_id)
WHERE e.calendar_id IN (%s)
AND e.start <= %s AND e.end >= %s
%s
GROUP BY e.event_id",
join(',', $calendar_ids),
$this->rc->db->fromunixtime($end),
$this->rc->db->fromunixtime($start),
$sql_add
));
while ($result && ($event = $this->rc->db->fetch_assoc($result))) {
$events[] = $this->_read_postprocess($event);
}
}
return $events;
}
/**
* Convert sql record into a rcube style event object
*/
private function _read_postprocess($event)
{
$free_busy_map = array_flip($this->free_busy_map);
$event['id'] = $event['event_id'];
$event['start'] = strtotime($event['start']);
$event['end'] = strtotime($event['end']);
$event['allday'] = intval($event['all_day']);
$event['changed'] = strtotime($event['changed']);
$event['free_busy'] = $free_busy_map[$event['free_busy']];
$event['calendar'] = $event['calendar_id'];
$event['recurrence_id'] = intval($event['recurrence_id']);
// parse recurrence rule
if ($event['recurrence'] && preg_match_all('/([A-Z]+)=([^;]+);?/', $event['recurrence'], $m, PREG_SET_ORDER)) {
$event['recurrence'] = array();
foreach ($m as $rr) {
if (is_numeric($rr[2]))
$rr[2] = intval($rr[2]);
else if ($rr[1] == 'UNTIL')
$rr[2] = strtotime($rr[2]);
else if ($rr[1] == 'EXDATE')
$rr[2] = array_map('strtotime', explode(',', $rr[2]));
$event['recurrence'][$rr[1]] = $rr[2];
}
}
if ($event['_attachments'] > 0)
$event['attachments'] = (array)$this->list_attachments($event);
// decode serialized event attendees
if ($event['attendees']) {
$attendees = array();
foreach (explode("\n", $event['attendees']) as $line) {
$att = array();
foreach (rcube_explode_quoted_string(';', $line) as $prop) {
list($key, $value) = explode("=", $prop);
$att[strtolower($key)] = stripslashes(trim($value, '""'));
}
$attendees[] = $att;
}
$event['attendees'] = $attendees;
}
unset($event['event_id'], $event['calendar_id'], $event['notifyat'], $event['all_day'], $event['_attachments']);
return $event;
}
/**
* Get a list of pending alarms to be displayed to the user
*
* @see calendar_driver::pending_alarms()
*/
public function pending_alarms($time, $calendars = null)
{
if (empty($calendars))
$calendars = array_keys($this->calendars);
else if (is_string($calendars))
$calendars = explode(',', $calendars);
// only allow to select from calendars with activated alarms
$calendar_ids = array();
foreach ($calendars as $cid) {
if ($this->calendars[$cid] && $this->calendars[$cid]['showalarms'])
$calendar_ids[] = $cid;
}
$calendar_ids = array_map(array($this->rc->db, 'quote'), $calendar_ids);
$alarms = array();
if (!empty($calendar_ids)) {
$result = $this->rc->db->query(sprintf(
"SELECT * FROM " . $this->db_events . "
WHERE calendar_id IN (%s)
AND notifyat <= %s AND end > %s",
join(',', $calendar_ids),
$this->rc->db->fromunixtime($time),
$this->rc->db->fromunixtime($time)
));
while ($result && ($event = $this->rc->db->fetch_assoc($result)))
$alarms[] = $this->_read_postprocess($event);
}
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
$notify_at = $snooze > 0 ? date('Y-m-d H:i:s', time() + $snooze) : null;
$query = $this->rc->db->query(sprintf(
"UPDATE " . $this->db_events . "
SET changed=%s, notifyat=?
WHERE event_id=?
AND calendar_id IN (" . $this->calendar_ids . ")",
$this->rc->db->now()),
$notify_at,
$event_id
);
return $this->rc->db->affected_rows($query);
}
/**
* Save an attachment related to the given event
*/
private function add_attachment($attachment, $event_id)
{
$data = $attachment['data'] ? $attachment['data'] : file_get_contents($attachment['path']);
$query = $this->rc->db->query(
"INSERT INTO " . $this->db_attachments .
" (event_id, filename, mimetype, size, data)" .
" VALUES (?, ?, ?, ?, ?)",
$event_id,
$attachment['name'],
$attachment['mimetype'],
strlen($data),
base64_encode($data)
);
return $this->rc->db->affected_rows($query);
}
/**
* Remove a specific attachment from the given event
*/
private function remove_attachment($attachment_id, $event_id)
{
$query = $this->rc->db->query(
"DELETE FROM " . $this->db_attachments .
" WHERE attachment_id = ?" .
" AND event_id IN (SELECT event_id FROM " . $this->db_events .
" WHERE event_id = ?" .
" AND calendar_id IN (" . $this->calendar_ids . "))",
$attachment_id,
$event_id
);
return $this->rc->db->affected_rows($query);
}
/**
* List attachments of specified event
*/
public function list_attachments($event)
{
$attachments = array();
$event_id = $event['recurrence_id'] ? $event['recurrence_id'] : $event['event_id'];
if (!empty($this->calendar_ids)) {
$result = $this->rc->db->query(
"SELECT attachment_id AS id, filename AS name, mimetype, size " .
" FROM " . $this->db_attachments .
" WHERE event_id IN (SELECT event_id FROM " . $this->db_events .
" WHERE event_id=?" .
" AND calendar_id IN (" . $this->calendar_ids . "))".
" ORDER BY filename",
$event['recurrence_id'] ? $event['recurrence_id'] : $event['event_id']
);
while ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
$attachments[] = $arr;
}
}
return $attachments;
}
/**
* Get attachment properties
*/
public function get_attachment($id, $event)
{
if (!empty($this->calendar_ids)) {
$result = $this->rc->db->query(
"SELECT attachment_id AS id, filename AS name, mimetype, size " .
" FROM " . $this->db_attachments .
" WHERE attachment_id=?".
" AND event_id=?",
$id,
$event['recurrence_id'] ? $event['recurrence_id'] : $event['id']
);
if ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
return $arr;
}
}
return null;
}
/**
* Get attachment body
*/
public function get_attachment_body($id, $event)
{
if (!empty($this->calendar_ids)) {
$result = $this->rc->db->query(
"SELECT data " .
" FROM " . $this->db_attachments .
" WHERE attachment_id=?".
" AND event_id=?",
$id,
$event['id']
);
if ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
return base64_decode($arr['data']);
}
}
return null;
}
/**
* Remove the given category
*/
public function remove_category($name)
{
$query = $this->rc->db->query(
"UPDATE " . $this->db_events . "
SET categories=''
WHERE categories=?
AND calendar_id IN (" . $this->calendar_ids . ")",
$name
);
return $this->rc->db->affected_rows($query);
}
/**
* Update/replace a category
*/
public function replace_category($oldname, $name, $color)
{
$query = $this->rc->db->query(
"UPDATE " . $this->db_events . "
SET categories=?
WHERE categories=?
AND calendar_id IN (" . $this->calendar_ids . ")",
$name,
$oldname
);
return $this->rc->db->affected_rows($query);
}
}
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index a346037d..ccd542ae 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -1,779 +1,779 @@
<?php
/**
* Kolab calendar storage class
*
* @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/>.
*/
class kolab_calendar
{
public $id;
public $ready = false;
public $readonly = true;
public $attachments = true;
public $alarms = false;
public $categories = array();
public $storage;
private $cal;
private $events;
private $id2uid;
private $imap_folder = 'INBOX/Calendar';
private $namespace;
private $search_fields = array('title', 'description', 'location', '_attendees');
private $sensitivity_map = array('public', 'private', 'confidential');
private $priority_map = array('low' => 9, 'normal' => 5, 'high' => 1);
private $role_map = array('REQ-PARTICIPANT' => 'required', 'OPT-PARTICIPANT' => 'optional', 'CHAIR' => 'resource');
private $status_map = array('NEEDS-ACTION' => 'none', 'TENTATIVE' => 'tentative', 'CONFIRMED' => 'accepted', 'ACCEPTED' => 'accepted', 'DECLINED' => 'declined');
private $month_map = array('', 'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december');
private $weekday_map = array('MO'=>'monday', 'TU'=>'tuesday', 'WE'=>'wednesday', 'TH'=>'thursday', 'FR'=>'friday', 'SA'=>'saturday', 'SU'=>'sunday');
/**
* Default constructor
*/
public function __construct($imap_folder, $calendar)
{
$this->cal = $calendar;
if (strlen($imap_folder))
$this->imap_folder = $imap_folder;
// ID is derrived from folder name
$this->id = rcube_kolab::folder_id($this->imap_folder);
// fetch objects from the given IMAP folder
$this->storage = rcube_kolab::get_storage($this->imap_folder);
$this->ready = !PEAR::isError($this->storage);
// Set readonly and alarms flags according to folder permissions
if ($this->ready) {
if ($this->get_owner() == $_SESSION['username']) {
$this->readonly = false;
$this->alarms = true;
}
else {
$rights = $this->storage->_folder->getMyRights();
if (!PEAR::isError($rights)) {
if (strpos($rights, 'i') !== false)
$this->readonly = false;
}
}
// user-specific alarms settings win
$prefs = $this->cal->rc->config->get('kolab_calendars', array());
if (isset($prefs[$this->id]['showalarms']))
$this->alarms = $prefs[$this->id]['showalarms'];
}
}
/**
* Getter for a nice and human readable name for this calendar
* See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference
*
* @return string Name of this calendar
*/
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->storage->_folder->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;
}
/**
* Getter for the top-end calendar folder name (not the entire path)
*
* @return string Name of this calendar
*/
public function get_foldername()
{
$parts = explode('/', $this->imap_folder);
- return rcube_charset_convert(end($parts), 'UTF7-IMAP');
+ return rcube_charset::convert(end($parts), 'UTF7-IMAP');
}
/**
* Return color to display this calendar
*/
public function get_color()
{
// color is defined in folder METADATA
if ($color = $this->storage->_folder->getKolabAttribute('color', HORDE_ANNOT_READ_PRIVATE_SHARED)) {
return $color;
}
// calendar color is stored in user prefs (temporary solution)
$prefs = $this->cal->rc->config->get('kolab_calendars', array());
if (!empty($prefs[$this->id]) && !empty($prefs[$this->id]['color']))
return $prefs[$this->id]['color'];
return 'cc0000';
}
/**
* Return the corresponding Kolab_Folder instance
*/
public function get_folder()
{
return $this->storage->_folder;
}
/**
* Getter for the attachment body
*/
public function get_attachment_body($id)
{
return $this->storage->getAttachment($id);
}
/**
* Getter for a single event object
*/
public function get_event($id)
{
$this->_fetch_events();
// event not found, maybe a recurring instance is requested
if (!$this->events[$id]) {
$master_id = preg_replace('/-\d+$/', '', $id);
if ($this->events[$master_id] && $this->events[$master_id]['recurrence']) {
$master = $this->events[$master_id];
$this->_get_recurring_events($master, $master['start'], $master['start'] + 86400 * 365 * 10, $id);
}
}
return $this->events[$id];
}
/**
* @param integer Event's new start (unix timestamp)
* @param integer Event's new end (unix timestamp)
* @param string Search query (optional)
* @param boolean Strip virtual events (optional)
* @return array A list of event records
*/
public function list_events($start, $end, $search = null, $virtual = 1)
{
$this->_fetch_events();
if (!empty($search))
$search = mb_strtolower($search);
$events = array();
foreach ($this->events as $id => $event) {
// remember seen categories
if ($event['categories'])
$this->categories[$event['categories']]++;
// filter events by search query
if (!empty($search)) {
$hit = false;
foreach ($this->search_fields as $col) {
$sval = is_array($col) ? $event[$col[0]][$col[1]] : $event[$col];
if (empty($sval))
continue;
// do a simple substring matching (to be improved)
$val = mb_strtolower($sval);
if (strpos($val, $search) !== false) {
$hit = true;
break;
}
}
if (!$hit) // skip this event if not match with search term
continue;
}
// list events in requested time window
if ($event['start'] <= $end && $event['end'] >= $start) {
unset($event['_attendees']);
$events[] = $event;
}
// resolve recurring events
if ($event['recurrence'] && $virtual == 1) {
unset($event['_attendees']);
$events = array_merge($events, $this->_get_recurring_events($event, $start, $end));
}
}
return $events;
}
/**
* Create a new event record
*
* @see calendar_driver::new_event()
*
* @return mixed The created record ID on success, False on error
*/
public function insert_event($event)
{
if (!is_array($event))
return false;
//generate new event from RC input
$object = $this->_from_rcube_event($event);
$saved = $this->storage->save($object);
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving event object to Kolab server:" . $saved->getMessage()),
true, false);
$saved = false;
}
else {
$event['id'] = $event['uid'];
$this->events[$event['uid']] = $event;
}
return $saved;
}
/**
* Update a specific event record
*
* @see calendar_driver::new_event()
* @return boolean True on success, False on error
*/
public function update_event($event)
{
$updated = false;
$old = $this->storage->getObject($event['id']);
if (PEAR::isError($old))
return false;
$old['recurrence'] = ''; # clear old field, could have been removed in new, too
$object = array_merge($old, $this->_from_rcube_event($event));
$saved = $this->storage->save($object, $event['id']);
if (PEAR::isError($saved)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving event object to Kolab server:" . $saved->getMessage()),
true, false);
}
else {
$updated = true;
$this->events[$event['id']] = $this->_to_rcube_event($object);
}
return $updated;
}
/**
* Delete an event record
*
* @see calendar_driver::remove_event()
* @return boolean True on success, False on error
*/
public function delete_event($event, $force = true)
{
$deleted = false;
if (!$force) {
// Get IMAP object ID
$imap_uid = $this->storage->_getStorageId($event['id']);
}
$deleteme = $this->storage->delete($event['id'], $force);
if (PEAR::isError($deleteme)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error deleting event object from Kolab server:" . $deleteme->getMessage()),
true, false);
}
else {
// Save IMAP object ID in session, will be used for restore action
if ($imap_uid)
$_SESSION['kolab_delete_uids'][$event['id']] = $imap_uid;
$deleted = true;
}
return $deleted;
}
/**
* Restore deleted event record
*
* @see calendar_driver::undelete_event()
* @return boolean True on success, False on error
*/
public function restore_event($event)
{
$imap_uid = $_SESSION['kolab_delete_uids'][$event['id']];
if (!$imap_uid)
return false;
$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($imap_uid);
if (is_object($result) && is_a($result, 'PEAR_Error')) {
$error = $result;
}
else {
// re-sync the cache
$this->storage->synchronize();
}
}
}
if ($error) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error undeleting an event object(s) from the Kolab server:" . $error->getMessage()),
true, false);
return false;
}
$rcmail = rcmail::get_instance();
$rcmail->session->remove('kolab_delete_uids');
return true;
}
/**
* Simply fetch all records and store them in private member vars
* We thereby rely on cahcing done by the Horde classes
*/
private function _fetch_events()
{
if (!isset($this->events)) {
$this->events = array();
foreach ((array)$this->storage->getObjects() as $record) {
$event = $this->_to_rcube_event($record);
$this->events[$event['id']] = $event;
}
}
}
/**
* Create instances of a recurring event
*/
public function _get_recurring_events($event, $start, $end, $event_id = null)
{
// include library class
require_once($this->cal->home . '/lib/calendar_recurrence.php');
$recurrence = new calendar_recurrence($this->cal, $event);
$events = array();
$duration = $event['end'] - $event['start'];
$i = 0;
while ($rec_start = $recurrence->next_start()) {
$rec_end = $rec_start + $duration;
$rec_id = $event['id'] . '-' . ++$i;
// add to output if in range
if (($rec_start <= $end && $rec_end >= $start) || ($event_id && $rec_id == $event_id)) {
$rec_event = $event;
$rec_event['id'] = $rec_id;
$rec_event['recurrence_id'] = $event['id'];
$rec_event['start'] = $rec_start;
$rec_event['end'] = $rec_end;
$rec_event['_instance'] = $i;
$events[] = $rec_event;
if ($rec_id == $event_id) {
$this->events[$rec_id] = $rec_event;
break;
}
}
else if ($rec_start > $end) // stop loop if out of range
break;
}
return $events;
}
/**
* Convert from Kolab_Format to internal representation
*/
private function _to_rcube_event($rec)
{
$start_time = date('H:i:s', $rec['start-date']);
$allday = $rec['_is_all_day'] || ($start_time == '00:00:00' && $start_time == date('H:i:s', $rec['end-date']));
if ($allday) { // in Roundcube all-day events only go from 12:00 to 13:00
$rec['start-date'] += 12 * 3600;
$rec['end-date'] -= 11 * 3600;
$rec['end-date'] -= $this->cal->gmt_offset - date('Z', $rec['end-date']); // shift times from server's timezone to user's timezone
$rec['start-date'] -= $this->cal->gmt_offset - date('Z', $rec['start-date']); // because generated with mktime() in Horde_Kolab_Format_Date::decodeDate()
// sanity check
if ($rec['end-date'] <= $rec['start-date'])
$rec['end-date'] += 86400;
}
// convert alarm time into internal format
if ($rec['alarm']) {
$alarm_value = $rec['alarm'];
$alarm_unit = 'M';
if ($rec['alarm'] % 1440 == 0) {
$alarm_value /= 1440;
$alarm_unit = 'D';
}
else if ($rec['alarm'] % 60 == 0) {
$alarm_value /= 60;
$alarm_unit = 'H';
}
$alarm_value *= -1;
}
// convert recurrence rules into internal pseudo-vcalendar format
if ($recurrence = $rec['recurrence']) {
$rrule = array(
'FREQ' => strtoupper($recurrence['cycle']),
'INTERVAL' => intval($recurrence['interval']),
);
if ($recurrence['range-type'] == 'number')
$rrule['COUNT'] = intval($recurrence['range']);
else if ($recurrence['range-type'] == 'date')
$rrule['UNTIL'] = $recurrence['range'];
if ($recurrence['day']) {
$byday = array();
$prefix = ($rrule['FREQ'] == 'MONTHLY' || $rrule['FREQ'] == 'YEARLY') ? intval($recurrence['daynumber'] ? $recurrence['daynumber'] : 1) : '';
foreach ($recurrence['day'] as $day)
$byday[] = $prefix . substr(strtoupper($day), 0, 2);
$rrule['BYDAY'] = join(',', $byday);
}
if ($recurrence['daynumber']) {
if ($recurrence['type'] == 'monthday' || $recurrence['type'] == 'daynumber')
$rrule['BYMONTHDAY'] = $recurrence['daynumber'];
else if ($recurrence['type'] == 'yearday')
$rrule['BYYEARDAY'] = $recurrence['daynumber'];
}
if ($recurrence['month']) {
$monthmap = array_flip($this->month_map);
$rrule['BYMONTH'] = strtolower($monthmap[$recurrence['month']]);
}
if ($recurrence['exclusion']) {
foreach ((array)$recurrence['exclusion'] as $excl)
$rrule['EXDATE'][] = strtotime($excl . date(' H:i:s', $rec['start-date'])); // use time of event start
}
}
$sensitivity_map = array_flip($this->sensitivity_map);
$status_map = array_flip($this->status_map);
$role_map = array_flip($this->role_map);
if (!empty($rec['_attachments'])) {
foreach ($rec['_attachments'] as $name => $attachment) {
// @TODO: 'type' and 'key' are the only supported (no 'size')
$attachments[] = array(
'id' => $attachment['key'],
'mimetype' => $attachment['type'],
'name' => $name,
);
}
}
if ($rec['organizer']) {
$attendees[] = array(
'role' => 'ORGANIZER',
'name' => $rec['organizer']['display-name'],
'email' => $rec['organizer']['smtp-address'],
'status' => 'ACCEPTED',
);
$_attendees .= $rec['organizer']['display-name'] . ' ' . $rec['organizer']['smtp-address'] . ' ';
}
foreach ((array)$rec['attendee'] as $attendee) {
$attendees[] = array(
'role' => $role_map[$attendee['role']],
'name' => $attendee['display-name'],
'email' => $attendee['smtp-address'],
'status' => $status_map[$attendee['status']],
'rsvp' => $attendee['request-response'],
);
$_attendees .= $rec['organizer']['display-name'] . ' ' . $rec['organizer']['smtp-address'] . ' ';
}
// Roundcube only supports one category assignment
$categories = explode(',', $rec['categories']);
return array(
'id' => $rec['uid'],
'uid' => $rec['uid'],
'title' => $rec['summary'],
'location' => $rec['location'],
'description' => $rec['body'],
'start' => $rec['start-date'],
'end' => $rec['end-date'],
'allday' => $allday,
'recurrence' => $rrule,
'alarms' => $alarm_value . $alarm_unit,
'_alarm' => intval($rec['alarm']),
'categories' => $categories[0],
'attachments' => $attachments,
'attendees' => $attendees,
'_attendees' => $_attendees,
'free_busy' => $rec['show-time-as'],
'priority' => is_numeric($rec['priority']) ? intval($rec['priority']) : (isset($this->priority_map[$rec['priority']]) ? $this->priority_map[$rec['priority']] : 0),
'sensitivity' => $sensitivity_map[$rec['sensitivity']],
'changed' => $rec['last-modification-date'],
'calendar' => $this->id,
);
}
/**
* Convert the given event record into a data structure that can be passed to Kolab_Storage backend for saving
* (opposite of self::_to_rcube_event())
*/
private function _from_rcube_event($event)
{
$priority_map = $this->priority_map;
$tz_offset = $this->cal->gmt_offset;
$object = array(
// kolab => roundcube
'uid' => $event['uid'],
'summary' => $event['title'],
'location' => $event['location'],
'body' => $event['description'],
'categories' => $event['categories'],
'start-date' => $event['start'],
'end-date' => $event['end'],
'sensitivity' =>$this->sensitivity_map[$event['sensitivity']],
'show-time-as' => $event['free_busy'],
'priority' => $event['priority'],
);
//handle alarms
if ($event['alarms']) {
//get the value
$alarmbase = explode(":", $event['alarms']);
//get number only
$avalue = preg_replace('/[^0-9]/', '', $alarmbase[0]);
if (preg_match("/H/",$alarmbase[0])) {
$object['alarm'] = $avalue*60;
} else if (preg_match("/D/",$alarmbase[0])) {
$object['alarm'] = $avalue*24*60;
} else {
$object['alarm'] = $avalue;
}
}
//recurr object/array
if (count($event['recurrence']) > 1) {
$ra = $event['recurrence'];
//Frequency abd interval
$object['recurrence']['cycle'] = strtolower($ra['FREQ']);
$object['recurrence']['interval'] = intval($ra['INTERVAL']);
//Range Type
if ($ra['UNTIL']) {
$object['recurrence']['range-type'] = 'date';
$object['recurrence']['range'] = $ra['UNTIL'];
}
if ($ra['COUNT']) {
$object['recurrence']['range-type'] = 'number';
$object['recurrence']['range'] = $ra['COUNT'];
}
//weekly
if ($ra['FREQ'] == 'WEEKLY') {
if ($ra['BYDAY']) {
foreach (split(",", $ra['BYDAY']) as $day)
$object['recurrence']['day'][] = $this->weekday_map[$day];
}
else {
// use weekday of start date if empty
$object['recurrence']['day'][] = strtolower(gmdate('l', $event['start'] + $tz_offset));
}
}
//monthly (temporary hack to follow current Horde logic)
if ($ra['FREQ'] == 'MONTHLY') {
if ($ra['BYDAY'] && preg_match('/(-?[1-4])([A-Z]+)/', $ra['BYDAY'], $m)) {
$object['recurrence']['daynumber'] = $m[1];
$object['recurrence']['day'] = array($this->weekday_map[$m[2]]);
$object['recurrence']['cycle'] = 'monthly';
$object['recurrence']['type'] = 'weekday';
}
else {
$object['recurrence']['daynumber'] = date('j', $event['start']);
$object['recurrence']['cycle'] = 'monthly';
$object['recurrence']['type'] = 'daynumber';
}
}
//yearly
if ($ra['FREQ'] == 'YEARLY') {
if (!$ra['BYMONTH'])
$ra['BYMONTH'] = gmdate('n', $event['start'] + $tz_offset);
$object['recurrence']['cycle'] = 'yearly';
$object['recurrence']['month'] = $this->month_map[intval($ra['BYMONTH'])];
if ($ra['BYDAY'] && preg_match('/(-?[1-4])([A-Z]+)/', $ra['BYDAY'], $m)) {
$object['recurrence']['type'] = 'weekday';
$object['recurrence']['daynumber'] = $m[1];
$object['recurrence']['day'] = array($this->weekday_map[$m[2]]);
}
else {
$object['recurrence']['type'] = 'monthday';
$object['recurrence']['daynumber'] = gmdate('j', $event['start'] + $tz_offset);
}
}
//exclusions
foreach ((array)$ra['EXDATE'] as $excl) {
$object['recurrence']['exclusion'][] = gmdate('Y-m-d', $excl + $tz_offset);
}
}
// whole day event
if ($event['allday']) {
$object['end-date'] += 12 * 3600; // end is at 13:00 => jump to the next day
$object['end-date'] += $tz_offset - date('Z'); // shift 00 times from user's timezone to server's timezone
$object['start-date'] += $tz_offset - date('Z'); // because Horde_Kolab_Format_Date::encodeDate() uses strftime()
// create timestamps at exactly 00:00. This is also needed for proper re-interpretation in _to_rcube_event() after updating an event
$object['start-date'] = mktime(0,0,0, date('n', $object['start-date']), date('j', $object['start-date']), date('Y', $object['start-date']));
$object['end-date'] = mktime(0,0,0, date('n', $object['end-date']), date('j', $object['end-date']), date('Y', $object['end-date']));
// sanity check: end date is same or smaller than start
if (date('Y-m-d', $object['end-date']) <= date('Y-m-d', $object['start-date']))
$object['end-date'] = mktime(13,0,0, date('n', $object['start-date']), date('j', $object['start-date']), date('Y', $object['start-date'])) + 86400;
$object['_is_all_day'] = 1;
}
// in Horde attachments are indexed by name
$object['_attachments'] = array();
if (!empty($event['attachments'])) {
$collisions = array();
foreach ($event['attachments'] as $idx => $attachment) {
// Roundcube ID has nothing to do with Horde ID, remove it
if ($attachment['content'])
unset($attachment['id']);
// Horde code assumes that there will be no more than
// one file with the same name: make filenames unique
$filename = $attachment['name'];
if ($collisions[$filename]++) {
$ext = preg_match('/(\.[a-z0-9]{1,6})$/i', $filename, $m) ? $m[1] : null;
$attachment['name'] = basename($filename, $ext) . '-' . $collisions[$filename] . $ext;
}
// set type parameter
if ($attachment['mimetype'])
$attachment['type'] = $attachment['mimetype'];
$object['_attachments'][$attachment['name']] = $attachment;
unset($event['attachments'][$idx]);
}
}
// process event attendees
foreach ((array)$event['attendees'] as $attendee) {
$role = $attendee['role'];
if ($role == 'ORGANIZER') {
$object['organizer'] = array(
'display-name' => $attendee['name'],
'smtp-address' => $attendee['email'],
);
}
else {
$object['attendee'][] = array(
'display-name' => $attendee['name'],
'smtp-address' => $attendee['email'],
'status' => $this->status_map[$attendee['status']],
'role' => $this->role_map[$role],
'request-response' => $attendee['rsvp'],
);
}
}
return $object;
}
}
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index 763dbbae..0617800a 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -1,1228 +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');
+ $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
- $this->rc->imap_connect();
- $this->rc->imap->subscribe($folder);
+ $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']])) {
- $this->rc->imap_connect();
+ $storage = $this->rc->get_storage();
if ($prop['active'])
- return $this->rc->imap->subscribe($cal->get_realname());
+ return $storage->subscribe($cal->get_realname());
else
- return $this->rc->imap->unsubscribe($cal->get_realname());
+ 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');
+ $folder = rcube_charset::convert($prop['name'], RCMAIL_CHARSET, 'UTF7-IMAP');
$oldfolder = $prop['oldname']; // UTF7
$parent = $prop['parent']; // UTF7
- $delimiter = $_SESSION['imap_delimiter'];
+ $storage = $this->rc->get_storage();
+ $delimiter = $storage->get_hierarchy_delimiter();
if (strlen($oldfolder)) {
- $this->rc->imap_connect();
- $options = $this->rc->imap->mailbox_info($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)
- $this->rc->imap_init();
- $folder = $this->rc->imap->mod_mailbox($folder, 'in');
+ $folder = $storage->mod_folder($folder, 'in');
}
// Check access rights to the parent folder
if (strlen($parent) && (!strlen($oldfolder) || $oldfolder != $folder)) {
- $this->rc->imap_connect();
- $parent_opts = $this->rc->imap->mailbox_info($parent);
+ $parent_opts = $storage->folder_info($parent);
if ($parent_opts['namespace'] != 'personal'
&& (empty($parent_opts['rights']) || !preg_match('/[ck]/', implode($parent_opts['rights'])))
) {
$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 = $this->rc->imap->set_metadata($folder, array('/shared/vendor/kolab/color' => $prop['color'])))) // try in shared namespace
- $meta_saved = $this->rc->imap->set_metadata($folder, array('/private/vendor/kolab/color' => $prop['color'])); // try in private namespace
+ 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);
- $delim = $_SESSION['imap_delimiter'];
+ $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);
- $this->rc->imap_connect();
- $options = $this->rc->imap->mailbox_info($folder);
+ $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);
- $delim = $_SESSION['imap_delimiter'];
+ $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);
- $this->rc->imap_connect();
- $options = $this->rc->imap->mailbox_info($folder);
+ $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_ical.php b/plugins/calendar/lib/calendar_ical.php
index 9a536722..c99fab35 100644
--- a/plugins/calendar/lib/calendar_ical.php
+++ b/plugins/calendar/lib/calendar_ical.php
@@ -1,463 +1,461 @@
<?php
/**
* iCalendar functions for the Calendar plugin
*
* @version @package_version@
* @author Lazlo Westerhof <hello@lazlo.me>
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Bogomil "Bogo" Shopov <shopov@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 to parse and build vCalendar (iCalendar) files
*
* Uses the Horde:iCalendar class for parsing. To install:
* > pear channel-discover pear.horde.org
* > pear install horde/Horde_Icalendar
*
*/
class calendar_ical
{
const EOL = "\r\n";
private $rc;
private $cal;
- private $timezone = 'Z';
-
+
public $method;
public $events = array();
function __construct($cal)
{
$this->cal = $cal;
$this->rc = $cal->rc;
-
- // compose timezone string
- if ($cal->timezone) {
- $hours = floor($cal->timezone + $cal->dst_active);
- $this->timezone = sprintf('%s%02d:%02d', ($hours >= 0 ? '+' : ''), $hours, ($cal->timezone - $hours) * 60);
- }
}
/**
* Import events from iCalendar format
*
* @param string vCalendar input
* @param string Input charset (from envelope)
* @return array List of events extracted from the input
*/
public function import($vcal, $charset = RCMAIL_CHARSET)
{
$parser = $this->get_parser();
$parser->parsevCalendar($vcal, 'VCALENDAR', $charset);
$this->method = $parser->getAttributeDefault('METHOD', '');
$this->events = $seen = array();
if ($data = $parser->getComponents()) {
foreach ($data as $comp) {
if ($comp->getType() == 'vEvent') {
$event = $this->_to_rcube_format($comp);
if (!$seen[$event['uid']]++)
$this->events[] = $event;
}
}
}
return $this->events;
}
/**
* Read iCalendar events from a file
*
* @param string File path to read from
* @return array List of events extracted from the file
*/
public function import_from_file($filepath)
{
$this->events = $seen = array();
$fp = fopen($filepath, 'r');
// check file content first
$begin = fread($fp, 1024);
if (!preg_match('/BEGIN:VCALENDAR/i', $begin))
return $this->events;
$parser = $this->get_parser();
$buffer = '';
fseek($fp, 0);
while (($line = fgets($fp, 2048)) !== false) {
$buffer .= $line;
if (preg_match('/END:VEVENT/i', $line)) {
$parser->parsevCalendar($buffer, 'VCALENDAR', RCMAIL_CHARSET, false);
$buffer = '';
}
}
fclose($fp);
if ($data = $parser->getComponents()) {
foreach ($data as $comp) {
if ($comp->getType() == 'vEvent') {
$event = $this->_to_rcube_format($comp);
if (!$seen[$event['uid']]++)
$this->events[] = $event;
}
}
}
return $this->events;
}
/**
* Load iCal parser from the Horde lib
*/
private function get_parser()
{
// use Horde:iCalendar to parse vcalendar file format
require_once 'Horde/iCalendar.php';
// set target charset for parsed events
$GLOBALS['_HORDE_STRING_CHARSET'] = RCMAIL_CHARSET;
return new Horde_iCalendar;
}
/**
* Convert the given File_IMC_Parse_Vcalendar_Event object to the internal event format
*/
private function _to_rcube_format($ve)
{
$event = array(
'uid' => $ve->getAttributeDefault('UID'),
'changed' => $ve->getAttributeDefault('DTSTAMP', 0),
'title' => $ve->getAttributeDefault('SUMMARY'),
'start' => $ve->getAttribute('DTSTART'),
'end' => $ve->getAttribute('DTEND'),
// set defaults
'free_busy' => 'busy',
'priority' => 0,
);
// check for all-day dates
if (is_array($event['start'])) {
// create timestamp at 12:00 in user's timezone
$event['start'] = $this->_date2time($event['start']);
$event['allday'] = true;
}
if (is_array($event['end'])) {
$event['end'] = $this->_date2time($event['end']) - 23 * 3600;
}
// map other attributes to internal fields
$_attendees = array();
foreach ($ve->getAllAttributes() as $attr) {
switch ($attr['name']) {
case 'ORGANIZER':
$organizer = array(
'name' => $attr['params']['CN'],
'email' => preg_replace('/^mailto:/i', '', $attr['value']),
'role' => 'ORGANIZER',
'status' => 'ACCEPTED',
);
if (isset($_attendees[$organizer['email']])) {
$i = $_attendees[$organizer['email']];
$event['attendees'][$i]['role'] = $organizer['role'];
}
break;
case 'ATTENDEE':
$attendee = array(
'name' => $attr['params']['CN'],
'email' => preg_replace('/^mailto:/i', '', $attr['value']),
'role' => $attr['params']['ROLE'] ? $attr['params']['ROLE'] : 'REQ-PARTICIPANT',
'status' => $attr['params']['PARTSTAT'],
'rsvp' => $attr['params']['RSVP'] == 'TRUE',
);
if ($organizer && $organizer['email'] == $attendee['email'])
$attendee['role'] = 'ORGANIZER';
$event['attendees'][] = $attendee;
$_attendees[$attendee['email']] = count($event['attendees']) - 1;
break;
case 'TRANSP':
$event['free_busy'] = $attr['value'] == 'TRANSPARENT' ? 'free' : 'busy';
break;
case 'STATUS':
if ($attr['value'] == 'TENTATIVE')
$event['free_busy'] == 'tentative';
break;
case 'PRIORITY':
if (is_numeric($attr['value'])) {
$event['priority'] = $attr['value'];
}
break;
case 'RRULE':
// parse recurrence rule attributes
foreach (explode(';', $attr['value']) as $par) {
list($k, $v) = explode('=', $par);
$params[$k] = $v;
}
if ($params['UNTIL'])
$params['UNTIL'] = $ve->_parseDateTime($params['UNTIL']);
if (!$params['INTERVAL'])
$params['INTERVAL'] = 1;
$event['recurrence'] = $params;
break;
case 'EXDATE':
break;
case 'RECURRENCE-ID':
$event['recurrence_id'] = $this->_date2time($attr['value']);
break;
case 'SEQUENCE':
$event['sequence'] = intval($attr['value']);
break;
case 'DESCRIPTION':
case 'LOCATION':
$event[strtolower($attr['name'])] = $attr['value'];
break;
case 'CLASS':
case 'X-CALENDARSERVER-ACCESS':
$sensitivity_map = array('PUBLIC' => 0, 'PRIVATE' => 1, 'CONFIDENTIAL' => 2);
$event['sensitivity'] = $sensitivity_map[$attr['value']];
break;
case 'X-MICROSOFT-CDO-BUSYSTATUS':
if ($attr['value'] == 'OOF')
$event['free_busy'] == 'outofoffice';
else if (in_array($attr['value'], array('FREE', 'BUSY', 'TENTATIVE')))
$event['free_busy'] = strtolower($attr['value']);
break;
}
}
// find alarms
if ($valarm = $ve->findComponent('valarm')) {
$action = 'DISPLAY';
$trigger = null;
foreach ($valarm->getAllAttributes() as $attr) {
switch ($attr['name']) {
case 'TRIGGER':
if ($attr['params']['VALUE'] == 'DATE-TIME') {
$trigger = '@' . $attr['value'];
}
else {
$trigger = $attr['value'];
$offset = abs($trigger);
$unit = 'S';
if ($offset % 86400 == 0) {
$unit = 'D';
$trigger = intval($trigger / 86400);
}
else if ($offset % 3600 == 0) {
$unit = 'H';
$trigger = intval($trigger / 3600);
}
else if ($offset % 60 == 0) {
$unit = 'M';
$trigger = intval($trigger / 60);
}
}
break;
case 'ACTION':
$action = $attr['value'];
break;
}
}
if ($trigger)
$event['alarms'] = $trigger . $unit . ':' . $action;
}
// add organizer to attendees list if not already present
if ($organizer && !isset($_attendees[$organizer['email']]))
array_unshift($event['attendees'], $organizer);
// make sure the event has an UID
if (!$event['uid'])
$event['uid'] = $this->cal->$this->generate_uid();
return $event;
}
/**
* Helper method to correctly interpret an all-day date value
*/
private function _date2time($prop)
{
// create timestamp at 12:00 in user's timezone
- return is_array($prop) ? strtotime(sprintf('%04d%02d%02dT120000%s', $prop['year'], $prop['month'], $prop['mday'], $this->timezone)) : $prop;
+ if (is_array($prop)) {
+ $date = new DateTime(sprintf('%04d%02d%02dT120000', $prop['year'], $prop['month'], $prop['mday']), $this->cal->timezone);
+ console($prop, $date->format('r'));
+ return $date->getTimestamp();
+ }
+ return $prop;
}
/**
* Free resources by clearing member vars
*/
public function reset()
{
$this->method = '';
$this->events = array();
}
/**
* Export events to iCalendar format
*
* @param array Events as array
* @param string VCalendar method to advertise
* @param boolean Directly send data to stdout instead of returning
* @return string Events in iCalendar format (http://tools.ietf.org/html/rfc5545)
*/
public function export($events, $method = null, $write = false)
{
$ical = "BEGIN:VCALENDAR" . self::EOL;
$ical .= "VERSION:2.0" . self::EOL;
$ical .= "PRODID:-//Roundcube Webmail " . RCMAIL_VERSION . "//NONSGML Calendar//EN" . self::EOL;
$ical .= "CALSCALE:GREGORIAN" . self::EOL;
if ($method)
$ical .= "METHOD:" . strtoupper($method) . self::EOL;
if ($write) {
echo $ical;
$ical = '';
}
foreach ($events as $event) {
$vevent = "BEGIN:VEVENT" . self::EOL;
$vevent .= "UID:" . self::escpape($event['uid']) . self::EOL;
$vevent .= "DTSTAMP:" . gmdate('Ymd\THis\Z', $event['changed'] ? $event['changed'] : time()) . self::EOL;
// correctly set all-day dates
if ($event['allday']) {
$vevent .= "DTSTART;VALUE=DATE:" . gmdate('Ymd', $event['start'] + $this->cal->gmt_offset) . self::EOL;
$vevent .= "DTEND;VALUE=DATE:" . gmdate('Ymd', $event['end'] + $this->cal->gmt_offset + 86400) . self::EOL; // ends the next day
}
else {
$vevent .= "DTSTART:" . gmdate('Ymd\THis\Z', $event['start']) . self::EOL;
$vevent .= "DTEND:" . gmdate('Ymd\THis\Z', $event['end']) . self::EOL;
}
$vevent .= "SUMMARY:" . self::escpape($event['title']) . self::EOL;
$vevent .= "DESCRIPTION:" . self::escpape($event['description']) . self::EOL;
if (!empty($event['attendees'])){
$vevent .= $this->_get_attendees($event['attendees']);
}
if (!empty($event['location'])) {
$vevent .= "LOCATION:" . self::escpape($event['location']) . self::EOL;
}
if ($event['recurrence']) {
$vevent .= "RRULE:" . calendar::to_rrule($event['recurrence'], self::EOL) . self::EOL;
}
if(!empty($event['categories'])) {
$vevent .= "CATEGORIES:" . self::escpape(strtoupper($event['categories'])) . self::EOL;
}
if ($event['sensitivity'] > 0) {
$vevent .= "CLASS:" . ($event['sensitivity'] == 2 ? 'CONFIDENTIAL' : 'PRIVATE') . self::EOL;
}
if ($event['alarms']) {
list($trigger, $action) = explode(':', $event['alarms']);
$val = calendar::parse_alaram_value($trigger);
$vevent .= "BEGIN:VALARM\n";
if ($val[1]) $vevent .= "TRIGGER:" . preg_replace('/^([-+])(.+)/', '\\1PT\\2', $trigger) . self::EOL;
else $vevent .= "TRIGGER;VALUE=DATE-TIME:" . gmdate('Ymd\THis\Z', $val[0]) . self::EOL;
if ($action) $vevent .= "ACTION:" . self::escpape(strtoupper($action)) . self::EOL;
$vevent .= "END:VALARM\n";
}
$vevent .= "TRANSP:" . ($event['free_busy'] == 'free' ? 'TRANSPARENT' : 'OPAQUE') . self::EOL;
if ($event['priority']) {
$vevent .= "PRIORITY:" . $event['priority'] . self::EOL;
}
if ($event['cancelled'])
$vevent .= "STATUS:CANCELLED" . self::EOL;
else if ($event['free_busy'] == 'tentative')
$vevent .= "STATUS:TENTATIVE" . self::EOL;
// TODO: export attachments
$vevent .= "END:VEVENT" . self::EOL;
if ($write)
echo rcube_vcard::rfc2425_fold($vevent);
else
$ical .= $vevent;
}
$ical .= "END:VCALENDAR" . self::EOL;
if ($write) {
echo $ical;
return true;
}
// fold lines to 75 chars
return rcube_vcard::rfc2425_fold($ical);
}
private function escpape($str)
{
return preg_replace('/(?<!\\\\)([\:\;\,\\n\\r])/', '\\\$1', $str);
}
/**
* Construct the orginizer of the event.
* @param Array Attendees and roles
*
*/
private function _get_attendees($ats)
{
$organizer = "";
$attendees = "";
foreach ($ats as $at) {
if ($at['role']=="ORGANIZER") {
//I am an orginizer
$organizer .= "ORGANIZER;";
if (!empty($at['name']))
$organizer .= 'CN="' . $at['name'] . '"';
$organizer .= ":mailto:". $at['email'] . self::EOL;
}
else {
//I am an attendee
$attendees .= "ATTENDEE;ROLE=" . $at['role'] . ";PARTSTAT=" . $at['status'];
if ($at['rsvp'])
$attendees .= ";RSVP=TRUE";
if (!empty($at['name']))
$attendees .= ';CN="' . $at['name'] . '"';
$attendees .= ":mailto:" . $at['email'] . self::EOL;
}
}
return $organizer . $attendees;
}
}
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/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index a8c89800..35a5c9ce 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -1,816 +1,820 @@
<?php
/**
* User Interface class for the Calendar plugin
*
* @version @package_version@
* @author Lazlo Westerhof <hello@lazlo.me>
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
* Copyright (C) 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_ui
{
private $rc;
private $cal;
private $ready = false;
public $screen;
function __construct($cal)
{
$this->cal = $cal;
$this->rc = $cal->rc;
$this->screen = $this->rc->task == 'calendar' ? ($this->rc->action ? $this->rc->action: 'calendar') : 'other';
}
/**
* Calendar UI initialization and requests handlers
*/
public function init()
{
if ($this->ready) // already done
return;
// add taskbar button
$this->cal->add_button(array(
- 'name' => 'calendar',
+ 'command' => 'calendar',
'class' => 'button-calendar',
+ 'classsel' => 'button-calendar button-selected',
+ 'innerclass' => 'button-inner',
'label' => 'calendar.calendar',
- 'href' => './?_task=calendar',
- 'onclick' => sprintf("%s.command('plugin.calendar');return false", JS_OBJECT_NAME),
), 'taskbar');
// load basic client script (which - unfortunately - requires fullcalendar)
$this->cal->include_script('lib/js/fullcalendar.js');
$this->cal->include_script('calendar_base.js');
$skin = $this->rc->config->get('skin');
$this->cal->include_stylesheet('skins/' . $skin . '/calendar.css');
$this->ready = true;
}
/**
* Register handler methods for the template engine
*/
public function init_templates()
{
$this->cal->register_handler('plugin.calendar_css', array($this, 'calendar_css'));
$this->cal->register_handler('plugin.calendar_list', array($this, 'calendar_list'));
$this->cal->register_handler('plugin.calendar_select', array($this, 'calendar_select'));
$this->cal->register_handler('plugin.category_select', array($this, 'category_select'));
$this->cal->register_handler('plugin.freebusy_select', array($this, 'freebusy_select'));
$this->cal->register_handler('plugin.priority_select', array($this, 'priority_select'));
$this->cal->register_handler('plugin.sensitivity_select', array($this, 'sensitivity_select'));
$this->cal->register_handler('plugin.alarm_select', array($this, 'alarm_select'));
$this->cal->register_handler('plugin.snooze_select', array($this, 'snooze_select'));
$this->cal->register_handler('plugin.recurrence_form', array($this, 'recurrence_form'));
$this->cal->register_handler('plugin.attachments_form', array($this, 'attachments_form'));
$this->cal->register_handler('plugin.attachments_list', array($this, 'attachments_list'));
$this->cal->register_handler('plugin.attendees_list', array($this, 'attendees_list'));
$this->cal->register_handler('plugin.attendees_form', array($this, 'attendees_form'));
$this->cal->register_handler('plugin.attendees_freebusy_table', array($this, 'attendees_freebusy_table'));
$this->cal->register_handler('plugin.edit_attendees_notify', array($this, 'edit_attendees_notify'));
$this->cal->register_handler('plugin.edit_recurring_warning', array($this, 'recurring_event_warning'));
$this->cal->register_handler('plugin.event_rsvp_buttons', array($this, 'event_rsvp_buttons'));
$this->cal->register_handler('plugin.angenda_options', array($this, 'angenda_options'));
$this->cal->register_handler('plugin.events_import_form', array($this, 'events_import_form'));
$this->cal->register_handler('plugin.searchform', array($this->rc->output, 'search_form')); // use generic method from rcube_template
}
/**
* Adds CSS stylesheets to the page header
*/
public function addCSS()
{
$skin = $this->rc->config->get('skin');
$this->cal->include_stylesheet('skins/' . $skin . '/fullcalendar.css');
$this->cal->include_stylesheet('skins/' . $skin . '/jquery.miniColors.css');
}
/**
* Adds JS files to the page header
*/
public function addJS()
{
$this->cal->include_script('calendar_ui.js');
$this->cal->include_script('lib/js/jquery.miniColors.min.js');
}
/**
*
*/
function calendar_css($attrib = array())
{
$mode = $this->rc->config->get('calendar_event_coloring', $this->cal->defaults['calendar_event_coloring']);
$categories = $this->cal->driver->list_categories();
$css = "\n";
foreach ((array)$categories as $class => $color) {
if (empty($color))
continue;
$class = 'cat-' . asciiwords(strtolower($class), true);
$css .= ".$class { color: #$color }\n";
if ($mode > 0) {
if ($mode == 2) {
$css .= ".fc-event-$class .fc-event-bg {";
$css .= " opacity: 0.9;";
$css .= " filter: alpha(opacity=90);";
}
else {
$css .= ".fc-event-$class.fc-event-skin, ";
$css .= ".fc-event-$class .fc-event-skin, ";
$css .= ".fc-event-$class .fc-event-inner {";
}
$css .= " background-color: #" . $color . ";";
if ($mode % 2)
$css .= " border-color: #$color;";
$css .= "}\n";
}
}
$calendars = $this->cal->driver->list_calendars();
foreach ((array)$calendars as $id => $prop) {
if (!$prop['color'])
continue;
$color = $prop['color'];
$class = 'cal-' . asciiwords($id, true);
$css .= "li.$class, #eventshow .$class { color: #$color }\n";
if ($mode != 1) {
if ($mode == 3) {
$css .= ".fc-event-$class .fc-event-bg {";
$css .= " opacity: 0.9;";
$css .= " filter: alpha(opacity=90);";
}
else {
$css .= ".fc-event-$class, ";
$css .= ".fc-event-$class .fc-event-inner {";
}
if (!$attrib['printmode'])
$css .= " background-color: #$color;";
if ($mode % 2 == 0)
$css .= " border-color: #$color;";
$css .= "}\n";
}
+ $css .= ".$class .handle { background-color: #$color; }";
}
return html::tag('style', array('type' => 'text/css'), $css);
}
/**
*
*/
function calendar_list($attrib = array())
{
$calendars = $this->cal->driver->list_calendars();
$li = '';
foreach ((array)$calendars as $id => $prop) {
if ($attrib['activeonly'] && !$prop['active'])
continue;
unset($prop['user_id']);
$prop['alarms'] = $this->cal->driver->alarms;
$prop['attendees'] = $this->cal->driver->attendees;
$prop['freebusy'] = $this->cal->driver->freebusy;
$prop['attachments'] = $this->cal->driver->attachments;
$prop['undelete'] = $this->cal->driver->undelete;
+ $prop['feedurl'] = $this->cal->get_url(array('_cal' => $this->cal->ical_feed_hash($id) . '.ics', 'action' => 'feed'));
$jsenv[$id] = $prop;
$html_id = html_identifier($id);
$class = 'cal-' . asciiwords($id, true);
if ($prop['readonly'])
$class .= ' readonly';
if ($prop['class_name'])
$class .= ' '.$prop['class_name'];
$li .= html::tag('li', array('id' => 'rcmlical' . $html_id, 'class' => $class),
- html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') . html::span(null, Q($prop['name'])));
+ html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => $prop['active']), '') .
+ html::span('handle', '&nbsp;') .
+ html::span('calname', Q($prop['name'])));
}
$this->rc->output->set_env('calendars', $jsenv);
$this->rc->output->add_gui_object('folderlist', $attrib['id']);
return html::tag('ul', $attrib, $li, html::$common_attrib);
}
/**
*
*/
function angenda_options($attrib = array())
{
$attrib += array('id' => 'agendaoptions');
$attrib['style'] .= 'display:none';
$select_range = new html_select(array('name' => 'listrange', 'id' => 'agenda-listrange'));
$select_range->add(1 . ' ' . preg_replace('/\(.+\)/', '', $this->cal->gettext('days')), $days);
foreach (array(2,5,7,14,30,60,90) as $days)
$select_range->add($days . ' ' . preg_replace('/\(|\)/', '', $this->cal->gettext('days')), $days);
$html .= html::label('agenda-listrange', $this->cal->gettext('listrange'));
$html .= $select_range->show($this->rc->config->get('calendar_agenda_range', $this->cal->defaults['calendar_agenda_range']));
$select_sections = new html_select(array('name' => 'listsections', 'id' => 'agenda-listsections'));
$select_sections->add('---', '');
foreach (array('day' => 'days', 'week' => 'weeks', 'month' => 'months', 'smart' => 'smartsections') as $val => $label)
$select_sections->add(preg_replace('/\(|\)/', '', ucfirst($this->cal->gettext($label))), $val);
$html .= html::span('spacer', '&nbsp');
$html .= html::label('agenda-listsections', $this->cal->gettext('listsections'));
$html .= $select_sections->show($this->rc->config->get('calendar_agenda_sections', $this->cal->defaults['calendar_agenda_sections']));
return html::div($attrib, $html);
}
/**
* Render a HTML select box for calendar selection
*/
function calendar_select($attrib = array())
{
$attrib['name'] = 'calendar';
$select = new html_select($attrib);
foreach ((array)$this->cal->driver->list_calendars() as $id => $prop) {
if (!$prop['readonly'])
$select->add($prop['name'], $id);
}
return $select->show(null);
}
/**
* Render a HTML select box to select an event category
*/
function category_select($attrib = array())
{
$attrib['name'] = 'categories';
$select = new html_select($attrib);
$select->add('---', '');
foreach ((array)$this->cal->driver->list_categories() as $cat => $color) {
$select->add($cat, $cat);
}
return $select->show(null);
}
/**
* Render a HTML select box for free/busy/out-of-office property
*/
function freebusy_select($attrib = array())
{
$attrib['name'] = 'freebusy';
$select = new html_select($attrib);
$select->add($this->cal->gettext('free'), 'free');
$select->add($this->cal->gettext('busy'), 'busy');
$select->add($this->cal->gettext('outofoffice'), 'outofoffice');
$select->add($this->cal->gettext('tentative'), 'tentative');
return $select->show(null);
}
/**
* Render a HTML select for event priorities
*/
function priority_select($attrib = array())
{
$attrib['name'] = 'priority';
$select = new html_select($attrib);
$select->add('---', '0');
$select->add('1 '.$this->cal->gettext('highest'), '1');
$select->add('2 '.$this->cal->gettext('high'), '2');
$select->add('3 ', '3');
$select->add('4 ', '4');
$select->add('5 '.$this->cal->gettext('normal'), '5');
$select->add('6 ', '6');
$select->add('7 ', '7');
$select->add('8 '.$this->cal->gettext('low'), '8');
$select->add('9 '.$this->cal->gettext('lowest'), '9');
return $select->show(null);
}
/**
* Render HTML input for sensitivity selection
*/
function sensitivity_select($attrib = array())
{
$attrib['name'] = 'sensitivity';
$select = new html_select($attrib);
$select->add($this->cal->gettext('public'), '0');
$select->add($this->cal->gettext('private'), '1');
$select->add($this->cal->gettext('confidential'), '2');
return $select->show(null);
}
/**
* Render HTML form for alarm configuration
*/
function alarm_select($attrib = array())
{
unset($attrib['name']);
$select_type = new html_select(array('name' => 'alarmtype[]', 'class' => 'edit-alarm-type'));
$select_type->add($this->cal->gettext('none'), '');
foreach ($this->cal->driver->alarm_types as $type)
$select_type->add($this->cal->gettext(strtolower("alarm{$type}option")), $type);
$input_value = new html_inputfield(array('name' => 'alarmvalue[]', 'class' => 'edit-alarm-value', 'size' => 3));
$input_date = new html_inputfield(array('name' => 'alarmdate[]', 'class' => 'edit-alarm-date', 'size' => 10));
$input_time = new html_inputfield(array('name' => 'alarmtime[]', 'class' => 'edit-alarm-time', 'size' => 6));
$select_offset = new html_select(array('name' => 'alarmoffset[]', 'class' => 'edit-alarm-offset'));
foreach (array('-M','-H','-D','+M','+H','+D','@') as $trigger)
$select_offset->add($this->cal->gettext('trigger' . $trigger), $trigger);
// pre-set with default values from user settings
$preset = calendar::parse_alaram_value($this->rc->config->get('calendar_default_alarm_offset', '-15M'));
$hidden = array('style' => 'display:none');
$html = html::span('edit-alarm-set',
$select_type->show($this->rc->config->get('calendar_default_alarm_type', '')) . ' ' .
html::span(array('class' => 'edit-alarm-values', 'style' => 'display:none'),
$input_value->show($preset[0]) . ' ' .
$select_offset->show($preset[1]) . ' ' .
$input_date->show('', $hidden) . ' ' .
$input_time->show('', $hidden)
)
);
// TODO: support adding more alarms
#$html .= html::a(array('href' => '#', 'id' => 'edit-alam-add', 'title' => $this->cal->gettext('addalarm')),
# $attrib['addicon'] ? html::img(array('src' => $attrib['addicon'], 'alt' => 'add')) : '(+)');
return $html;
}
function snooze_select($attrib = array())
{
$steps = array(
5 => 'repeatinmin',
10 => 'repeatinmin',
15 => 'repeatinmin',
20 => 'repeatinmin',
30 => 'repeatinmin',
60 => 'repeatinhr',
120 => 'repeatinhrs',
1440 => 'repeattomorrow',
10080 => 'repeatinweek',
);
$items = array();
foreach ($steps as $n => $label) {
$items[] = html::tag('li', null, html::a(array('href' => "#" . ($n * 60), 'class' => 'active'),
$this->cal->gettext(array('name' => $label, 'vars' => array('min' => $n % 60, 'hrs' => intval($n / 60))))));
}
return html::tag('ul', $attrib, join("\n", $items), html::$common_attrib);
}
/**
*
*/
function edit_attendees_notify($attrib = array())
{
$checkbox = new html_checkbox(array('name' => '_notify', 'id' => 'edit-attendees-donotify', 'value' => 1));
return html::div($attrib, html::label(null, $checkbox->show(1) . ' ' . $this->cal->gettext('sendnotifications')));
}
/**
* Generate the form for recurrence settings
*/
function recurring_event_warning($attrib = array())
{
$attrib['id'] = 'edit-recurring-warning';
$radio = new html_radiobutton(array('name' => '_savemode', 'class' => 'edit-recurring-savemode'));
$form = html::label(null, $radio->show('', array('value' => 'current')) . $this->cal->gettext('currentevent')) . ' ' .
html::label(null, $radio->show('', array('value' => 'future')) . $this->cal->gettext('futurevents')) . ' ' .
html::label(null, $radio->show('all', array('value' => 'all')) . $this->cal->gettext('allevents')) . ' ' .
html::label(null, $radio->show('', array('value' => 'new')) . $this->cal->gettext('saveasnew'));
return html::div($attrib, html::div('message', html::span('ui-icon ui-icon-alert', '') . $this->cal->gettext('changerecurringeventwarning')) . html::div('savemode', $form));
}
/**
* Generate the form for recurrence settings
*/
function recurrence_form($attrib = array())
{
switch ($attrib['part']) {
// frequency selector
case 'frequency':
$select = new html_select(array('name' => 'frequency', 'id' => 'edit-recurrence-frequency'));
$select->add($this->cal->gettext('never'), '');
$select->add($this->cal->gettext('daily'), 'DAILY');
$select->add($this->cal->gettext('weekly'), 'WEEKLY');
$select->add($this->cal->gettext('monthly'), 'MONTHLY');
$select->add($this->cal->gettext('yearly'), 'YEARLY');
$html = html::label('edit-frequency', $this->cal->gettext('frequency')) . $select->show('');
break;
// daily recurrence
case 'daily':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-daily'));
$html = html::div($attrib, html::label(null, $this->cal->gettext('every')) . $select->show(1) . html::span('label-after', $this->cal->gettext('days')));
break;
// weekly recurrence form
case 'weekly':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-weekly'));
$html = html::div($attrib, html::label(null, $this->cal->gettext('every')) . $select->show(1) . html::span('label-after', $this->cal->gettext('weeks')));
// weekday selection
$daymap = array('sun','mon','tue','wed','thu','fri','sat');
$checkbox = new html_checkbox(array('name' => 'byday', 'class' => 'edit-recurrence-weekly-byday'));
$first = $this->rc->config->get('calendar_first_day', 1);
for ($weekdays = '', $j = $first; $j <= $first+6; $j++) {
$d = $j % 7;
$weekdays .= html::label(array('class' => 'weekday'), $checkbox->show('', array('value' => strtoupper(substr($daymap[$d], 0, 2)))) . $this->cal->gettext($daymap[$d])) . ' ';
}
$html .= html::div($attrib, html::label(null, $this->cal->gettext('bydays')) . $weekdays);
break;
// monthly recurrence form
case 'monthly':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-monthly'));
$html = html::div($attrib, html::label(null, $this->cal->gettext('every')) . $select->show(1) . html::span('label-after', $this->cal->gettext('months')));
/* multiple month selection is not supported by Kolab
$checkbox = new html_radiobutton(array('name' => 'bymonthday', 'class' => 'edit-recurrence-monthly-bymonthday'));
for ($monthdays = '', $d = 1; $d <= 31; $d++) {
$monthdays .= html::label(array('class' => 'monthday'), $checkbox->show('', array('value' => $d)) . $d);
$monthdays .= $d % 7 ? ' ' : html::br();
}
*/
// rule selectors
$radio = new html_radiobutton(array('name' => 'repeatmode', 'class' => 'edit-recurrence-monthly-mode'));
$table = new html_table(array('cols' => 2, 'border' => 0, 'cellpadding' => 0, 'class' => 'formtable'));
$table->add('label', html::label(null, $radio->show('BYMONTHDAY', array('value' => 'BYMONTHDAY')) . ' ' . $this->cal->gettext('onsamedate'))); // $this->cal->gettext('each')
$table->add(null, $monthdays);
$table->add('label', html::label(null, $radio->show('', array('value' => 'BYDAY')) . ' ' . $this->cal->gettext('onevery')));
$table->add(null, $this->rrule_selectors($attrib['part']));
$html .= html::div($attrib, $table->show());
break;
// annually recurrence form
case 'yearly':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-yearly'));
$html = html::div($attrib, html::label(null, $this->cal->gettext('every')) . $select->show(1) . html::span('label-after', $this->cal->gettext('years')));
// month selector
$monthmap = array('','jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec');
$boxtype = is_a($this->cal->driver, 'kolab_driver') ? 'radio' : 'checkbox';
$checkbox = new html_inputfield(array('type' => $boxtype, 'name' => 'bymonth', 'class' => 'edit-recurrence-yearly-bymonth'));
for ($months = '', $m = 1; $m <= 12; $m++) {
$months .= html::label(array('class' => 'month'), $checkbox->show(null, array('value' => $m)) . $this->cal->gettext($monthmap[$m]));
$months .= $m % 4 ? ' ' : html::br();
}
$html .= html::div($attrib + array('id' => 'edit-recurrence-yearly-bymonthblock'), $months);
// day rule selection
$html .= html::div($attrib, html::label(null, $this->cal->gettext('onevery')) . $this->rrule_selectors($attrib['part'], '---'));
break;
// end of recurrence form
case 'until':
$radio = new html_radiobutton(array('name' => 'repeat', 'class' => 'edit-recurrence-until'));
$select = $this->interval_selector(array('name' => 'times', 'id' => 'edit-recurrence-repeat-times'));
$input = new html_inputfield(array('name' => 'untildate', 'id' => 'edit-recurrence-enddate', 'size' => "10"));
$table = new html_table(array('cols' => 2, 'border' => 0, 'cellpadding' => 0, 'class' => 'formtable'));
$table->add('label', ucfirst($this->cal->gettext('recurrencend')));
$table->add(null, html::label(null, $radio->show('', array('value' => '', 'id' => 'edit-recurrence-repeat-forever')) . ' ' .
$this->cal->gettext('forever')));
$table->add('label', '');
$table->add(null, $radio->show('', array('value' => 'count', 'id' => 'edit-recurrence-repeat-count')) . ' ' .
$this->cal->gettext(array(
'name' => 'forntimes',
'vars' => array('nr' => $select->show(1)))
));
$table->add('label', '');
$table->add(null, $radio->show('', array('value' => 'until', 'id' => 'edit-recurrence-repeat-until')) . ' ' .
$this->cal->gettext('untildate') . ' ' . $input->show(''));
$html = $table->show();
break;
}
return $html;
}
/**
* Input field for interval selection
*/
private function interval_selector($attrib)
{
$select = new html_select($attrib);
$select->add(range(1,30), range(1,30));
return $select;
}
/**
* Drop-down menus for recurrence rules like "each last sunday of"
*/
private function rrule_selectors($part, $noselect = null)
{
// rule selectors
$select_prefix = new html_select(array('name' => 'bydayprefix', 'id' => "edit-recurrence-$part-prefix"));
if ($noselect) $select_prefix->add($noselect, '');
$select_prefix->add(array(
$this->cal->gettext('first'),
$this->cal->gettext('second'),
$this->cal->gettext('third'),
$this->cal->gettext('fourth')
),
array(1, 2, 3, 4));
// Kolab doesn't support 'last' but others do.
if (!is_a($this->cal->driver, 'kolab_driver'))
$select_prefix->add($this->cal->gettext('last'), -1);
$select_wday = new html_select(array('name' => 'byday', 'id' => "edit-recurrence-$part-byday"));
if ($noselect) $select_wday->add($noselect, '');
$daymap = array('sunday','monday','tuesday','wednesday','thursday','friday','saturday');
$first = $this->rc->config->get('calendar_first_day', 1);
for ($j = $first; $j <= $first+6; $j++) {
$d = $j % 7;
$select_wday->add($this->cal->gettext($daymap[$d]), strtoupper(substr($daymap[$d], 0, 2)));
}
if ($part == 'monthly')
$select_wday->add($this->cal->gettext('dayofmonth'), '');
return $select_prefix->show() . '&nbsp;' . $select_wday->show();
}
/**
* Form for uploading and importing events
*/
function events_import_form($attrib = array())
{
if (!$attrib['id'])
$attrib['id'] = 'rcmImportForm';
// Get max filesize, enable upload progress bar
$max_filesize = rcube_upload_init();
$button = new html_inputfield(array('type' => 'button'));
$input = new html_inputfield(array(
'type' => 'file', 'name' => '_data', 'size' => $attrib['uploadfieldsize']));
$select = new html_select(array('name' => '_range', 'id' => 'event-import-range'));
$select->add(array(
$this->cal->gettext('onemonthback'),
$this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>2))),
$this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>6))),
$this->cal->gettext(array('name' => 'nmonthsback', 'vars' => array('nr'=>12))),
$this->cal->gettext('all'),
),
array('1','2','6','12',0));
$html .= html::div('form-section',
html::div(null, $input->show()) .
html::div('hint', rcube_label(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize))))
);
$html .= html::div('form-section',
html::label('event-import-calendar', $this->cal->gettext('calendar')) .
$this->calendar_select(array('name' => 'calendar', 'id' => 'event-import-calendar'))
);
$html .= html::div('form-section',
html::label('event-import-range', $this->cal->gettext('importrange')) .
$select->show(1)
);
$this->rc->output->add_gui_object('importform', $attrib['id']);
$this->rc->output->add_label('import');
return html::tag('form', array('action' => $this->rc->url(array('task' => 'calendar', 'action' => 'import_events')),
'method' => "post", 'enctype' => 'multipart/form-data', 'id' => $attrib['id']),
$html
);
}
/**
* Generate the form for event attachments upload
*/
function attachments_form($attrib = array())
{
// add ID if not given
if (!$attrib['id'])
$attrib['id'] = 'rcmUploadForm';
// Get max filesize, enable upload progress bar
$max_filesize = rcube_upload_init();
$button = new html_inputfield(array('type' => 'button'));
$input = new html_inputfield(array(
'type' => 'file', 'name' => '_attachments[]',
'multiple' => 'multiple', 'size' => $attrib['attachmentfieldsize']));
return html::div($attrib,
html::div(null, $input->show()) .
html::div('buttons', $button->show(rcube_label('upload'), array('class' => 'button mainaction',
'onclick' => JS_OBJECT_NAME . ".upload_file(this.form)"))) .
html::div('hint', rcube_label(array('name' => 'maxuploadsize', 'vars' => array('size' => $max_filesize))))
);
}
/**
* Generate HTML element for attachments list
*/
function attachments_list($attrib = array())
{
if (!$attrib['id'])
$attrib['id'] = 'rcmAttachmentList';
$skin_path = $this->rc->config->get('skin_path');
if ($attrib['deleteicon']) {
$_SESSION['calendar_deleteicon'] = $skin_path . $attrib['deleteicon'];
$this->rc->output->set_env('deleteicon', $skin_path . $attrib['deleteicon']);
}
if ($attrib['cancelicon'])
$this->rc->output->set_env('cancelicon', $skin_path . $attrib['cancelicon']);
if ($attrib['loadingicon'])
$this->rc->output->set_env('loadingicon', $skin_path . $attrib['loadingicon']);
$this->rc->output->add_gui_object('attachmentlist', $attrib['id']);
return html::tag('ul', $attrib, '', html::$common_attrib);
}
function attachment_controls($attrib = array())
{
$table = new html_table(array('cols' => 3));
if (!empty($this->cal->attachment['name'])) {
$table->add('title', Q(rcube_label('filename')));
$table->add(null, Q($this->cal->attachment['name']));
$table->add(null, '[' . html::a('?'.str_replace('_frame=', '_download=', $_SERVER['QUERY_STRING']), Q(rcube_label('download'))) . ']');
}
if (!empty($this->cal->attachment['size'])) {
$table->add('title', Q(rcube_label('filesize')));
$table->add(null, Q(show_bytes($this->cal->attachment['size'])));
}
return $table->show($attrib);
}
/**
* Handler for calendar form template.
* The form content could be overriden by the driver
*/
function calendar_editform($action, $calendar = array())
{
// compose default calendar form fields
$input_name = new html_inputfield(array('name' => 'name', 'id' => 'calendar-name', 'size' => 20));
$input_color = new html_inputfield(array('name' => 'color', 'id' => 'calendar-color', 'size' => 6));
$formfields = array(
'name' => array(
'label' => $this->cal->gettext('name'),
'value' => $input_name->show($name),
'id' => 'calendar-name',
),
'color' => array(
'label' => $this->cal->gettext('color'),
'value' => $input_color->show($calendar['color']),
'id' => 'calendar-color',
),
);
if ($this->cal->driver->alarms) {
$checkbox = new html_checkbox(array('name' => 'showalarms', 'id' => 'calendar-showalarms', 'value' => 1));
$formfields['showalarms'] = array(
'label' => $this->cal->gettext('showalarms'),
'value' => $checkbox->show($calendar['showalarms']?1:0),
'id' => 'calendar-showalarms',
);
}
// allow driver to extend or replace the form content
return html::tag('form', array('action' => "#", 'method' => "get", 'id' => 'calendarpropform'),
$this->cal->driver->calendar_form($action, $calendar, $formfields)
);
}
/**
*
*/
function attendees_list($attrib = array())
{
$table = new html_table(array('cols' => 5, 'border' => 0, 'cellpadding' => 0, 'class' => 'rectable'));
$table->add_header('role', $this->cal->gettext('role'));
$table->add_header('name', $this->cal->gettext('attendee'));
$table->add_header('availability', $this->cal->gettext('availability'));
$table->add_header('confirmstate', $this->cal->gettext('confirmstate'));
$table->add_header('options', '');
return $table->show($attrib);
}
/**
*
*/
function attendees_form($attrib = array())
{
$input = new html_inputfield(array('name' => 'participant', 'id' => 'edit-attendee-name', 'size' => 30));
$checkbox = new html_checkbox(array('name' => 'invite', 'id' => 'edit-attendees-invite', 'value' => 1));
return html::div($attrib,
html::div(null, $input->show() . " " .
html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-add', 'value' => $this->cal->gettext('addattendee'))) . " " .
html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-schedule', 'value' => $this->cal->gettext('scheduletime').'...'))) .
html::p('attendees-invitebox', html::label(null, $checkbox->show(1) . $this->cal->gettext('sendinvitations')))
);
}
/**
*
*/
function attendees_freebusy_table($attrib = array())
{
$table = new html_table(array('cols' => 2, 'border' => 0, 'cellspacing' => 0));
$table->add('attendees',
html::tag('h3', 'boxtitle', $this->cal->gettext('tabattendees')) .
html::div('timesheader', '&nbsp;') .
html::div(array('id' => 'schedule-attendees-list', 'class' => 'attendees-list'), '')
);
$table->add('times',
html::div('scroll',
html::tag('table', array('id' => 'schedule-freebusy-times', 'border' => 0, 'cellspacing' => 0), html::tag('thead') . html::tag('tbody')) .
html::div(array('id' => 'schedule-event-time', 'style' => 'display:none'), '&nbsp;')
)
);
return $table->show($attrib);
}
/**
* Render event details in a table
*/
function event_details_table($event, $title)
{
$table = new html_table(array('cols' => 2, 'border' => 0, 'class' => 'calendar-eventdetails'));
$table->add('ititle', $title);
$table->add('title', Q($event['title']));
$table->add('label', $this->cal->gettext('date'));
$table->add('location', Q($this->cal->event_date_text($event)));
if ($event['location']) {
$table->add('label', $this->cal->gettext('location'));
$table->add('location', Q($event['location']));
}
return $table->show();
}
/**
*
*/
function event_invitebox($attrib = array())
{
if ($this->cal->event) {
return html::div($attrib,
$this->event_details_table($this->cal->event, $this->cal->gettext('itipinvitation')) .
$this->cal->invitestatus
);
}
return '';
}
function event_rsvp_buttons($attrib = array())
{
$attrib += array('type' => 'button');
foreach (array('accepted','tentative','declined') as $method) {
$buttons .= html::tag('input', array(
'type' => $attrib['type'],
'name' => $attrib['iname'],
'class' => 'button',
'rel' => $method,
'value' => $this->cal->gettext('itip' . $method),
));
}
return html::div($attrib,
html::div('label', $this->cal->gettext('acceptinvitation')) .
html::div('rsvp-buttons', $buttons));
}
}
diff --git a/plugins/calendar/lib/js/fullcalendar.js b/plugins/calendar/lib/js/fullcalendar.js
index b04c86ef..bffc0e7a 100644
--- a/plugins/calendar/lib/js/fullcalendar.js
+++ b/plugins/calendar/lib/js/fullcalendar.js
@@ -1,5905 +1,5919 @@
/**
* @preserve
- * FullCalendar v1.5.2
- * http://arshaw.com/fullcalendar/
+ * FullCalendar v1.5.3-rcube-0.7.1
+ * https://github.com/roundcube/fullcalendar
*
* Use fullcalendar.css for basic styling.
* For event drag & drop, requires jQuery UI draggable.
* For event resizing, requires jQuery UI resizable.
*
* Copyright (c) 2011 Adam Shaw
+ * Copyright (c) 2011, Kolab Systems AG
* Dual licensed under the MIT and GPL licenses, located in
* MIT-LICENSE.txt and GPL-LICENSE.txt respectively.
*
- * Date: Sun Aug 21 22:06:09 2011 -0700
+ * Date: Mon Feb 13 23:00:46 2012 +0100
*
*/
(function($, undefined) {
var defaults = {
// display
defaultView: 'month',
aspectRatio: 1.35,
header: {
left: 'title',
center: '',
right: 'today prev,next'
},
weekends: true,
currentTimeIndicator: false,
// editing
//editable: false,
//disableDragging: false,
//disableResizing: false,
allDayDefault: true,
ignoreTimezone: true,
// event ajax
lazyFetching: true,
startParam: 'start',
endParam: 'end',
// time formats
titleFormat: {
month: 'MMMM yyyy',
week: "MMM d[ yyyy]{ '&#8212;'[ MMM] d yyyy}",
day: 'dddd, MMM d, yyyy',
list: 'MMM d, yyyy',
table: 'MMM d, yyyy'
},
columnFormat: {
month: 'ddd',
week: 'ddd M/d',
day: 'dddd M/d',
list: 'dddd, MMM d, yyyy',
- table: 'dddd, MMM d, yyyy'
+ table: 'MMM d, yyyy'
},
timeFormat: { // for event elements
'': 'h(:mm)t' // default
},
// locale
isRTL: false,
firstDay: 0,
monthNames: ['January','February','March','April','May','June','July','August','September','October','November','December'],
monthNamesShort: ['Jan','Feb','Mar','Apr','May','Jun','Jul','Aug','Sep','Oct','Nov','Dec'],
dayNames: ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'],
dayNamesShort: ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'],
buttonText: {
prev: '&nbsp;&#9668;&nbsp;',
next: '&nbsp;&#9658;&nbsp;',
prevYear: '&nbsp;&lt;&lt;&nbsp;',
nextYear: '&nbsp;&gt;&gt;&nbsp;',
today: 'today',
month: 'month',
week: 'week',
day: 'day',
list: 'list',
table: 'table'
},
listTexts: {
until: 'until',
past: 'Past events',
today: 'Today',
tomorrow: 'Tomorrow',
thisWeek: 'This week',
nextWeek: 'Next week',
thisMonth: 'This month',
nextMonth: 'Next month',
future: 'Future events',
week: 'W'
},
// list/table options
listSections: 'month', // false|'day'|'week'|'month'|'smart'
listRange: 30, // number of days to be displayed
listPage: 7, // number of days to jump when paging
tableCols: ['handle', 'date', 'time', 'title'],
// jquery-ui theming
theme: false,
buttonIcons: {
prev: 'circle-triangle-w',
next: 'circle-triangle-e'
},
//selectable: false,
unselectAuto: true,
dropAccept: '*'
};
// right-to-left defaults
var rtlDefaults = {
header: {
left: 'next,prev today',
center: '',
right: 'title'
},
buttonText: {
prev: '&nbsp;&#9658;&nbsp;',
next: '&nbsp;&#9668;&nbsp;',
prevYear: '&nbsp;&gt;&gt;&nbsp;',
nextYear: '&nbsp;&lt;&lt;&nbsp;'
},
buttonIcons: {
prev: 'circle-triangle-e',
next: 'circle-triangle-w'
}
};
-var fc = $.fullCalendar = { version: "1.5.2" };
+var fc = $.fullCalendar = { version: "1.5.3-rcube-0.7.1" };
var fcViews = fc.views = {};
$.fn.fullCalendar = function(options) {
// method calling
if (typeof options == 'string') {
var args = Array.prototype.slice.call(arguments, 1);
var res;
this.each(function() {
var calendar = $.data(this, 'fullCalendar');
if (calendar && $.isFunction(calendar[options])) {
var r = calendar[options].apply(calendar, args);
if (res === undefined) {
res = r;
}
if (options == 'destroy') {
$.removeData(this, 'fullCalendar');
}
}
});
if (res !== undefined) {
return res;
}
return this;
}
// would like to have this logic in EventManager, but needs to happen before options are recursively extended
var eventSources = options.eventSources || [];
delete options.eventSources;
if (options.events) {
eventSources.push(options.events);
delete options.events;
}
options = $.extend(true, {},
defaults,
(options.isRTL || options.isRTL===undefined && defaults.isRTL) ? rtlDefaults : {},
options
);
this.each(function(i, _element) {
var element = $(_element);
var calendar = new Calendar(element, options, eventSources);
element.data('fullCalendar', calendar); // TODO: look into memory leak implications
calendar.render();
});
return this;
};
// function for adding/overriding defaults
function setDefaults(d) {
$.extend(true, defaults, d);
}
function Calendar(element, options, eventSources) {
var t = this;
// exports
t.options = options;
t.render = render;
t.destroy = destroy;
t.refetchEvents = refetchEvents;
t.reportEvents = reportEvents;
t.reportEventChange = reportEventChange;
t.rerenderEvents = rerenderEvents;
t.changeView = changeView;
t.select = select;
t.unselect = unselect;
t.prev = prev;
t.next = next;
t.prevYear = prevYear;
t.nextYear = nextYear;
t.today = today;
t.gotoDate = gotoDate;
t.incrementDate = incrementDate;
t.formatDate = function(format, date) { return formatDate(format, date, options) };
t.formatDates = function(format, date1, date2) { return formatDates(format, date1, date2, options) };
t.getDate = getDate;
t.getView = getView;
t.option = option;
t.trigger = trigger;
// imports
EventManager.call(t, options, eventSources);
var isFetchNeeded = t.isFetchNeeded;
var fetchEvents = t.fetchEvents;
// locals
var _element = element[0];
var header;
var headerElement;
var content;
var tm; // for making theme classes
var currentView;
var viewInstances = {};
var elementOuterWidth;
var suggestedViewHeight;
var absoluteViewElement;
var resizeUID = 0;
var ignoreWindowResize = 0;
var date = new Date();
var events = [];
var _dragElement;
/* Main Rendering
-----------------------------------------------------------------------------*/
setYMD(date, options.year, options.month, options.date);
function render(inc) {
if (!content) {
initialRender();
}else{
calcSize();
markSizesDirty();
markEventsDirty();
renderView(inc);
}
}
function initialRender() {
tm = options.theme ? 'ui' : 'fc';
element.addClass('fc');
if (options.isRTL) {
element.addClass('fc-rtl');
}
if (options.theme) {
element.addClass('ui-widget');
}
content = $("<div class='fc-content' style='position:relative'/>")
.prependTo(element);
header = new Header(t, options);
headerElement = header.render();
if (headerElement) {
element.prepend(headerElement);
}
changeView(options.defaultView);
$(window).resize(windowResize);
// needed for IE in a 0x0 iframe, b/c when it is resized, never triggers a windowResize
if (!bodyVisible()) {
lateRender();
}
}
// called when we know the calendar couldn't be rendered when it was initialized,
// but we think it's ready now
function lateRender() {
setTimeout(function() { // IE7 needs this so dimensions are calculated correctly
if (!currentView.start && bodyVisible()) { // !currentView.start makes sure this never happens more than once
renderView();
}
},0);
}
function destroy() {
$(window).unbind('resize', windowResize);
header.destroy();
content.remove();
element.removeClass('fc fc-rtl ui-widget');
}
function elementVisible() {
return _element.offsetWidth !== 0;
}
function bodyVisible() {
return $('body')[0].offsetWidth !== 0;
}
/* View Rendering
-----------------------------------------------------------------------------*/
// TODO: improve view switching (still weird transition in IE, and FF has whiteout problem)
function changeView(newViewName) {
if (!currentView || newViewName != currentView.name) {
ignoreWindowResize++; // because setMinHeight might change the height before render (and subsequently setSize) is reached
unselect();
var oldView = currentView;
var newViewElement;
if (oldView) {
(oldView.beforeHide || noop)(); // called before changing min-height. if called after, scroll state is reset (in Opera)
setMinHeight(content, content.height());
oldView.element.hide();
}else{
setMinHeight(content, 1); // needs to be 1 (not 0) for IE7, or else view dimensions miscalculated
}
content.css('overflow', 'hidden');
currentView = viewInstances[newViewName];
if (currentView) {
currentView.element.show();
}else{
currentView = viewInstances[newViewName] = new fcViews[newViewName](
newViewElement = absoluteViewElement =
$("<div class='fc-view fc-view-" + newViewName + "' style='position:absolute'/>")
.appendTo(content),
t // the calendar object
);
}
if (oldView) {
header.deactivateButton(oldView.name);
}
header.activateButton(newViewName);
renderView(); // after height has been set, will make absoluteViewElement's position=relative, then set to null
content.css('overflow', '');
if (oldView) {
setMinHeight(content, 1);
}
if (!newViewElement) {
(currentView.afterShow || noop)(); // called after setting min-height/overflow, so in final scroll state (for Opera)
}
ignoreWindowResize--;
}
}
function renderView(inc) {
if (elementVisible()) {
ignoreWindowResize++; // because renderEvents might temporarily change the height before setSize is reached
unselect();
if (suggestedViewHeight === undefined) {
calcSize();
}
var forceEventRender = false;
if (!currentView.start || inc || date < currentView.start || date >= currentView.end) {
// view must render an entire new date range (and refetch/render events)
currentView.render(date, inc || 0); // responsible for clearing events
setSize(true);
forceEventRender = true;
}
else if (currentView.sizeDirty) {
// view must resize (and rerender events)
currentView.clearEvents();
setSize();
forceEventRender = true;
}
else if (currentView.eventsDirty) {
currentView.clearEvents();
forceEventRender = true;
}
currentView.sizeDirty = false;
currentView.eventsDirty = false;
updateEvents(forceEventRender);
elementOuterWidth = element.outerWidth();
header.updateTitle(currentView.title);
var today = new Date();
if (today >= currentView.start && today < currentView.end) {
header.disableButton('today');
}else{
header.enableButton('today');
}
ignoreWindowResize--;
currentView.trigger('viewDisplay', _element);
}
}
/* Resizing
-----------------------------------------------------------------------------*/
function updateSize() {
markSizesDirty();
if (elementVisible()) {
calcSize();
setSize();
unselect();
currentView.clearEvents();
currentView.trigger('viewRender', currentView);
currentView.renderEvents(events);
currentView.sizeDirty = false;
}
}
function markSizesDirty() {
$.each(viewInstances, function(i, inst) {
inst.sizeDirty = true;
});
}
function calcSize() {
if (options.contentHeight) {
suggestedViewHeight = options.contentHeight;
}
else if (options.height) {
suggestedViewHeight = options.height - (headerElement ? headerElement.height() : 0) - vsides(content);
}
else {
suggestedViewHeight = Math.round(content.width() / Math.max(options.aspectRatio, .5));
}
}
function setSize(dateChanged) { // todo: dateChanged?
ignoreWindowResize++;
currentView.setHeight(suggestedViewHeight, dateChanged);
if (absoluteViewElement) {
absoluteViewElement.css('position', 'relative');
absoluteViewElement = null;
}
currentView.setWidth(content.width(), dateChanged);
ignoreWindowResize--;
}
function windowResize() {
if (!ignoreWindowResize) {
if (currentView.start) { // view has already been rendered
var uid = ++resizeUID;
setTimeout(function() { // add a delay
if (uid == resizeUID && !ignoreWindowResize && elementVisible()) {
if (elementOuterWidth != (elementOuterWidth = element.outerWidth())) {
ignoreWindowResize++; // in case the windowResize callback changes the height
updateSize();
currentView.trigger('windowResize', _element);
ignoreWindowResize--;
}
}
}, 200);
}else{
// calendar must have been initialized in a 0x0 iframe that has just been resized
lateRender();
}
}
}
/* Event Fetching/Rendering
-----------------------------------------------------------------------------*/
// fetches events if necessary, rerenders events if necessary (or if forced)
function updateEvents(forceRender) {
if (!options.lazyFetching || isFetchNeeded(currentView.visStart, currentView.visEnd)) {
refetchEvents();
}
else if (forceRender) {
rerenderEvents();
}
}
function refetchEvents(source) {
fetchEvents(currentView.visStart, currentView.visEnd, source); // will call reportEvents
}
// called when event data arrives
function reportEvents(_events) {
events = _events;
rerenderEvents();
}
// called when a single event's data has been changed
function reportEventChange(eventID) {
rerenderEvents(eventID);
}
// attempts to rerenderEvents
function rerenderEvents(modifiedEventID) {
markEventsDirty();
if (elementVisible()) {
currentView.clearEvents();
currentView.trigger('viewRender', currentView);
currentView.renderEvents(events, modifiedEventID);
currentView.eventsDirty = false;
}
}
function markEventsDirty() {
$.each(viewInstances, function(i, inst) {
inst.eventsDirty = true;
});
}
/* Selection
-----------------------------------------------------------------------------*/
function select(start, end, allDay) {
currentView.select(start, end, allDay===undefined ? true : allDay);
}
function unselect() { // safe to be called before renderView
if (currentView) {
currentView.unselect();
}
}
/* Date
-----------------------------------------------------------------------------*/
function prev() {
renderView(-1);
}
function next() {
renderView(1);
}
function prevYear() {
addYears(date, -1);
renderView();
}
function nextYear() {
addYears(date, 1);
renderView();
}
function today() {
date = new Date();
renderView();
}
function gotoDate(year, month, dateOfMonth) {
if (year instanceof Date) {
date = cloneDate(year); // provided 1 argument, a Date
}else{
setYMD(date, year, month, dateOfMonth);
}
renderView();
}
function incrementDate(years, months, days) {
if (years !== undefined) {
addYears(date, years);
}
if (months !== undefined) {
addMonths(date, months);
}
if (days !== undefined) {
addDays(date, days);
}
renderView();
}
function getDate() {
return cloneDate(date);
}
/* Misc
-----------------------------------------------------------------------------*/
function getView() {
return currentView;
}
function option(name, value) {
if (value === undefined) {
return options[name];
}
if (name == 'height' || name == 'contentHeight' || name == 'aspectRatio') {
options[name] = value;
updateSize();
} else if (name.indexOf('list') == 0 || name == 'tableCols') {
options[name] = value;
currentView.start = null; // force re-render
- }
- else if (name == 'maxHeight') {
+ } else if (name == 'maxHeight') {
options[name] = value;
}
}
function trigger(name, thisObj) {
if (options[name]) {
return options[name].apply(
thisObj || _element,
Array.prototype.slice.call(arguments, 2)
);
}
}
/* External Dragging
------------------------------------------------------------------------*/
if (options.droppable) {
$(document)
.bind('dragstart', function(ev, ui) {
var _e = ev.target;
var e = $(_e);
if (!e.parents('.fc').length) { // not already inside a calendar
var accept = options.dropAccept;
if ($.isFunction(accept) ? accept.call(_e, e) : e.is(accept)) {
_dragElement = _e;
currentView.dragStart(_dragElement, ev, ui);
}
}
})
.bind('dragstop', function(ev, ui) {
if (_dragElement) {
currentView.dragStop(_dragElement, ev, ui);
_dragElement = null;
}
});
}
}
function Header(calendar, options) {
var t = this;
// exports
t.render = render;
t.destroy = destroy;
t.updateTitle = updateTitle;
t.activateButton = activateButton;
t.deactivateButton = deactivateButton;
t.disableButton = disableButton;
t.enableButton = enableButton;
// locals
var element = $([]);
var tm;
function render() {
tm = options.theme ? 'ui' : 'fc';
var sections = options.header;
if (sections) {
element = $("<table class='fc-header' style='width:100%'/>")
.append(
$("<tr/>")
.append(renderSection('left'))
.append(renderSection('center'))
.append(renderSection('right'))
);
return element;
}
}
function destroy() {
element.remove();
}
function renderSection(position) {
var e = $("<td class='fc-header-" + position + "'/>");
var buttonStr = options.header[position];
if (buttonStr) {
$.each(buttonStr.split(' '), function(i) {
if (i > 0) {
e.append("<span class='fc-header-space'/>");
}
var prevButton;
$.each(this.split(','), function(j, buttonName) {
if (buttonName == 'title') {
e.append("<span class='fc-header-title'><h2>&nbsp;</h2></span>");
if (prevButton) {
prevButton.addClass(tm + '-corner-right');
}
prevButton = null;
}else{
var buttonClick;
if (calendar[buttonName]) {
buttonClick = calendar[buttonName]; // calendar method
}
else if (fcViews[buttonName]) {
buttonClick = function() {
button.removeClass(tm + '-state-hover'); // forget why
calendar.changeView(buttonName);
};
}
if (buttonClick) {
var icon = options.theme ? smartProperty(options.buttonIcons, buttonName) : null; // why are we using smartProperty here?
var text = smartProperty(options.buttonText, buttonName); // why are we using smartProperty here?
var button = $(
"<span class='fc-button fc-button-" + buttonName + " " + tm + "-state-default'>" +
"<span class='fc-button-inner'>" +
"<span class='fc-button-content'>" +
(icon ?
"<span class='fc-icon-wrap'>" +
"<span class='ui-icon ui-icon-" + icon + "'/>" +
"</span>" :
text
) +
"</span>" +
"<span class='fc-button-effect'><span></span></span>" +
"</span>" +
"</span>"
);
if (button) {
button
.click(function() {
if (!button.hasClass(tm + '-state-disabled')) {
buttonClick();
}
})
.mousedown(function() {
button
.not('.' + tm + '-state-active')
.not('.' + tm + '-state-disabled')
.addClass(tm + '-state-down');
})
.mouseup(function() {
button.removeClass(tm + '-state-down');
})
.hover(
function() {
button
.not('.' + tm + '-state-active')
.not('.' + tm + '-state-disabled')
.addClass(tm + '-state-hover');
},
function() {
button
.removeClass(tm + '-state-hover')
.removeClass(tm + '-state-down');
}
)
.appendTo(e);
if (!prevButton) {
button.addClass(tm + '-corner-left');
}
prevButton = button;
}
}
}
});
if (prevButton) {
prevButton.addClass(tm + '-corner-right');
}
});
}
return e;
}
function updateTitle(html) {
element.find('h2')
.html(html);
}
function activateButton(buttonName) {
element.find('span.fc-button-' + buttonName)
.addClass(tm + '-state-active');
}
function deactivateButton(buttonName) {
element.find('span.fc-button-' + buttonName)
.removeClass(tm + '-state-active');
}
function disableButton(buttonName) {
element.find('span.fc-button-' + buttonName)
.addClass(tm + '-state-disabled');
}
function enableButton(buttonName) {
element.find('span.fc-button-' + buttonName)
.removeClass(tm + '-state-disabled');
}
}
fc.sourceNormalizers = [];
fc.sourceFetchers = [];
var ajaxDefaults = {
dataType: 'json',
cache: false
};
var eventGUID = 1;
function EventManager(options, _sources) {
var t = this;
// exports
t.isFetchNeeded = isFetchNeeded;
t.fetchEvents = fetchEvents;
t.addEventSource = addEventSource;
t.removeEventSource = removeEventSource;
t.updateEvent = updateEvent;
t.renderEvent = renderEvent;
t.removeEvents = removeEvents;
t.clientEvents = clientEvents;
t.normalizeEvent = normalizeEvent;
// imports
var trigger = t.trigger;
var getView = t.getView;
var reportEvents = t.reportEvents;
// locals
var stickySource = { events: [] };
var sources = [ stickySource ];
var rangeStart, rangeEnd;
var currentFetchID = 0;
var pendingSourceCnt = 0;
var loadingLevel = 0;
var cache = [];
for (var i=0; i<_sources.length; i++) {
_addEventSource(_sources[i]);
}
/* Fetching
-----------------------------------------------------------------------------*/
function isFetchNeeded(start, end) {
return !rangeStart || start < rangeStart || end > rangeEnd;
}
function fetchEvents(start, end, src) {
rangeStart = start;
rangeEnd = end;
+ // partially clear cache if refreshing one source only (issue #1061)
cache = typeof src != 'undefined' ? $.grep(cache, function(e) { return !isSourcesEqual(e.source, src); }) : [];
var fetchID = ++currentFetchID;
var len = sources.length;
pendingSourceCnt = typeof src == 'undefined' ? len : 1;
for (var i=0; i<len; i++) {
if (typeof src == 'undefined' || isSourcesEqual(sources[i], src))
fetchEventSource(sources[i], fetchID);
}
}
+
function fetchEventSource(source, fetchID) {
_fetchEventSource(source, function(events) {
if (fetchID == currentFetchID) {
if (events) {
for (var i=0; i<events.length; i++) {
events[i].source = source;
normalizeEvent(events[i]);
}
cache = cache.concat(events);
}
pendingSourceCnt--;
if (!pendingSourceCnt) {
reportEvents(cache);
}
}
});
}
function _fetchEventSource(source, callback) {
var i;
var fetchers = fc.sourceFetchers;
var res;
for (i=0; i<fetchers.length; i++) {
res = fetchers[i](source, rangeStart, rangeEnd, callback);
if (res === true) {
// the fetcher is in charge. made its own async request
return;
}
else if (typeof res == 'object') {
// the fetcher returned a new source. process it
_fetchEventSource(res, callback);
return;
}
}
var events = source.events;
if (events) {
if ($.isFunction(events)) {
pushLoading();
events(cloneDate(rangeStart), cloneDate(rangeEnd), function(events) {
callback(events);
popLoading();
});
}
else if ($.isArray(events)) {
callback(events);
}
else {
callback();
}
}else{
var url = source.url;
if (url) {
var success = source.success;
var error = source.error;
var complete = source.complete;
var data = $.extend({}, source.data || {});
var startParam = firstDefined(source.startParam, options.startParam);
var endParam = firstDefined(source.endParam, options.endParam);
if (startParam) {
data[startParam] = Math.round(+rangeStart / 1000);
}
if (endParam) {
data[endParam] = Math.round(+rangeEnd / 1000);
}
pushLoading();
$.ajax($.extend({}, ajaxDefaults, source, {
data: data,
success: function(events) {
events = events || [];
var res = applyAll(success, this, arguments);
if ($.isArray(res)) {
events = res;
}
callback(events);
},
error: function() {
applyAll(error, this, arguments);
callback();
},
complete: function() {
applyAll(complete, this, arguments);
popLoading();
}
}));
}else{
callback();
}
}
}
/* Sources
-----------------------------------------------------------------------------*/
function addEventSource(source) {
source = _addEventSource(source);
if (source) {
pendingSourceCnt++;
fetchEventSource(source, currentFetchID); // will eventually call reportEvents
}
}
function _addEventSource(source) {
if ($.isFunction(source) || $.isArray(source)) {
source = { events: source };
}
else if (typeof source == 'string') {
source = { url: source };
}
if (typeof source == 'object') {
normalizeSource(source);
sources.push(source);
return source;
}
}
function removeEventSource(source) {
sources = $.grep(sources, function(src) {
return !isSourcesEqual(src, source);
});
// remove all client events from that source
cache = $.grep(cache, function(e) {
return !isSourcesEqual(e.source, source);
});
reportEvents(cache);
}
/* Manipulation
-----------------------------------------------------------------------------*/
function updateEvent(event) { // update an existing event
var i, len = cache.length, e,
defaultEventEnd = getView().defaultEventEnd, // getView???
startDelta = event.start - event._start,
endDelta = event.end ?
(event.end - (event._end || defaultEventEnd(event))) // event._end would be null if event.end
: 0; // was null and event was just resized
for (i=0; i<len; i++) {
e = cache[i];
if (e._id == event._id && e != event) {
e.start = new Date(+e.start + startDelta);
if (event.end) {
if (e.end) {
e.end = new Date(+e.end + endDelta);
}else{
e.end = new Date(+defaultEventEnd(e) + endDelta);
}
}else{
e.end = null;
}
e.title = event.title;
e.url = event.url;
e.allDay = event.allDay;
e.className = event.className;
e.editable = event.editable;
e.color = event.color;
e.backgroudColor = event.backgroudColor;
e.borderColor = event.borderColor;
e.textColor = event.textColor;
normalizeEvent(e);
}
}
normalizeEvent(event);
reportEvents(cache);
}
function renderEvent(event, stick) {
normalizeEvent(event);
if (!event.source) {
if (stick) {
stickySource.events.push(event);
event.source = stickySource;
}
}
-
+ // always push event to cache (issue #1112:)
cache.push(event);
reportEvents(cache);
}
function removeEvents(filter) {
if (!filter) { // remove all
cache = [];
// clear all array sources
for (var i=0; i<sources.length; i++) {
if ($.isArray(sources[i].events)) {
sources[i].events = [];
}
}
}else{
if (!$.isFunction(filter)) { // an event ID
var id = filter + '';
filter = function(e) {
return e._id == id;
};
}
cache = $.grep(cache, filter, true);
// remove events from array sources
for (var i=0; i<sources.length; i++) {
if ($.isArray(sources[i].events)) {
sources[i].events = $.grep(sources[i].events, filter, true);
}
}
}
reportEvents(cache);
}
function clientEvents(filter) {
if ($.isFunction(filter)) {
return $.grep(cache, filter);
}
else if (filter) { // an event ID
filter += '';
return $.grep(cache, function(e) {
return e._id == filter;
});
}
return cache; // else, return all
}
/* Loading State
-----------------------------------------------------------------------------*/
function pushLoading() {
if (!loadingLevel++) {
trigger('loading', null, true);
}
}
function popLoading() {
if (!--loadingLevel) {
trigger('loading', null, false);
}
}
/* Event Normalization
-----------------------------------------------------------------------------*/
function normalizeEvent(event) {
var source = event.source || {};
var ignoreTimezone = firstDefined(source.ignoreTimezone, options.ignoreTimezone);
event._id = event._id || (event.id === undefined ? '_fc' + eventGUID++ : event.id + '');
if (event.date) {
if (!event.start) {
event.start = event.date;
}
delete event.date;
}
event._start = cloneDate(event.start = parseDate(event.start, ignoreTimezone));
event.end = parseDate(event.end, ignoreTimezone);
if (event.end && event.end <= event.start) {
event.end = null;
}
event._end = event.end ? cloneDate(event.end) : null;
if (event.allDay === undefined) {
event.allDay = firstDefined(source.allDayDefault, options.allDayDefault);
}
if (event.className) {
if (typeof event.className == 'string') {
event.className = event.className.split(/\s+/);
}
}else{
event.className = [];
}
// TODO: if there is no start date, return false to indicate an invalid event
}
/* Utils
------------------------------------------------------------------------------*/
function normalizeSource(source) {
if (source.className) {
// TODO: repeat code, same code for event classNames
if (typeof source.className == 'string') {
source.className = source.className.split(/\s+/);
}
}else{
source.className = [];
}
var normalizers = fc.sourceNormalizers;
for (var i=0; i<normalizers.length; i++) {
normalizers[i](source);
}
}
function isSourcesEqual(source1, source2) {
return source1 && source2 && getSourcePrimitive(source1) == getSourcePrimitive(source2);
}
function getSourcePrimitive(source) {
return ((typeof source == 'object') ? (source.events || source.url) : '') || source;
}
}
fc.addDays = addDays;
fc.cloneDate = cloneDate;
fc.parseDate = parseDate;
fc.parseISO8601 = parseISO8601;
fc.parseTime = parseTime;
fc.formatDate = formatDate;
fc.formatDates = formatDates;
/* Date Math
-----------------------------------------------------------------------------*/
var dayIDs = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'],
DAY_MS = 86400000,
HOUR_MS = 3600000,
MINUTE_MS = 60000;
function addYears(d, n, keepTime) {
d.setFullYear(d.getFullYear() + n);
if (!keepTime) {
clearTime(d);
}
return d;
}
function addMonths(d, n, keepTime) { // prevents day overflow/underflow
if (+d) { // prevent infinite looping on invalid dates
var m = d.getMonth() + n,
check = cloneDate(d);
check.setDate(1);
check.setMonth(m);
d.setMonth(m);
if (!keepTime) {
clearTime(d);
}
while (d.getMonth() != check.getMonth()) {
d.setDate(d.getDate() + (d < check ? 1 : -1));
}
}
return d;
}
function addDays(d, n, keepTime) { // deals with daylight savings
if (+d) {
var dd = d.getDate() + n,
check = cloneDate(d);
check.setHours(9); // set to middle of day
check.setDate(dd);
d.setDate(dd);
if (!keepTime) {
clearTime(d);
}
fixDate(d, check);
}
return d;
}
function fixDate(d, check) { // force d to be on check's YMD, for daylight savings purposes
if (+d) { // prevent infinite looping on invalid dates
while (d.getDate() != check.getDate()) {
d.setTime(+d + (d < check ? 1 : -1) * HOUR_MS);
}
}
}
function addMinutes(d, n) {
d.setMinutes(d.getMinutes() + n);
return d;
}
function clearTime(d) {
d.setHours(0);
d.setMinutes(0);
d.setSeconds(0);
d.setMilliseconds(0);
return d;
}
function cloneDate(d, dontKeepTime) {
if (dontKeepTime) {
return clearTime(new Date(+d));
}
return new Date(+d);
}
function zeroDate() { // returns a Date with time 00:00:00 and dateOfMonth=1
var i=0, d;
do {
d = new Date(1970, i++, 1);
} while (d.getHours()); // != 0
return d;
}
function skipWeekend(date, inc, excl) {
inc = inc || 1;
while (!date.getDay() || (excl && date.getDay()==1 || !excl && date.getDay()==6)) {
addDays(date, inc);
}
return date;
}
function dayDiff(d1, d2) { // d1 - d2
return Math.round((cloneDate(d1, true) - cloneDate(d2, true)) / DAY_MS);
}
function setYMD(date, y, m, d) {
if (y !== undefined && y != date.getFullYear()) {
date.setDate(1);
date.setMonth(0);
date.setFullYear(y);
}
if (m !== undefined && m != date.getMonth()) {
date.setDate(1);
date.setMonth(m);
}
if (d !== undefined) {
date.setDate(d);
}
}
/* Date Parsing
-----------------------------------------------------------------------------*/
function parseDate(s, ignoreTimezone) { // ignoreTimezone defaults to true
if (typeof s == 'object') { // already a Date object
return s;
}
if (typeof s == 'number') { // a UNIX timestamp
return new Date(s * 1000);
}
if (typeof s == 'string') {
if (s.match(/^\d+(\.\d+)?$/)) { // a UNIX timestamp
return new Date(parseFloat(s) * 1000);
}
if (ignoreTimezone === undefined) {
ignoreTimezone = true;
}
return parseISO8601(s, ignoreTimezone) || (s ? new Date(s) : null);
}
// TODO: never return invalid dates (like from new Date(<string>)), return null instead
return null;
}
function parseISO8601(s, ignoreTimezone) { // ignoreTimezone defaults to false
// derived from http://delete.me.uk/2005/03/iso8601.html
// TODO: for a know glitch/feature, read tests/issue_206_parseDate_dst.html
var m = s.match(/^([0-9]{4})(-([0-9]{2})(-([0-9]{2})([T ]([0-9]{2}):([0-9]{2})(:([0-9]{2})(\.([0-9]+))?)?(Z|(([-+])([0-9]{2})(:?([0-9]{2}))?))?)?)?)?$/);
if (!m) {
return null;
}
var date = new Date(m[1], 0, 1);
if (ignoreTimezone || !m[13]) {
var check = new Date(m[1], 0, 1, 9, 0);
if (m[3]) {
date.setMonth(m[3] - 1);
check.setMonth(m[3] - 1);
}
if (m[5]) {
date.setDate(m[5]);
check.setDate(m[5]);
}
fixDate(date, check);
if (m[7]) {
date.setHours(m[7]);
}
if (m[8]) {
date.setMinutes(m[8]);
}
if (m[10]) {
date.setSeconds(m[10]);
}
if (m[12]) {
date.setMilliseconds(Number("0." + m[12]) * 1000);
}
fixDate(date, check);
}else{
date.setUTCFullYear(
m[1],
m[3] ? m[3] - 1 : 0,
m[5] || 1
);
date.setUTCHours(
m[7] || 0,
m[8] || 0,
m[10] || 0,
m[12] ? Number("0." + m[12]) * 1000 : 0
);
if (m[14]) {
var offset = Number(m[16]) * 60 + (m[18] ? Number(m[18]) : 0);
offset *= m[15] == '-' ? 1 : -1;
date = new Date(+date + (offset * 60 * 1000));
}
}
return date;
}
function parseTime(s) { // returns minutes since start of day
if (typeof s == 'number') { // an hour
return s * 60;
}
if (typeof s == 'object') { // a Date object
return s.getHours() * 60 + s.getMinutes();
}
var m = s.match(/(\d+)(?::(\d+))?\s*(\w+)?/);
if (m) {
var h = parseInt(m[1], 10);
if (m[3]) {
h %= 12;
if (m[3].toLowerCase().charAt(0) == 'p') {
h += 12;
}
}
return h * 60 + (m[2] ? parseInt(m[2], 10) : 0);
}
}
/* Date Formatting
-----------------------------------------------------------------------------*/
// TODO: use same function formatDate(date, [date2], format, [options])
function formatDate(date, format, options) {
return formatDates(date, null, format, options);
}
function formatDates(date1, date2, format, options) {
options = options || defaults;
var date = date1,
otherDate = date2,
i, len = format.length, c,
i2, formatter,
res = '';
for (i=0; i<len; i++) {
c = format.charAt(i);
if (c == "'") {
for (i2=i+1; i2<len; i2++) {
if (format.charAt(i2) == "'") {
if (date) {
if (i2 == i+1) {
res += "'";
}else{
res += format.substring(i+1, i2);
}
i = i2;
}
break;
}
}
}
else if (c == '(') {
for (i2=i+1; i2<len; i2++) {
if (format.charAt(i2) == ')') {
var subres = formatDate(date, format.substring(i+1, i2), options);
if (parseInt(subres.replace(/\D/, ''), 10)) {
res += subres;
}
i = i2;
break;
}
}
}
else if (c == '[') {
for (i2=i+1; i2<len; i2++) {
if (format.charAt(i2) == ']') {
var subformat = format.substring(i+1, i2);
var subres = formatDate(date, subformat, options);
if (subres != formatDate(otherDate, subformat, options)) {
res += subres;
}
i = i2;
break;
}
}
}
else if (c == '{') {
date = date2;
otherDate = date1;
}
else if (c == '}') {
date = date1;
otherDate = date2;
}
else {
for (i2=len; i2>i; i2--) {
if (formatter = dateFormatters[format.substring(i, i2)]) {
if (date) {
res += formatter(date, options);
}
i = i2 - 1;
break;
}
}
if (i2 == i) {
if (date) {
res += c;
}
}
}
}
return res;
};
var dateFormatters = {
s : function(d) { return d.getSeconds() },
ss : function(d) { return zeroPad(d.getSeconds()) },
m : function(d) { return d.getMinutes() },
mm : function(d) { return zeroPad(d.getMinutes()) },
h : function(d) { return d.getHours() % 12 || 12 },
hh : function(d) { return zeroPad(d.getHours() % 12 || 12) },
H : function(d) { return d.getHours() },
HH : function(d) { return zeroPad(d.getHours()) },
d : function(d) { return d.getDate() },
dd : function(d) { return zeroPad(d.getDate()) },
ddd : function(d,o) { return o.dayNamesShort[d.getDay()] },
dddd: function(d,o) { return o.dayNames[d.getDay()] },
M : function(d) { return d.getMonth() + 1 },
MM : function(d) { return zeroPad(d.getMonth() + 1) },
MMM : function(d,o) { return o.monthNamesShort[d.getMonth()] },
MMMM: function(d,o) { return o.monthNames[d.getMonth()] },
yy : function(d) { return (d.getFullYear()+'').substring(2) },
yyyy: function(d) { return d.getFullYear() },
t : function(d) { return d.getHours() < 12 ? 'a' : 'p' },
tt : function(d) { return d.getHours() < 12 ? 'am' : 'pm' },
T : function(d) { return d.getHours() < 12 ? 'A' : 'P' },
TT : function(d) { return d.getHours() < 12 ? 'AM' : 'PM' },
u : function(d) { return formatDate(d, "yyyy-MM-dd'T'HH:mm:ss'Z'") },
S : function(d) {
var date = d.getDate();
if (date > 10 && date < 20) {
return 'th';
}
return ['st', 'nd', 'rd'][date%10-1] || 'th';
- },
+ } ,
W : function(d) { return iso8601Week(d); }
};
// Determine the week of the year based on the ISO 8601 definition.
// copied from jquery UI Datepicker
var iso8601Week = function(date) {
var checkDate = cloneDate(date);
// Find Thursday of this week starting on Monday
checkDate.setDate(checkDate.getDate() + 4 - (checkDate.getDay() || 7));
var time = checkDate.getTime();
checkDate.setMonth(0); // Compare with Jan 1
checkDate.setDate(1);
return Math.floor(Math.round((time - checkDate) / 86400000) / 7) + 1;
};
fc.applyAll = applyAll;
/* Event Date Math
-----------------------------------------------------------------------------*/
function exclEndDay(event) {
if (event.end) {
return _exclEndDay(event.end, event.allDay);
}else{
return addDays(cloneDate(event.start), 1);
}
}
function _exclEndDay(end, allDay) {
end = cloneDate(end);
return allDay || end.getHours() || end.getMinutes() ? addDays(end, 1) : clearTime(end);
}
function segCmp(a, b) {
return (b.msLength - a.msLength) * 100 + (a.event.start - b.event.start);
}
function segsCollide(seg1, seg2) {
return seg1.end > seg2.start && seg1.start < seg2.end;
}
/* Event Sorting
-----------------------------------------------------------------------------*/
// event rendering utilities
function sliceSegs(events, visEventEnds, start, end) {
var segs = [],
i, len=events.length, event,
eventStart, eventEnd,
segStart, segEnd,
isStart, isEnd;
for (i=0; i<len; i++) {
event = events[i];
eventStart = event.start;
eventEnd = visEventEnds[i];
if (eventEnd > start && eventStart < end) {
if (eventStart < start) {
segStart = cloneDate(start);
isStart = false;
}else{
segStart = eventStart;
isStart = true;
}
if (eventEnd > end) {
segEnd = cloneDate(end);
isEnd = false;
}else{
segEnd = eventEnd;
isEnd = true;
}
segs.push({
event: event,
start: segStart,
end: segEnd,
isStart: isStart,
isEnd: isEnd,
msLength: segEnd - segStart
});
}
}
return segs.sort(segCmp);
}
// event rendering calculation utilities
function stackSegs(segs) {
var levels = [],
i, len = segs.length, seg,
j, collide, k;
for (i=0; i<len; i++) {
seg = segs[i];
j = 0; // the level index where seg should belong
while (true) {
collide = false;
if (levels[j]) {
for (k=0; k<levels[j].length; k++) {
if (segsCollide(levels[j][k], seg)) {
collide = true;
break;
}
}
}
if (collide) {
j++;
}else{
break;
}
}
if (levels[j]) {
levels[j].push(seg);
}else{
levels[j] = [seg];
}
}
return levels;
}
/* Event Element Binding
-----------------------------------------------------------------------------*/
function lazySegBind(container, segs, bindHandlers) {
container.unbind('mouseover').mouseover(function(ev) {
var parent=ev.target, e,
i, seg;
while (parent != this) {
e = parent;
parent = parent.parentNode;
}
if ((i = e._fci) !== undefined) {
e._fci = undefined;
seg = segs[i];
bindHandlers(seg.event, seg.element, seg);
$(ev.target).trigger(ev);
}
ev.stopPropagation();
});
}
/* Element Dimensions
-----------------------------------------------------------------------------*/
function setOuterWidth(element, width, includeMargins) {
for (var i=0, e; i<element.length; i++) {
e = $(element[i]);
e.width(Math.max(0, width - hsides(e, includeMargins)));
}
}
function setOuterHeight(element, height, includeMargins) {
for (var i=0, e; i<element.length; i++) {
e = $(element[i]);
e.height(Math.max(0, height - vsides(e, includeMargins)));
}
}
// TODO: curCSS has been deprecated (jQuery 1.4.3 - 10/16/2010)
function hsides(element, includeMargins) {
return hpadding(element) + hborders(element) + (includeMargins ? hmargins(element) : 0);
}
function hpadding(element) {
return (parseFloat($.curCSS(element[0], 'paddingLeft', true)) || 0) +
(parseFloat($.curCSS(element[0], 'paddingRight', true)) || 0);
}
function hmargins(element) {
return (parseFloat($.curCSS(element[0], 'marginLeft', true)) || 0) +
(parseFloat($.curCSS(element[0], 'marginRight', true)) || 0);
}
function hborders(element) {
return (parseFloat($.curCSS(element[0], 'borderLeftWidth', true)) || 0) +
(parseFloat($.curCSS(element[0], 'borderRightWidth', true)) || 0);
}
function vsides(element, includeMargins) {
return vpadding(element) + vborders(element) + (includeMargins ? vmargins(element) : 0);
}
function vpadding(element) {
return (parseFloat($.curCSS(element[0], 'paddingTop', true)) || 0) +
(parseFloat($.curCSS(element[0], 'paddingBottom', true)) || 0);
}
function vmargins(element) {
return (parseFloat($.curCSS(element[0], 'marginTop', true)) || 0) +
(parseFloat($.curCSS(element[0], 'marginBottom', true)) || 0);
}
function vborders(element) {
return (parseFloat($.curCSS(element[0], 'borderTopWidth', true)) || 0) +
(parseFloat($.curCSS(element[0], 'borderBottomWidth', true)) || 0);
}
function setMinHeight(element, height) {
height = (typeof height == 'number' ? height + 'px' : height);
element.each(function(i, _element) {
_element.style.cssText += ';min-height:' + height + ';_height:' + height;
// why can't we just use .css() ? i forget
});
}
/* Misc Utils
-----------------------------------------------------------------------------*/
//TODO: arraySlice
//TODO: isFunction, grep ?
function noop() { }
function cmp(a, b) {
return a - b;
}
function arrayMax(a) {
return Math.max.apply(Math, a);
}
function zeroPad(n) {
return (n < 10 ? '0' : '') + n;
}
function smartProperty(obj, name) { // get a camel-cased/namespaced property of an object
if (obj[name] !== undefined) {
return obj[name];
}
var parts = name.split(/(?=[A-Z])/),
i=parts.length-1, res;
for (; i>=0; i--) {
res = obj[parts[i].toLowerCase()];
if (res !== undefined) {
return res;
}
}
return obj[''];
}
function htmlEscape(s) {
return s.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/'/g, '&#039;')
.replace(/"/g, '&quot;')
.replace(/\n/g, '<br />');
}
function cssKey(_element) {
return _element.id + '/' + _element.className + '/' + _element.style.cssText.replace(/(^|;)\s*(top|left|width|height)\s*:[^;]*/ig, '');
}
function disableTextSelection(element) {
element
.attr('unselectable', 'on')
.css('MozUserSelect', 'none')
.bind('selectstart.ui', function() { return false; });
}
/*
function enableTextSelection(element) {
element
.attr('unselectable', 'off')
.css('MozUserSelect', '')
.unbind('selectstart.ui');
}
*/
function markFirstLast(e) {
e.children()
.removeClass('fc-first fc-last')
.filter(':first-child')
.addClass('fc-first')
.end()
.filter(':last-child')
.addClass('fc-last');
}
function setDayID(cell, date) {
cell.each(function(i, _cell) {
_cell.className = _cell.className.replace(/^fc-\w*/, 'fc-' + dayIDs[date.getDay()]);
// TODO: make a way that doesn't rely on order of classes
});
}
function getSkinCss(event, opt) {
var source = event.source || {};
var eventColor = event.color;
var sourceColor = source.color;
var optionColor = opt('eventColor');
var backgroundColor =
event.backgroundColor ||
eventColor ||
source.backgroundColor ||
sourceColor ||
opt('eventBackgroundColor') ||
optionColor;
var borderColor =
event.borderColor ||
eventColor ||
source.borderColor ||
sourceColor ||
opt('eventBorderColor') ||
optionColor;
var textColor =
event.textColor ||
source.textColor ||
opt('eventTextColor');
var statements = [];
if (backgroundColor) {
statements.push('background-color:' + backgroundColor);
}
if (borderColor) {
statements.push('border-color:' + borderColor);
}
if (textColor) {
statements.push('color:' + textColor);
}
return statements.join(';');
}
function applyAll(functions, thisObj, args) {
if ($.isFunction(functions)) {
functions = [ functions ];
}
if (functions) {
var i;
var ret;
for (i=0; i<functions.length; i++) {
ret = functions[i].apply(thisObj, args) || ret;
}
return ret;
}
}
function firstDefined() {
for (var i=0; i<arguments.length; i++) {
if (arguments[i] !== undefined) {
return arguments[i];
}
}
}
fcViews.month = MonthView;
function MonthView(element, calendar) {
var t = this;
// exports
t.render = render;
// imports
BasicView.call(t, element, calendar, 'month');
var opt = t.opt;
var renderBasic = t.renderBasic;
var formatDate = calendar.formatDate;
function render(date, delta) {
if (delta) {
addMonths(date, delta);
date.setDate(1);
}
var start = cloneDate(date, true);
start.setDate(1);
var end = addMonths(cloneDate(start), 1);
var visStart = cloneDate(start);
var visEnd = cloneDate(end);
var firstDay = opt('firstDay');
var nwe = opt('weekends') ? 0 : 1;
if (nwe) {
skipWeekend(visStart);
skipWeekend(visEnd, -1, true);
}
addDays(visStart, -((visStart.getDay() - Math.max(firstDay, nwe) + 7) % 7));
addDays(visEnd, (7 - visEnd.getDay() + Math.max(firstDay, nwe)) % 7);
var rowCnt = Math.round((visEnd - visStart) / (DAY_MS * 7));
if (opt('weekMode') == 'fixed') {
addDays(visEnd, (6 - rowCnt) * 7);
rowCnt = 6;
}
t.title = formatDate(start, opt('titleFormat'));
t.start = start;
t.end = end;
t.visStart = visStart;
t.visEnd = visEnd;
renderBasic(6, rowCnt, nwe ? 5 : 7, true);
}
}
fcViews.basicWeek = BasicWeekView;
function BasicWeekView(element, calendar) {
var t = this;
// exports
t.render = render;
// imports
BasicView.call(t, element, calendar, 'basicWeek');
var opt = t.opt;
var renderBasic = t.renderBasic;
var formatDates = calendar.formatDates;
function render(date, delta) {
if (delta) {
addDays(date, delta * 7);
}
var start = addDays(cloneDate(date), -((date.getDay() - opt('firstDay') + 7) % 7));
var end = addDays(cloneDate(start), 7);
var visStart = cloneDate(start);
var visEnd = cloneDate(end);
var weekends = opt('weekends');
if (!weekends) {
skipWeekend(visStart);
skipWeekend(visEnd, -1, true);
}
t.title = formatDates(
visStart,
addDays(cloneDate(visEnd), -1),
opt('titleFormat')
);
t.start = start;
t.end = end;
t.visStart = visStart;
t.visEnd = visEnd;
renderBasic(1, 1, weekends ? 7 : 5, false);
}
}
fcViews.basicDay = BasicDayView;
//TODO: when calendar's date starts out on a weekend, shouldn't happen
function BasicDayView(element, calendar) {
var t = this;
// exports
t.render = render;
// imports
BasicView.call(t, element, calendar, 'basicDay');
var opt = t.opt;
var renderBasic = t.renderBasic;
var formatDate = calendar.formatDate;
function render(date, delta) {
if (delta) {
addDays(date, delta);
if (!opt('weekends')) {
skipWeekend(date, delta < 0 ? -1 : 1);
}
}
t.title = formatDate(date, opt('titleFormat'));
t.start = t.visStart = cloneDate(date, true);
t.end = t.visEnd = addDays(cloneDate(t.start), 1);
renderBasic(1, 1, 1, false);
}
}
setDefaults({
weekMode: 'fixed'
});
function BasicView(element, calendar, viewName) {
var t = this;
// exports
t.renderBasic = renderBasic;
t.setHeight = setHeight;
t.setWidth = setWidth;
t.renderDayOverlay = renderDayOverlay;
t.defaultSelectionEnd = defaultSelectionEnd;
t.renderSelection = renderSelection;
t.clearSelection = clearSelection;
t.reportDayClick = reportDayClick; // for selection (kinda hacky)
t.dragStart = dragStart;
t.dragStop = dragStop;
t.defaultEventEnd = defaultEventEnd;
t.getHoverListener = function() { return hoverListener };
t.colContentLeft = colContentLeft;
t.colContentRight = colContentRight;
t.dayOfWeekCol = dayOfWeekCol;
t.dateCell = dateCell;
t.cellDate = cellDate;
t.cellIsAllDay = function() { return true };
t.allDayRow = allDayRow;
t.allDayBounds = allDayBounds;
t.getRowCnt = function() { return rowCnt };
t.getColCnt = function() { return colCnt };
t.getColWidth = function() { return colWidth };
t.getDaySegmentContainer = function() { return daySegmentContainer };
// imports
View.call(t, element, calendar, viewName);
OverlayManager.call(t);
SelectionManager.call(t);
BasicEventRenderer.call(t);
var opt = t.opt;
var trigger = t.trigger;
var clearEvents = t.clearEvents;
var renderOverlay = t.renderOverlay;
var clearOverlays = t.clearOverlays;
var daySelectionMousedown = t.daySelectionMousedown;
var formatDate = calendar.formatDate;
// locals
var head;
var headCells;
var body;
var bodyRows;
var bodyCells;
var bodyFirstCells;
var bodyCellTopInners;
var daySegmentContainer;
var viewWidth;
var viewHeight;
var colWidth;
var rowCnt, colCnt;
var coordinateGrid;
var hoverListener;
var colContentPositions;
var rtl, dis, dit;
var firstDay;
var nwe;
var tm;
var colFormat;
/* Rendering
------------------------------------------------------------*/
disableTextSelection(element.addClass('fc-grid'));
function renderBasic(maxr, r, c, showNumbers) {
rowCnt = r;
colCnt = c;
updateOptions();
var firstTime = !body;
if (firstTime) {
buildSkeleton(maxr, showNumbers);
}else{
clearEvents();
}
updateCells(firstTime);
}
function updateOptions() {
rtl = opt('isRTL');
if (rtl) {
dis = -1;
dit = colCnt - 1;
}else{
dis = 1;
dit = 0;
}
firstDay = opt('firstDay');
nwe = opt('weekends') ? 0 : 1;
tm = opt('theme') ? 'ui' : 'fc';
colFormat = opt('columnFormat');
}
function buildSkeleton(maxRowCnt, showNumbers) {
var s;
var headerClass = tm + "-widget-header";
var contentClass = tm + "-widget-content";
var i, j;
var table;
s =
"<table class='fc-border-separate' style='width:100%' cellspacing='0'>" +
"<thead>" +
"<tr>";
for (i=0; i<colCnt; i++) {
s +=
"<th class='fc- " + headerClass + "'/>"; // need fc- for setDayID
}
s +=
"</tr>" +
"</thead>" +
"<tbody>";
for (i=0; i<maxRowCnt; i++) {
s +=
"<tr class='fc-week" + i + "'>";
for (j=0; j<colCnt; j++) {
s +=
"<td class='fc- " + contentClass + " fc-day" + (i*colCnt+j) + "'>" + // need fc- for setDayID
"<div>" +
(showNumbers ?
"<div class='fc-day-number'/>" :
''
) +
"<div class='fc-day-content'>" +
"<div style='position:relative'>&nbsp;</div>" +
"</div>" +
"</div>" +
"</td>";
}
s +=
"</tr>";
}
s +=
"</tbody>" +
"</table>";
table = $(s).appendTo(element);
head = table.find('thead');
headCells = head.find('th');
body = table.find('tbody');
bodyRows = body.find('tr');
bodyCells = body.find('td');
bodyFirstCells = bodyCells.filter(':first-child');
bodyCellTopInners = bodyRows.eq(0).find('div.fc-day-content div');
markFirstLast(head.add(head.find('tr'))); // marks first+last tr/th's
markFirstLast(bodyRows); // marks first+last td's
bodyRows.eq(0).addClass('fc-first'); // fc-last is done in updateCells
dayBind(bodyCells);
daySegmentContainer =
$("<div style='position:absolute;z-index:8;top:0;left:0'/>")
.appendTo(element);
}
function updateCells(firstTime) {
var dowDirty = firstTime || rowCnt == 1; // could the cells' day-of-weeks need updating?
var month = t.start.getMonth();
var today = clearTime(new Date());
var cell;
var date;
var row;
if (dowDirty) {
headCells.each(function(i, _cell) {
cell = $(_cell);
date = indexDate(i);
cell.html(formatDate(date, colFormat));
setDayID(cell, date);
});
}
bodyCells.each(function(i, _cell) {
cell = $(_cell);
date = indexDate(i);
if (date.getMonth() == month) {
cell.removeClass('fc-other-month');
}else{
cell.addClass('fc-other-month');
}
if (+date == +today) {
cell.addClass(tm + '-state-highlight fc-today');
}else{
cell.removeClass(tm + '-state-highlight fc-today');
}
cell.find('div.fc-day-number').text(date.getDate());
if (dowDirty) {
setDayID(cell, date);
}
});
bodyRows.each(function(i, _row) {
row = $(_row);
if (i < rowCnt) {
row.show();
if (i == rowCnt-1) {
row.addClass('fc-last');
}else{
row.removeClass('fc-last');
}
}else{
row.hide();
}
});
}
function setHeight(height) {
viewHeight = height;
var bodyHeight = viewHeight - head.height();
var rowHeight;
var rowHeightLast;
var cell;
if (opt('weekMode') == 'variable') {
rowHeight = rowHeightLast = Math.floor(bodyHeight / (rowCnt==1 ? 2 : 6));
}else{
rowHeight = Math.floor(bodyHeight / rowCnt);
rowHeightLast = bodyHeight - rowHeight * (rowCnt-1);
}
bodyFirstCells.each(function(i, _cell) {
if (i < rowCnt) {
cell = $(_cell);
setMinHeight(
cell.find('> div'),
(i==rowCnt-1 ? rowHeightLast : rowHeight) - vsides(cell)
);
}
});
}
function setWidth(width) {
viewWidth = width;
colContentPositions.clear();
colWidth = Math.floor(viewWidth / colCnt);
setOuterWidth(headCells.slice(0, -1), colWidth);
}
/* Day clicking and binding
-----------------------------------------------------------*/
function dayBind(days) {
days.click(dayClick)
.mousedown(daySelectionMousedown);
}
function dayClick(ev) {
if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick
var index = parseInt(this.className.match(/fc\-day(\d+)/)[1]); // TODO: maybe use .data
var date = indexDate(index);
trigger('dayClick', this, date, true, ev);
}
}
/* Semi-transparent Overlay Helpers
------------------------------------------------------*/
function renderDayOverlay(overlayStart, overlayEnd, refreshCoordinateGrid) { // overlayEnd is exclusive
if (refreshCoordinateGrid) {
coordinateGrid.build();
}
var rowStart = cloneDate(t.visStart);
var rowEnd = addDays(cloneDate(rowStart), colCnt);
for (var i=0; i<rowCnt; i++) {
var stretchStart = new Date(Math.max(rowStart, overlayStart));
var stretchEnd = new Date(Math.min(rowEnd, overlayEnd));
if (stretchStart < stretchEnd) {
var colStart, colEnd;
if (rtl) {
colStart = dayDiff(stretchEnd, rowStart)*dis+dit+1;
colEnd = dayDiff(stretchStart, rowStart)*dis+dit+1;
}else{
colStart = dayDiff(stretchStart, rowStart);
colEnd = dayDiff(stretchEnd, rowStart);
}
dayBind(
renderCellOverlay(i, colStart, i, colEnd-1)
);
}
addDays(rowStart, 7);
addDays(rowEnd, 7);
}
}
function renderCellOverlay(row0, col0, row1, col1) { // row1,col1 is inclusive
var rect = coordinateGrid.rect(row0, col0, row1, col1, element);
return renderOverlay(rect, element);
}
/* Selection
-----------------------------------------------------------------------*/
function defaultSelectionEnd(startDate, allDay) {
return cloneDate(startDate);
}
function renderSelection(startDate, endDate, allDay) {
renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true); // rebuild every time???
}
function clearSelection() {
clearOverlays();
}
function reportDayClick(date, allDay, ev) {
var cell = dateCell(date);
var _element = bodyCells[cell.row*colCnt + cell.col];
trigger('dayClick', _element, date, allDay, ev);
}
/* External Dragging
-----------------------------------------------------------------------*/
function dragStart(_dragElement, ev, ui) {
hoverListener.start(function(cell) {
clearOverlays();
if (cell) {
renderCellOverlay(cell.row, cell.col, cell.row, cell.col);
}
}, ev);
}
function dragStop(_dragElement, ev, ui) {
var cell = hoverListener.stop();
clearOverlays();
if (cell) {
var d = cellDate(cell);
trigger('drop', _dragElement, d, true, ev, ui);
}
}
/* Utilities
--------------------------------------------------------*/
function defaultEventEnd(event) {
return cloneDate(event.start);
}
coordinateGrid = new CoordinateGrid(function(rows, cols) {
var e, n, p;
headCells.each(function(i, _e) {
e = $(_e);
n = e.offset().left;
if (i) {
p[1] = n;
}
p = [n];
cols[i] = p;
});
p[1] = n + e.outerWidth();
bodyRows.each(function(i, _e) {
if (i < rowCnt) {
e = $(_e);
n = e.offset().top;
if (i) {
p[1] = n;
}
p = [n];
rows[i] = p;
}
});
p[1] = n + e.outerHeight();
});
hoverListener = new HoverListener(coordinateGrid);
colContentPositions = new HorizontalPositionCache(function(col) {
return bodyCellTopInners.eq(col);
});
function colContentLeft(col) {
return colContentPositions.left(col);
}
function colContentRight(col) {
return colContentPositions.right(col);
}
function dateCell(date) {
return {
row: Math.floor(dayDiff(date, t.visStart) / 7),
col: dayOfWeekCol(date.getDay())
};
}
function cellDate(cell) {
return _cellDate(cell.row, cell.col);
}
function _cellDate(row, col) {
return addDays(cloneDate(t.visStart), row*7 + col*dis+dit);
// what about weekends in middle of week?
}
function indexDate(index) {
return _cellDate(Math.floor(index/colCnt), index%colCnt);
}
function dayOfWeekCol(dayOfWeek) {
return ((dayOfWeek - Math.max(firstDay, nwe) + colCnt) % colCnt) * dis + dit;
}
function allDayRow(i) {
return bodyRows.eq(i);
}
function allDayBounds(i) {
return {
left: 0,
right: viewWidth
};
}
}
function BasicEventRenderer() {
var t = this;
// exports
t.renderEvents = renderEvents;
t.compileDaySegs = compileSegs; // for DayEventRenderer
t.clearEvents = clearEvents;
t.bindDaySeg = bindDaySeg;
// imports
DayEventRenderer.call(t);
var opt = t.opt;
var trigger = t.trigger;
//var setOverflowHidden = t.setOverflowHidden;
var isEventDraggable = t.isEventDraggable;
var isEventResizable = t.isEventResizable;
var reportEvents = t.reportEvents;
var reportEventClear = t.reportEventClear;
var eventElementHandlers = t.eventElementHandlers;
var showEvents = t.showEvents;
var hideEvents = t.hideEvents;
var eventDrop = t.eventDrop;
var getDaySegmentContainer = t.getDaySegmentContainer;
var getHoverListener = t.getHoverListener;
var renderDayOverlay = t.renderDayOverlay;
var clearOverlays = t.clearOverlays;
var getRowCnt = t.getRowCnt;
var getColCnt = t.getColCnt;
var renderDaySegs = t.renderDaySegs;
var resizableDayEvent = t.resizableDayEvent;
/* Rendering
--------------------------------------------------------------------*/
function renderEvents(events, modifiedEventId) {
reportEvents(events);
renderDaySegs(compileSegs(events), modifiedEventId);
}
function clearEvents() {
reportEventClear();
getDaySegmentContainer().empty();
}
function compileSegs(events) {
var rowCnt = getRowCnt(),
colCnt = getColCnt(),
d1 = cloneDate(t.visStart),
d2 = addDays(cloneDate(d1), colCnt),
visEventsEnds = $.map(events, exclEndDay),
i, row,
j, level,
k, seg,
segs=[];
for (i=0; i<rowCnt; i++) {
row = stackSegs(sliceSegs(events, visEventsEnds, d1, d2));
for (j=0; j<row.length; j++) {
level = row[j];
for (k=0; k<level.length; k++) {
seg = level[k];
seg.row = i;
seg.level = j; // not needed anymore
segs.push(seg);
}
}
addDays(d1, 7);
addDays(d2, 7);
}
return segs;
}
function bindDaySeg(event, eventElement, seg) {
if (isEventDraggable(event)) {
draggableDayEvent(event, eventElement);
}
if (seg.isEnd && isEventResizable(event)) {
resizableDayEvent(event, eventElement, seg);
}
eventElementHandlers(event, eventElement);
// needs to be after, because resizableDayEvent might stopImmediatePropagation on click
}
/* Dragging
----------------------------------------------------------------------------*/
function draggableDayEvent(event, eventElement) {
var hoverListener = getHoverListener();
var dayDelta;
eventElement.draggable({
zIndex: 9,
delay: 50,
opacity: opt('dragOpacity'),
revertDuration: opt('dragRevertDuration'),
start: function(ev, ui) {
trigger('eventDragStart', eventElement, event, ev, ui);
hideEvents(event, eventElement);
hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
eventElement.draggable('option', 'revert', !cell || !rowDelta && !colDelta);
clearOverlays();
if (cell) {
//setOverflowHidden(true);
dayDelta = rowDelta*7 + colDelta * (opt('isRTL') ? -1 : 1);
renderDayOverlay(
addDays(cloneDate(event.start), dayDelta),
addDays(exclEndDay(event), dayDelta)
);
}else{
//setOverflowHidden(false);
dayDelta = 0;
}
}, ev, 'drag');
},
stop: function(ev, ui) {
hoverListener.stop();
clearOverlays();
trigger('eventDragStop', eventElement, event, ev, ui);
if (dayDelta) {
eventDrop(this, event, dayDelta, 0, event.allDay, ev, ui);
}else{
eventElement.css('filter', ''); // clear IE opacity side-effects
showEvents(event, eventElement);
}
//setOverflowHidden(false);
}
});
}
}
fcViews.agendaWeek = AgendaWeekView;
function AgendaWeekView(element, calendar) {
var t = this;
// exports
t.render = render;
// imports
AgendaView.call(t, element, calendar, 'agendaWeek');
var opt = t.opt;
var renderAgenda = t.renderAgenda;
var formatDates = calendar.formatDates;
function render(date, delta) {
if (delta) {
addDays(date, delta * 7);
}
var start = addDays(cloneDate(date), -((date.getDay() - opt('firstDay') + 7) % 7));
var end = addDays(cloneDate(start), 7);
var visStart = cloneDate(start);
var visEnd = cloneDate(end);
var weekends = opt('weekends');
if (!weekends) {
skipWeekend(visStart);
skipWeekend(visEnd, -1, true);
}
t.title = formatDates(
visStart,
addDays(cloneDate(visEnd), -1),
opt('titleFormat')
);
t.start = start;
t.end = end;
t.visStart = visStart;
t.visEnd = visEnd;
renderAgenda(weekends ? 7 : 5);
}
}
fcViews.agendaDay = AgendaDayView;
function AgendaDayView(element, calendar) {
var t = this;
// exports
t.render = render;
// imports
AgendaView.call(t, element, calendar, 'agendaDay');
var opt = t.opt;
var renderAgenda = t.renderAgenda;
var formatDate = calendar.formatDate;
function render(date, delta) {
if (delta) {
addDays(date, delta);
if (!opt('weekends')) {
skipWeekend(date, delta < 0 ? -1 : 1);
}
}
var start = cloneDate(date, true);
var end = addDays(cloneDate(start), 1);
t.title = formatDate(date, opt('titleFormat'));
t.start = t.visStart = start;
t.end = t.visEnd = end;
renderAgenda(1);
}
}
setDefaults({
allDaySlot: true,
allDayText: 'all-day',
firstHour: 6,
slotMinutes: 30,
defaultEventMinutes: 120,
axisFormat: 'h(:mm)tt',
timeFormat: {
agenda: 'h:mm{ - h:mm}'
},
dragOpacity: {
agenda: .5
},
minTime: 0,
maxTime: 24
});
// TODO: make it work in quirks mode (event corners, all-day height)
// TODO: test liquid width, especially in IE6
function AgendaView(element, calendar, viewName) {
var t = this;
// exports
t.renderAgenda = renderAgenda;
t.setWidth = setWidth;
t.setHeight = setHeight;
t.beforeHide = beforeHide;
t.afterShow = afterShow;
t.defaultEventEnd = defaultEventEnd;
t.timePosition = timePosition;
t.dayOfWeekCol = dayOfWeekCol;
t.dateCell = dateCell;
t.cellDate = cellDate;
t.cellIsAllDay = cellIsAllDay;
t.allDayRow = getAllDayRow;
t.allDayBounds = allDayBounds;
t.getHoverListener = function() { return hoverListener };
t.colContentLeft = colContentLeft;
t.colContentRight = colContentRight;
t.getDaySegmentContainer = function() { return daySegmentContainer };
t.getSlotSegmentContainer = function() { return slotSegmentContainer };
t.getMinMinute = function() { return minMinute };
t.getMaxMinute = function() { return maxMinute };
t.getBodyContent = function() { return slotContent }; // !!??
t.getRowCnt = function() { return 1 };
t.getColCnt = function() { return colCnt };
t.getColWidth = function() { return colWidth };
t.getSlotHeight = function() { return slotHeight };
t.defaultSelectionEnd = defaultSelectionEnd;
t.renderDayOverlay = renderDayOverlay;
t.renderSelection = renderSelection;
t.clearSelection = clearSelection;
t.reportDayClick = reportDayClick; // selection mousedown hack
t.dragStart = dragStart;
t.dragStop = dragStop;
// imports
View.call(t, element, calendar, viewName);
OverlayManager.call(t);
SelectionManager.call(t);
AgendaEventRenderer.call(t);
var opt = t.opt;
var trigger = t.trigger;
var clearEvents = t.clearEvents;
var renderOverlay = t.renderOverlay;
var clearOverlays = t.clearOverlays;
var reportSelection = t.reportSelection;
var unselect = t.unselect;
var daySelectionMousedown = t.daySelectionMousedown;
var slotSegHtml = t.slotSegHtml;
var formatDate = calendar.formatDate;
// locals
var dayTable;
var dayHead;
var dayHeadCells;
var dayBody;
var dayBodyCells;
var dayBodyCellInners;
var dayBodyFirstCell;
var dayBodyFirstCellStretcher;
var slotLayer;
var daySegmentContainer;
var allDayTable;
var allDayRow;
var slotScroller;
var slotContent;
var slotSegmentContainer;
var slotTable;
var slotTableFirstInner;
var axisFirstCells;
var gutterCells;
var selectionHelper;
var viewWidth;
var viewHeight;
var axisWidth;
var colWidth;
var gutterWidth;
var slotHeight; // TODO: what if slotHeight changes? (see issue 650)
var savedScrollTop;
var colCnt;
var slotCnt;
var coordinateGrid;
var hoverListener;
var colContentPositions;
var slotTopCache = {};
var tm;
var firstDay;
var nwe; // no weekends (int)
var rtl, dis, dit; // day index sign / translate
var minMinute, maxMinute;
var colFormat;
/* Rendering
-----------------------------------------------------------------------------*/
disableTextSelection(element.addClass('fc-agenda'));
function renderAgenda(c) {
colCnt = c;
updateOptions();
if (!dayTable) {
buildSkeleton();
}else{
clearEvents();
}
updateCells();
}
function updateOptions() {
tm = opt('theme') ? 'ui' : 'fc';
nwe = opt('weekends') ? 0 : 1;
firstDay = opt('firstDay');
if (rtl = opt('isRTL')) {
dis = -1;
dit = colCnt - 1;
}else{
dis = 1;
dit = 0;
}
minMinute = parseTime(opt('minTime'));
maxMinute = parseTime(opt('maxTime'));
colFormat = opt('columnFormat');
}
function buildSkeleton() {
var headerClass = tm + "-widget-header";
var contentClass = tm + "-widget-content";
var s;
var i;
var d;
var maxd;
var minutes;
var slotNormal = opt('slotMinutes') % 15 == 0;
s =
"<table style='width:100%' class='fc-agenda-days fc-border-separate' cellspacing='0'>" +
"<thead>" +
"<tr>" +
"<th class='fc-agenda-axis " + headerClass + "'>&nbsp;</th>";
for (i=0; i<colCnt; i++) {
s +=
"<th class='fc- fc-col" + i + ' ' + headerClass + "'/>"; // fc- needed for setDayID
}
s +=
"<th class='fc-agenda-gutter " + headerClass + "'>&nbsp;</th>" +
"</tr>" +
"</thead>" +
"<tbody>" +
"<tr>" +
"<th class='fc-agenda-axis " + headerClass + "'>&nbsp;</th>";
for (i=0; i<colCnt; i++) {
s +=
"<td class='fc- fc-col" + i + ' ' + contentClass + "'>" + // fc- needed for setDayID
"<div>" +
"<div class='fc-day-content'>" +
"<div style='position:relative'>&nbsp;</div>" +
"</div>" +
"</div>" +
"</td>";
}
s +=
"<td class='fc-agenda-gutter " + contentClass + "'>&nbsp;</td>" +
"</tr>" +
"</tbody>" +
"</table>";
dayTable = $(s).appendTo(element);
dayHead = dayTable.find('thead');
dayHeadCells = dayHead.find('th').slice(1, -1);
dayBody = dayTable.find('tbody');
dayBodyCells = dayBody.find('td').slice(0, -1);
dayBodyCellInners = dayBodyCells.find('div.fc-day-content div');
dayBodyFirstCell = dayBodyCells.eq(0);
dayBodyFirstCellStretcher = dayBodyFirstCell.find('> div');
markFirstLast(dayHead.add(dayHead.find('tr')));
markFirstLast(dayBody.add(dayBody.find('tr')));
axisFirstCells = dayHead.find('th:first');
gutterCells = dayTable.find('.fc-agenda-gutter');
slotLayer =
$("<div style='position:absolute;z-index:2;left:0;width:100%'/>")
.appendTo(element);
if (opt('allDaySlot')) {
daySegmentContainer =
$("<div style='position:absolute;z-index:8;top:0;left:0'/>")
.appendTo(slotLayer);
s =
"<table style='width:100%' class='fc-agenda-allday' cellspacing='0'>" +
"<tr>" +
"<th class='" + headerClass + " fc-agenda-axis'>" + opt('allDayText') + "</th>" +
"<td>" +
"<div class='fc-day-content'><div style='position:relative'/></div>" +
"</td>" +
"<th class='" + headerClass + " fc-agenda-gutter'>&nbsp;</th>" +
"</tr>" +
"</table>";
allDayTable = $(s).appendTo(slotLayer);
allDayRow = allDayTable.find('tr');
dayBind(allDayRow.find('td'));
axisFirstCells = axisFirstCells.add(allDayTable.find('th:first'));
gutterCells = gutterCells.add(allDayTable.find('th.fc-agenda-gutter'));
slotLayer.append(
"<div class='fc-agenda-divider " + headerClass + "'>" +
"<div class='fc-agenda-divider-inner'/>" +
"</div>"
);
}else{
daySegmentContainer = $([]); // in jQuery 1.4, we can just do $()
}
slotScroller =
$("<div style='position:absolute;width:100%;overflow-x:hidden;overflow-y:auto'/>")
.appendTo(slotLayer);
slotContent =
$("<div style='position:relative;width:100%;overflow:hidden'/>")
.appendTo(slotScroller);
slotSegmentContainer =
$("<div style='position:absolute;z-index:8;top:0;left:0'/>")
.appendTo(slotContent);
s =
"<table class='fc-agenda-slots' style='width:100%' cellspacing='0'>" +
"<tbody>";
d = zeroDate();
maxd = addMinutes(cloneDate(d), maxMinute);
addMinutes(d, minMinute);
slotCnt = 0;
for (i=0; d < maxd; i++) {
minutes = d.getMinutes();
s +=
"<tr class='fc-slot" + i + ' ' + (!minutes ? '' : 'fc-minor') + "'>" +
"<th class='fc-agenda-axis " + headerClass + "'>" +
((!slotNormal || !minutes) ? formatDate(d, opt('axisFormat')) : '&nbsp;') +
"</th>" +
"<td class='" + contentClass + "'>" +
"<div style='position:relative'>&nbsp;</div>" +
"</td>" +
"</tr>";
addMinutes(d, opt('slotMinutes'));
slotCnt++;
}
s +=
"</tbody>" +
"</table>";
slotTable = $(s).appendTo(slotContent);
slotTableFirstInner = slotTable.find('div:first');
slotBind(slotTable.find('td'));
axisFirstCells = axisFirstCells.add(slotTable.find('th:first'));
}
function updateCells() {
var i;
var headCell;
var bodyCell;
var date;
var today = clearTime(new Date());
for (i=0; i<colCnt; i++) {
date = colDate(i);
headCell = dayHeadCells.eq(i);
headCell.html(formatDate(date, colFormat));
bodyCell = dayBodyCells.eq(i);
if (+date == +today) {
bodyCell.addClass(tm + '-state-highlight fc-today');
}else{
bodyCell.removeClass(tm + '-state-highlight fc-today');
}
setDayID(headCell.add(bodyCell), date);
}
}
function setHeight(height, dateChanged) {
if (height === undefined) {
height = viewHeight;
}
viewHeight = height;
slotTopCache = {};
var headHeight = dayBody.position().top;
var allDayHeight = slotScroller.position().top; // including divider
var bodyHeight = Math.min( // total body height, including borders
height - headHeight, // when scrollbars
slotTable.height() + allDayHeight + 1 // when no scrollbars. +1 for bottom border
);
dayBodyFirstCellStretcher
.height(bodyHeight - vsides(dayBodyFirstCell));
slotLayer.css('top', headHeight);
slotScroller.height(bodyHeight - allDayHeight - 1);
slotHeight = slotTableFirstInner.height() + 1; // +1 for border
if (dateChanged) {
resetScroll();
}
}
function setWidth(width) {
viewWidth = width;
colContentPositions.clear();
axisWidth = 0;
setOuterWidth(
axisFirstCells
.width('')
.each(function(i, _cell) {
axisWidth = Math.max(axisWidth, $(_cell).outerWidth());
}),
axisWidth
);
var slotTableWidth = slotScroller[0].clientWidth; // needs to be done after axisWidth (for IE7)
//slotTable.width(slotTableWidth);
gutterWidth = slotScroller.width() - slotTableWidth;
if (gutterWidth) {
setOuterWidth(gutterCells, gutterWidth);
gutterCells
.show()
.prev()
.removeClass('fc-last');
}else{
gutterCells
.hide()
.prev()
.addClass('fc-last');
}
colWidth = Math.floor((slotTableWidth - axisWidth) / colCnt);
setOuterWidth(dayHeadCells.slice(0, -1), colWidth);
}
function resetScroll() {
var d0 = zeroDate();
var scrollDate = cloneDate(d0);
scrollDate.setHours(opt('firstHour'));
var top = timePosition(d0, scrollDate) + 1; // +1 for the border
function scroll() {
slotScroller.scrollTop(top);
}
scroll();
setTimeout(scroll, 0); // overrides any previous scroll state made by the browser
}
function beforeHide() {
savedScrollTop = slotScroller.scrollTop();
}
function afterShow() {
slotScroller.scrollTop(savedScrollTop);
}
/* Slot/Day clicking and binding
-----------------------------------------------------------------------*/
function dayBind(cells) {
cells.click(slotClick)
.mousedown(daySelectionMousedown);
}
function slotBind(cells) {
cells.click(slotClick)
.mousedown(slotSelectionMousedown);
}
function slotClick(ev) {
if (!opt('selectable')) { // if selectable, SelectionManager will worry about dayClick
var col = Math.min(colCnt-1, Math.floor((ev.pageX - dayTable.offset().left - axisWidth) / colWidth));
var date = colDate(col);
var rowMatch = this.parentNode.className.match(/fc-slot(\d+)/); // TODO: maybe use data
if (rowMatch) {
var mins = parseInt(rowMatch[1]) * opt('slotMinutes');
var hours = Math.floor(mins/60);
date.setHours(hours);
date.setMinutes(mins%60 + minMinute);
trigger('dayClick', dayBodyCells[col], date, false, ev);
}else{
trigger('dayClick', dayBodyCells[col], date, true, ev);
}
}
}
/* Semi-transparent Overlay Helpers
-----------------------------------------------------*/
function renderDayOverlay(startDate, endDate, refreshCoordinateGrid) { // endDate is exclusive
if (refreshCoordinateGrid) {
coordinateGrid.build();
}
var visStart = cloneDate(t.visStart);
var startCol, endCol;
if (rtl) {
startCol = dayDiff(endDate, visStart)*dis+dit+1;
endCol = dayDiff(startDate, visStart)*dis+dit+1;
}else{
startCol = dayDiff(startDate, visStart);
endCol = dayDiff(endDate, visStart);
}
startCol = Math.max(0, startCol);
endCol = Math.min(colCnt, endCol);
if (startCol < endCol) {
dayBind(
renderCellOverlay(0, startCol, 0, endCol-1)
);
}
}
function renderCellOverlay(row0, col0, row1, col1) { // only for all-day?
var rect = coordinateGrid.rect(row0, col0, row1, col1, slotLayer);
return renderOverlay(rect, slotLayer);
}
function renderSlotOverlay(overlayStart, overlayEnd) {
var dayStart = cloneDate(t.visStart);
var dayEnd = addDays(cloneDate(dayStart), 1);
for (var i=0; i<colCnt; i++) {
var stretchStart = new Date(Math.max(dayStart, overlayStart));
var stretchEnd = new Date(Math.min(dayEnd, overlayEnd));
if (stretchStart < stretchEnd) {
var col = i*dis+dit;
var rect = coordinateGrid.rect(0, col, 0, col, slotContent); // only use it for horizontal coords
var top = timePosition(dayStart, stretchStart);
var bottom = timePosition(dayStart, stretchEnd);
rect.top = top;
rect.height = bottom - top;
slotBind(
renderOverlay(rect, slotContent)
);
}
addDays(dayStart, 1);
addDays(dayEnd, 1);
}
}
/* Coordinate Utilities
-----------------------------------------------------------------------------*/
coordinateGrid = new CoordinateGrid(function(rows, cols) {
var e, n, p;
dayHeadCells.each(function(i, _e) {
e = $(_e);
n = e.offset().left;
if (i) {
p[1] = n;
}
p = [n];
cols[i] = p;
});
p[1] = n + e.outerWidth();
if (opt('allDaySlot')) {
e = allDayRow;
n = e.offset().top;
rows[0] = [n, n+e.outerHeight()];
}
var slotTableTop = slotContent.offset().top;
var slotScrollerTop = slotScroller.offset().top;
var slotScrollerBottom = slotScrollerTop + slotScroller.outerHeight();
function constrain(n) {
return Math.max(slotScrollerTop, Math.min(slotScrollerBottom, n));
}
for (var i=0; i<slotCnt; i++) {
rows.push([
constrain(slotTableTop + slotHeight*i),
constrain(slotTableTop + slotHeight*(i+1))
]);
}
});
hoverListener = new HoverListener(coordinateGrid);
colContentPositions = new HorizontalPositionCache(function(col) {
return dayBodyCellInners.eq(col);
});
function colContentLeft(col) {
return colContentPositions.left(col);
}
function colContentRight(col) {
return colContentPositions.right(col);
}
function dateCell(date) { // "cell" terminology is now confusing
return {
row: Math.floor(dayDiff(date, t.visStart) / 7),
col: dayOfWeekCol(date.getDay())
};
}
function cellDate(cell) {
var d = colDate(cell.col);
var slotIndex = cell.row;
if (opt('allDaySlot')) {
slotIndex--;
}
if (slotIndex >= 0) {
addMinutes(d, minMinute + slotIndex * opt('slotMinutes'));
}
return d;
}
function colDate(col) { // returns dates with 00:00:00
return addDays(cloneDate(t.visStart), col*dis+dit);
}
function cellIsAllDay(cell) {
return opt('allDaySlot') && !cell.row;
}
function dayOfWeekCol(dayOfWeek) {
return ((dayOfWeek - Math.max(firstDay, nwe) + colCnt) % colCnt)*dis+dit;
}
// get the Y coordinate of the given time on the given day (both Date objects)
function timePosition(day, time) { // both date objects. day holds 00:00 of current day
day = cloneDate(day, true);
if (time < addMinutes(cloneDate(day), minMinute)) {
return 0;
}
if (time >= addMinutes(cloneDate(day), maxMinute)) {
return slotTable.height();
}
var slotMinutes = opt('slotMinutes'),
minutes = time.getHours()*60 + time.getMinutes() - minMinute,
slotI = Math.floor(minutes / slotMinutes),
slotTop = slotTopCache[slotI];
if (slotTop === undefined) {
slotTop = slotTopCache[slotI] = slotTable.find('tr:eq(' + slotI + ') td div')[0].offsetTop; //.position().top; // need this optimization???
}
return Math.max(0, Math.round(
slotTop - 1 + slotHeight * ((minutes % slotMinutes) / slotMinutes)
));
}
function allDayBounds() {
return {
left: axisWidth,
right: viewWidth - gutterWidth
}
}
function getAllDayRow(index) {
return allDayRow;
}
function defaultEventEnd(event) {
var start = cloneDate(event.start);
if (event.allDay) {
return start;
}
return addMinutes(start, opt('defaultEventMinutes'));
}
/* Selection
---------------------------------------------------------------------------------*/
function defaultSelectionEnd(startDate, allDay) {
if (allDay) {
return cloneDate(startDate);
}
return addMinutes(cloneDate(startDate), opt('slotMinutes'));
}
function renderSelection(startDate, endDate, allDay) { // only for all-day
if (allDay) {
if (opt('allDaySlot')) {
renderDayOverlay(startDate, addDays(cloneDate(endDate), 1), true);
}
}else{
renderSlotSelection(startDate, endDate);
}
}
function renderSlotSelection(startDate, endDate) {
var helperOption = opt('selectHelper');
coordinateGrid.build();
if (helperOption) {
var col = dayDiff(startDate, t.visStart) * dis + dit;
if (col >= 0 && col < colCnt) { // only works when times are on same day
var rect = coordinateGrid.rect(0, col, 0, col, slotContent); // only for horizontal coords
var top = timePosition(startDate, startDate);
var bottom = timePosition(startDate, endDate);
if (bottom > top) { // protect against selections that are entirely before or after visible range
rect.top = top;
rect.height = bottom - top;
rect.left += 2;
rect.width -= 5;
if ($.isFunction(helperOption)) {
var helperRes = helperOption(startDate, endDate);
if (helperRes) {
rect.position = 'absolute';
rect.zIndex = 8;
selectionHelper = $(helperRes)
.css(rect)
.appendTo(slotContent);
}
}else{
rect.isStart = true; // conside rect a "seg" now
rect.isEnd = true; //
selectionHelper = $(slotSegHtml(
{
title: '',
start: startDate,
end: endDate,
className: ['fc-select-helper'],
editable: false
},
rect
));
selectionHelper.css('opacity', opt('dragOpacity'));
}
if (selectionHelper) {
slotBind(selectionHelper);
slotContent.append(selectionHelper);
setOuterWidth(selectionHelper, rect.width, true); // needs to be after appended
setOuterHeight(selectionHelper, rect.height, true);
}
}
}
}else{
renderSlotOverlay(startDate, endDate);
}
}
function clearSelection() {
clearOverlays();
if (selectionHelper) {
selectionHelper.remove();
selectionHelper = null;
}
}
function slotSelectionMousedown(ev) {
if (ev.which == 1 && opt('selectable')) { // ev.which==1 means left mouse button
unselect(ev);
var dates, helperOption = opt('selectHelper');
hoverListener.start(function(cell, origCell) {
clearSelection();
if (cell && (cell.col == origCell.col || !helperOption) && !cellIsAllDay(cell)) {
var d1 = cellDate(origCell);
var d2 = cellDate(cell);
dates = [
d1,
addMinutes(cloneDate(d1), opt('slotMinutes')),
d2,
addMinutes(cloneDate(d2), opt('slotMinutes'))
].sort(cmp);
renderSlotSelection(dates[0], dates[3]);
}else{
dates = null;
}
}, ev);
$(document).one('mouseup', function(ev) {
hoverListener.stop();
if (dates) {
if (+dates[0] == +dates[1]) {
reportDayClick(dates[0], false, ev);
}
reportSelection(dates[0], dates[3], false, ev);
}
});
}
}
function reportDayClick(date, allDay, ev) {
trigger('dayClick', dayBodyCells[dayOfWeekCol(date.getDay())], date, allDay, ev);
}
/* External Dragging
--------------------------------------------------------------------------------*/
function dragStart(_dragElement, ev, ui) {
hoverListener.start(function(cell) {
clearOverlays();
if (cell) {
if (cellIsAllDay(cell)) {
renderCellOverlay(cell.row, cell.col, cell.row, cell.col);
}else{
var d1 = cellDate(cell);
var d2 = addMinutes(cloneDate(d1), opt('defaultEventMinutes'));
renderSlotOverlay(d1, d2);
}
}
}, ev);
}
function dragStop(_dragElement, ev, ui) {
var cell = hoverListener.stop();
clearOverlays();
if (cell) {
trigger('drop', _dragElement, cellDate(cell), cellIsAllDay(cell), ev, ui);
}
}
}
function AgendaEventRenderer() {
var t = this;
// exports
t.renderEvents = renderEvents;
t.compileDaySegs = compileDaySegs; // for DayEventRenderer
t.clearEvents = clearEvents;
t.slotSegHtml = slotSegHtml;
t.bindDaySeg = bindDaySeg;
// imports
DayEventRenderer.call(t);
var opt = t.opt;
var trigger = t.trigger;
//var setOverflowHidden = t.setOverflowHidden;
var isEventDraggable = t.isEventDraggable;
var isEventResizable = t.isEventResizable;
var eventEnd = t.eventEnd;
var reportEvents = t.reportEvents;
var reportEventClear = t.reportEventClear;
var eventElementHandlers = t.eventElementHandlers;
var setHeight = t.setHeight;
var getDaySegmentContainer = t.getDaySegmentContainer;
var getSlotSegmentContainer = t.getSlotSegmentContainer;
var getHoverListener = t.getHoverListener;
var getMaxMinute = t.getMaxMinute;
var getMinMinute = t.getMinMinute;
var timePosition = t.timePosition;
var colContentLeft = t.colContentLeft;
var colContentRight = t.colContentRight;
var renderDaySegs = t.renderDaySegs;
var resizableDayEvent = t.resizableDayEvent; // TODO: streamline binding architecture
var getColCnt = t.getColCnt;
var getColWidth = t.getColWidth;
var getSlotHeight = t.getSlotHeight;
var getBodyContent = t.getBodyContent;
var reportEventElement = t.reportEventElement;
var showEvents = t.showEvents;
var hideEvents = t.hideEvents;
var eventDrop = t.eventDrop;
var eventResize = t.eventResize;
var renderDayOverlay = t.renderDayOverlay;
var clearOverlays = t.clearOverlays;
var calendar = t.calendar;
var formatDate = calendar.formatDate;
var formatDates = calendar.formatDates;
var timeLineInterval;
+
/* Rendering
----------------------------------------------------------------------------*/
function renderEvents(events, modifiedEventId) {
reportEvents(events);
var i, len=events.length,
dayEvents=[],
slotEvents=[];
for (i=0; i<len; i++) {
if (events[i].allDay) {
dayEvents.push(events[i]);
}else{
slotEvents.push(events[i]);
}
}
if (opt('allDaySlot')) {
renderDaySegs(compileDaySegs(dayEvents), modifiedEventId);
setHeight(); // no params means set to viewHeight
}
renderSlotSegs(compileSlotSegs(slotEvents), modifiedEventId);
if (opt('currentTimeIndicator')) {
window.clearInterval(timeLineInterval);
timeLineInterval = window.setInterval(setTimeIndicator, 30000);
setTimeIndicator();
}
}
function clearEvents() {
reportEventClear();
getDaySegmentContainer().empty();
getSlotSegmentContainer().empty();
}
function compileDaySegs(events) {
var levels = stackSegs(sliceSegs(events, $.map(events, exclEndDay), t.visStart, t.visEnd)),
i, levelCnt=levels.length, level,
j, seg,
segs=[];
for (i=0; i<levelCnt; i++) {
level = levels[i];
for (j=0; j<level.length; j++) {
seg = level[j];
seg.row = 0;
seg.level = i; // not needed anymore
segs.push(seg);
}
}
return segs;
}
function compileSlotSegs(events) {
var colCnt = getColCnt(),
minMinute = getMinMinute(),
maxMinute = getMaxMinute(),
d = addMinutes(cloneDate(t.visStart), minMinute),
visEventEnds = $.map(events, slotEventEnd),
i, col,
j, level,
k, seg,
segs=[];
for (i=0; i<colCnt; i++) {
col = stackSegs(sliceSegs(events, visEventEnds, d, addMinutes(cloneDate(d), maxMinute-minMinute)));
countForwardSegs(col);
for (j=0; j<col.length; j++) {
level = col[j];
for (k=0; k<level.length; k++) {
seg = level[k];
seg.col = i;
seg.level = j;
segs.push(seg);
}
}
addDays(d, 1, true);
}
return segs;
}
function slotEventEnd(event) {
if (event.end) {
return cloneDate(event.end);
}else{
return addMinutes(cloneDate(event.start), opt('defaultEventMinutes'));
}
}
// renders events in the 'time slots' at the bottom
function renderSlotSegs(segs, modifiedEventId) {
var i, segCnt=segs.length, seg,
event,
classes,
top, bottom,
colI, levelI, forward,
leftmost,
availWidth,
outerWidth,
left,
html='',
eventElements,
eventElement,
triggerRes,
vsideCache={},
hsideCache={},
key, val,
contentElement,
height,
slotSegmentContainer = getSlotSegmentContainer(),
rtl, dis, dit,
colCnt = getColCnt(),
overlapping = colCnt > 1;
if (rtl = opt('isRTL')) {
dis = -1;
dit = colCnt - 1;
}else{
dis = 1;
dit = 0;
}
// calculate position/dimensions, create html
for (i=0; i<segCnt; i++) {
seg = segs[i];
event = seg.event;
top = timePosition(seg.start, seg.start);
bottom = timePosition(seg.start, seg.end);
colI = seg.col;
levelI = seg.level;
forward = seg.forward || 0;
leftmost = colContentLeft(colI*dis + dit);
availWidth = colContentRight(colI*dis + dit) - leftmost;
availWidth = Math.min(availWidth-6, availWidth*.95); // TODO: move this to CSS
if (levelI) {
// indented and thin
outerWidth = availWidth / (levelI + forward + 1);
}else{
if (forward) {
if (overlapping) { // moderately wide, aligned left still
outerWidth = ((availWidth / (forward + 1)) - (12/2)) * 2; // 12 is the predicted width of resizer =
}else{
outerWidth = outerWidth = availWidth / (forward + 1);
}
}else{
// can be entire width, aligned left
outerWidth = availWidth;
}
}
left = leftmost + // leftmost possible
(availWidth / (levelI + forward + 1) * levelI) // indentation
* dis + (rtl ? availWidth - outerWidth : 0); // rtl
seg.top = top;
seg.left = left;
seg.outerWidth = outerWidth - (overlapping ? 0 : 1);
seg.outerHeight = bottom - top;
html += slotSegHtml(event, seg);
}
slotSegmentContainer[0].innerHTML = html; // faster than html()
eventElements = slotSegmentContainer.children();
// retrieve elements, run through eventRender callback, bind event handlers
for (i=0; i<segCnt; i++) {
seg = segs[i];
event = seg.event;
eventElement = $(eventElements[i]); // faster than eq()
triggerRes = trigger('eventRender', event, event, eventElement);
if (triggerRes === false) {
eventElement.remove();
}else{
if (triggerRes && triggerRes !== true) {
eventElement.remove();
eventElement = $(triggerRes)
.css({
position: 'absolute',
top: seg.top,
left: seg.left
})
.appendTo(slotSegmentContainer);
}
seg.element = eventElement;
if (event._id === modifiedEventId) {
bindSlotSeg(event, eventElement, seg);
}else{
eventElement[0]._fci = i; // for lazySegBind
}
reportEventElement(event, eventElement);
}
}
lazySegBind(slotSegmentContainer, segs, bindSlotSeg);
// record event sides and title positions
for (i=0; i<segCnt; i++) {
seg = segs[i];
if (eventElement = seg.element) {
val = vsideCache[key = seg.key = cssKey(eventElement[0])];
seg.vsides = val === undefined ? (vsideCache[key] = vsides(eventElement, true)) : val;
val = hsideCache[key];
seg.hsides = val === undefined ? (hsideCache[key] = hsides(eventElement, true)) : val;
contentElement = eventElement.find('div.fc-event-content');
if (contentElement.length) {
seg.contentTop = contentElement[0].offsetTop;
}
}
}
// set all positions/dimensions at once
for (i=0; i<segCnt; i++) {
seg = segs[i];
if (eventElement = seg.element) {
eventElement[0].style.width = Math.max(0, seg.outerWidth - seg.hsides) + 'px';
height = Math.max(0, seg.outerHeight - seg.vsides);
eventElement[0].style.height = height + 'px';
event = seg.event;
if (seg.contentTop !== undefined && height - seg.contentTop < 10) {
// not enough room for title, put it in the time header
eventElement.find('div.fc-event-time')
.text(formatDate(event.start, opt('timeFormat')) + ' - ' + event.title);
eventElement.find('div.fc-event-title')
.remove();
}
trigger('eventAfterRender', event, event, eventElement);
}
}
}
function slotSegHtml(event, seg) {
var html = "<";
var url = event.url;
var skinCss = getSkinCss(event, opt);
var skinCssAttr = (skinCss ? " style='" + skinCss + "'" : '');
var classes = ['fc-event', 'fc-event-skin', 'fc-event-vert'];
if (isEventDraggable(event)) {
classes.push('fc-event-draggable');
}
if (seg.isStart) {
classes.push('fc-corner-top');
}
if (seg.isEnd) {
classes.push('fc-corner-bottom');
}
classes = classes.concat(event.className);
if (event.source) {
classes = classes.concat(event.source.className || []);
}
if (url) {
html += "a href='" + htmlEscape(event.url) + "'";
}else{
html += "div";
}
html +=
" class='" + classes.join(' ') + "'" +
" style='position:absolute;z-index:8;top:" + seg.top + "px;left:" + seg.left + "px;" + skinCss + "'" +
">" +
"<div class='fc-event-inner fc-event-skin'" + skinCssAttr + ">" +
"<div class='fc-event-head fc-event-skin'" + skinCssAttr + ">" +
"<div class='fc-event-time'>" +
htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) +
"</div>" +
"</div>" +
"<div class='fc-event-content'>" +
"<div class='fc-event-title'>" +
htmlEscape(event.title) +
"</div>" +
"</div>" +
"<div class='fc-event-bg'></div>" +
"</div>"; // close inner
if (seg.isEnd && isEventResizable(event)) {
html +=
"<div class='ui-resizable-handle ui-resizable-s'>=</div>";
}
html +=
"</" + (url ? "a" : "div") + ">";
return html;
}
function bindDaySeg(event, eventElement, seg) {
if (isEventDraggable(event)) {
draggableDayEvent(event, eventElement, seg.isStart);
}
if (seg.isEnd && isEventResizable(event)) {
resizableDayEvent(event, eventElement, seg);
}
eventElementHandlers(event, eventElement);
// needs to be after, because resizableDayEvent might stopImmediatePropagation on click
}
function bindSlotSeg(event, eventElement, seg) {
var timeElement = eventElement.find('div.fc-event-time');
if (isEventDraggable(event)) {
draggableSlotEvent(event, eventElement, timeElement);
}
if (seg.isEnd && isEventResizable(event)) {
resizableSlotEvent(event, eventElement, timeElement);
}
eventElementHandlers(event, eventElement);
}
- // draw a horizontal line across the agenda view indicating the current time (#143)
+ // draw a horizontal line indicating the current time (#143)
function setTimeIndicator()
{
var container = getBodyContent();
var timeline = container.children('.fc-timeline');
if (timeline.length == 0) { // if timeline isn't there, add it
timeline = $('<hr>').addClass('fc-timeline').appendTo(container);
}
var cur_time = new Date();
if (t.visStart < cur_time && t.visEnd > cur_time) {
timeline.show();
}
else {
timeline.hide();
return;
}
var secs = (cur_time.getHours() * 60 * 60) + (cur_time.getMinutes() * 60) + cur_time.getSeconds();
var percents = secs / 86400; // 24 * 60 * 60 = 86400, # of seconds in a day
timeline.css('top', Math.floor(container.height() * percents - 1) + 'px');
if (t.name == 'agendaWeek') { // week view, don't want the timeline to go the whole way across
var daycol = $('.fc-today', t.element);
var left = daycol.position().left + 1;
var width = daycol.width();
timeline.css({ left: left + 'px', width: width + 'px' });
}
}
/* Dragging
-----------------------------------------------------------------------------------*/
// when event starts out FULL-DAY
function draggableDayEvent(event, eventElement, isStart) {
var origWidth;
var revert;
var allDay=true;
var dayDelta;
var dis = opt('isRTL') ? -1 : 1;
var hoverListener = getHoverListener();
var colWidth = getColWidth();
var slotHeight = getSlotHeight();
var minMinute = getMinMinute();
eventElement.draggable({
zIndex: 9,
opacity: opt('dragOpacity', 'month'), // use whatever the month view was using
revertDuration: opt('dragRevertDuration'),
start: function(ev, ui) {
trigger('eventDragStart', eventElement, event, ev, ui);
hideEvents(event, eventElement);
origWidth = eventElement.width();
hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
clearOverlays();
if (cell) {
//setOverflowHidden(true);
revert = false;
dayDelta = colDelta * dis;
if (!cell.row) {
// on full-days
renderDayOverlay(
addDays(cloneDate(event.start), dayDelta),
addDays(exclEndDay(event), dayDelta)
);
resetElement();
}else{
// mouse is over bottom slots
if (isStart) {
if (allDay) {
// convert event to temporary slot-event
eventElement.width(colWidth - 10); // don't use entire width
setOuterHeight(
eventElement,
slotHeight * Math.round(
(event.end ? ((event.end - event.start) / MINUTE_MS) : opt('defaultEventMinutes'))
/ opt('slotMinutes')
)
);
eventElement.draggable('option', 'grid', [colWidth, 1]);
allDay = false;
}
}else{
revert = true;
}
}
revert = revert || (allDay && !dayDelta);
}else{
resetElement();
//setOverflowHidden(false);
revert = true;
}
eventElement.draggable('option', 'revert', revert);
}, ev, 'drag');
},
stop: function(ev, ui) {
hoverListener.stop();
clearOverlays();
trigger('eventDragStop', eventElement, event, ev, ui);
if (revert) {
// hasn't moved or is out of bounds (draggable has already reverted)
resetElement();
eventElement.css('filter', ''); // clear IE opacity side-effects
showEvents(event, eventElement);
}else{
// changed!
var minuteDelta = 0;
if (!allDay) {
minuteDelta = Math.round((eventElement.offset().top - getBodyContent().offset().top) / slotHeight)
* opt('slotMinutes')
+ minMinute
- (event.start.getHours() * 60 + event.start.getMinutes());
}
eventDrop(this, event, dayDelta, minuteDelta, allDay, ev, ui);
}
//setOverflowHidden(false);
}
});
function resetElement() {
if (!allDay) {
eventElement
.width(origWidth)
.height('')
.draggable('option', 'grid', null);
allDay = true;
}
}
}
// when event starts out IN TIMESLOTS
function draggableSlotEvent(event, eventElement, timeElement) {
var origPosition;
var allDay=false;
var dayDelta;
var minuteDelta;
var prevMinuteDelta;
var dis = opt('isRTL') ? -1 : 1;
var hoverListener = getHoverListener();
var colCnt = getColCnt();
var colWidth = getColWidth();
var slotHeight = getSlotHeight();
eventElement.draggable({
zIndex: 9,
scroll: false,
grid: [colWidth, slotHeight],
axis: colCnt==1 ? 'y' : false,
opacity: opt('dragOpacity'),
revertDuration: opt('dragRevertDuration'),
start: function(ev, ui) {
trigger('eventDragStart', eventElement, event, ev, ui);
hideEvents(event, eventElement);
origPosition = eventElement.position();
minuteDelta = prevMinuteDelta = 0;
hoverListener.start(function(cell, origCell, rowDelta, colDelta) {
eventElement.draggable('option', 'revert', !cell);
clearOverlays();
if (cell) {
dayDelta = colDelta * dis;
if (opt('allDaySlot') && !cell.row) {
// over full days
if (!allDay) {
// convert to temporary all-day event
allDay = true;
timeElement.hide();
eventElement.draggable('option', 'grid', null);
}
renderDayOverlay(
addDays(cloneDate(event.start), dayDelta),
addDays(exclEndDay(event), dayDelta)
);
}else{
// on slots
resetElement();
}
}
}, ev, 'drag');
},
drag: function(ev, ui) {
minuteDelta = Math.round((ui.position.top - origPosition.top) / slotHeight) * opt('slotMinutes');
if (minuteDelta != prevMinuteDelta) {
if (!allDay) {
updateTimeText(minuteDelta);
}
prevMinuteDelta = minuteDelta;
}
},
stop: function(ev, ui) {
var cell = hoverListener.stop();
clearOverlays();
trigger('eventDragStop', eventElement, event, ev, ui);
if (cell && (dayDelta || minuteDelta || allDay)) {
// changed!
eventDrop(this, event, dayDelta, allDay ? 0 : minuteDelta, allDay, ev, ui);
}else{
// either no change or out-of-bounds (draggable has already reverted)
resetElement();
eventElement.css('filter', ''); // clear IE opacity side-effects
eventElement.css(origPosition); // sometimes fast drags make event revert to wrong position
updateTimeText(0);
showEvents(event, eventElement);
}
}
});
function updateTimeText(minuteDelta) {
var newStart = addMinutes(cloneDate(event.start), minuteDelta);
var newEnd;
if (event.end) {
newEnd = addMinutes(cloneDate(event.end), minuteDelta);
}
timeElement.text(formatDates(newStart, newEnd, opt('timeFormat')));
}
function resetElement() {
// convert back to original slot-event
if (allDay) {
timeElement.css('display', ''); // show() was causing display=inline
eventElement.draggable('option', 'grid', [colWidth, slotHeight]);
allDay = false;
}
}
}
/* Resizing
--------------------------------------------------------------------------------------*/
function resizableSlotEvent(event, eventElement, timeElement) {
var slotDelta, prevSlotDelta;
var slotHeight = getSlotHeight();
eventElement.resizable({
handles: {
s: 'div.ui-resizable-s'
},
grid: slotHeight,
start: function(ev, ui) {
slotDelta = prevSlotDelta = 0;
hideEvents(event, eventElement);
eventElement.css('z-index', 9);
trigger('eventResizeStart', this, event, ev, ui);
},
resize: function(ev, ui) {
// don't rely on ui.size.height, doesn't take grid into account
slotDelta = Math.round((Math.max(slotHeight, eventElement.height()) - ui.originalSize.height) / slotHeight);
if (slotDelta != prevSlotDelta) {
timeElement.text(
formatDates(
event.start,
(!slotDelta && !event.end) ? null : // no change, so don't display time range
addMinutes(eventEnd(event), opt('slotMinutes')*slotDelta),
opt('timeFormat')
)
);
prevSlotDelta = slotDelta;
}
},
stop: function(ev, ui) {
trigger('eventResizeStop', this, event, ev, ui);
if (slotDelta) {
eventResize(this, event, 0, opt('slotMinutes')*slotDelta, ev, ui);
}else{
eventElement.css('z-index', 8);
showEvents(event, eventElement);
// BUG: if event was really short, need to put title back in span
}
}
});
}
}
function countForwardSegs(levels) {
var i, j, k, level, segForward, segBack;
for (i=levels.length-1; i>0; i--) {
level = levels[i];
for (j=0; j<level.length; j++) {
segForward = level[j];
for (k=0; k<levels[i-1].length; k++) {
segBack = levels[i-1][k];
if (segsCollide(segForward, segBack)) {
segBack.forward = Math.max(segBack.forward||0, (segForward.forward||0)+1);
}
}
}
}
}
function View(element, calendar, viewName) {
var t = this;
// exports
t.element = element;
t.calendar = calendar;
t.name = viewName;
t.opt = opt;
t.trigger = trigger;
//t.setOverflowHidden = setOverflowHidden;
t.isEventDraggable = isEventDraggable;
t.isEventResizable = isEventResizable;
t.reportEvents = reportEvents;
t.eventEnd = eventEnd;
t.reportEventElement = reportEventElement;
t.reportEventClear = reportEventClear;
t.eventElementHandlers = eventElementHandlers;
t.showEvents = showEvents;
t.hideEvents = hideEvents;
t.eventDrop = eventDrop;
t.eventResize = eventResize;
// t.title
// t.start, t.end
// t.visStart, t.visEnd
// imports
var defaultEventEnd = t.defaultEventEnd;
var normalizeEvent = calendar.normalizeEvent; // in EventManager
var reportEventChange = calendar.reportEventChange;
// locals
var eventsByID = {};
var eventElements = [];
var eventElementsByID = {};
var options = calendar.options;
function opt(name, viewNameOverride) {
var v = options[name];
if (typeof v == 'object' && !v.length) {
return smartProperty(v, viewNameOverride || viewName);
}
return v;
}
function trigger(name, thisObj) {
return calendar.trigger.apply(
calendar,
[name, thisObj || t].concat(Array.prototype.slice.call(arguments, 2), [t])
);
}
/*
function setOverflowHidden(bool) {
element.css('overflow', bool ? 'hidden' : '');
}
*/
function isEventDraggable(event) {
return isEventEditable(event) && !opt('disableDragging');
}
function isEventResizable(event) { // but also need to make sure the seg.isEnd == true
return isEventEditable(event) && !opt('disableResizing');
}
function isEventEditable(event) {
return firstDefined(event.editable, (event.source || {}).editable, opt('editable'));
}
/* Event Data
------------------------------------------------------------------------------*/
// report when view receives new events
function reportEvents(events) { // events are already normalized at this point
eventsByID = {};
var i, len=events.length, event;
for (i=0; i<len; i++) {
event = events[i];
if (eventsByID[event._id]) {
eventsByID[event._id].push(event);
}else{
eventsByID[event._id] = [event];
}
}
}
// returns a Date object for an event's end
function eventEnd(event) {
return event.end ? cloneDate(event.end) : defaultEventEnd(event);
}
/* Event Elements
------------------------------------------------------------------------------*/
// report when view creates an element for an event
function reportEventElement(event, element) {
eventElements.push(element);
if (eventElementsByID[event._id]) {
eventElementsByID[event._id].push(element);
}else{
eventElementsByID[event._id] = [element];
}
}
function reportEventClear() {
eventElements = [];
eventElementsByID = {};
}
// attaches eventClick, eventMouseover, eventMouseout
function eventElementHandlers(event, eventElement) {
eventElement
.click(function(ev) {
if (!eventElement.hasClass('ui-draggable-dragging') &&
!eventElement.hasClass('ui-resizable-resizing')) {
return trigger('eventClick', this, event, ev);
}
})
.hover(
function(ev) {
trigger('eventMouseover', this, event, ev);
},
function(ev) {
trigger('eventMouseout', this, event, ev);
}
);
// TODO: don't fire eventMouseover/eventMouseout *while* dragging is occuring (on subject element)
// TODO: same for resizing
}
function showEvents(event, exceptElement) {
eachEventElement(event, exceptElement, 'show');
}
function hideEvents(event, exceptElement) {
eachEventElement(event, exceptElement, 'hide');
}
function eachEventElement(event, exceptElement, funcName) {
var elements = eventElementsByID[event._id],
i, len = elements.length;
for (i=0; i<len; i++) {
if (!exceptElement || elements[i][0] != exceptElement[0]) {
elements[i][funcName]();
}
}
}
/* Event Modification Reporting
---------------------------------------------------------------------------------*/
function eventDrop(e, event, dayDelta, minuteDelta, allDay, ev, ui) {
var oldAllDay = event.allDay;
var eventId = event._id;
moveEvents(eventsByID[eventId], dayDelta, minuteDelta, allDay);
trigger(
'eventDrop',
e,
event,
dayDelta,
minuteDelta,
allDay,
function() {
// TODO: investigate cases where this inverse technique might not work
moveEvents(eventsByID[eventId], -dayDelta, -minuteDelta, oldAllDay);
reportEventChange(eventId);
},
ev,
ui
);
reportEventChange(eventId);
}
function eventResize(e, event, dayDelta, minuteDelta, ev, ui) {
var eventId = event._id;
elongateEvents(eventsByID[eventId], dayDelta, minuteDelta);
trigger(
'eventResize',
e,
event,
dayDelta,
minuteDelta,
function() {
// TODO: investigate cases where this inverse technique might not work
elongateEvents(eventsByID[eventId], -dayDelta, -minuteDelta);
reportEventChange(eventId);
},
ev,
ui
);
reportEventChange(eventId);
}
/* Event Modification Math
---------------------------------------------------------------------------------*/
function moveEvents(events, dayDelta, minuteDelta, allDay) {
minuteDelta = minuteDelta || 0;
for (var e, len=events.length, i=0; i<len; i++) {
e = events[i];
if (allDay !== undefined) {
e.allDay = allDay;
}
addMinutes(addDays(e.start, dayDelta, true), minuteDelta);
if (e.end) {
e.end = addMinutes(addDays(e.end, dayDelta, true), minuteDelta);
}
normalizeEvent(e, options);
}
}
function elongateEvents(events, dayDelta, minuteDelta) {
minuteDelta = minuteDelta || 0;
for (var e, len=events.length, i=0; i<len; i++) {
e = events[i];
e.end = addMinutes(addDays(eventEnd(e), dayDelta, true), minuteDelta);
normalizeEvent(e, options);
}
}
}
function DayEventRenderer() {
var t = this;
// exports
t.renderDaySegs = renderDaySegs;
t.resizableDayEvent = resizableDayEvent;
// imports
var opt = t.opt;
var trigger = t.trigger;
var isEventDraggable = t.isEventDraggable;
var isEventResizable = t.isEventResizable;
var eventEnd = t.eventEnd;
var reportEventElement = t.reportEventElement;
var showEvents = t.showEvents;
var hideEvents = t.hideEvents;
var eventResize = t.eventResize;
var getRowCnt = t.getRowCnt;
var getColCnt = t.getColCnt;
var getColWidth = t.getColWidth;
var allDayRow = t.allDayRow;
var allDayBounds = t.allDayBounds;
var colContentLeft = t.colContentLeft;
var colContentRight = t.colContentRight;
var dayOfWeekCol = t.dayOfWeekCol;
var dateCell = t.dateCell;
var compileDaySegs = t.compileDaySegs;
var getDaySegmentContainer = t.getDaySegmentContainer;
var bindDaySeg = t.bindDaySeg; //TODO: streamline this
var formatDates = t.calendar.formatDates;
var renderDayOverlay = t.renderDayOverlay;
var clearOverlays = t.clearOverlays;
var clearSelection = t.clearSelection;
/* Rendering
-----------------------------------------------------------------------------*/
function renderDaySegs(segs, modifiedEventId) {
var segmentContainer = getDaySegmentContainer();
var rowDivs;
var rowCnt = getRowCnt();
var colCnt = getColCnt();
var i = 0;
var rowI;
var levelI;
var colHeights;
var j;
var segCnt = segs.length;
var seg;
var top;
var k;
var overflows;
var overflowLinks;
var maxHeight = opt('maxHeight');
segmentContainer[0].innerHTML = daySegHTML(segs); // faster than .html()
daySegElementResolve(segs, segmentContainer.children());
daySegElementReport(segs);
daySegHandlers(segs, segmentContainer, modifiedEventId);
daySegCalcHSides(segs);
daySegSetWidths(segs);
daySegCalcHeights(segs);
rowDivs = getRowDivs();
// set row heights, calculate event tops (in relation to row top)
for (rowI=0; rowI<rowCnt; rowI++) {
levelI = 0;
overflows = [];
overflowLinks = {};
colHeights = [];
for (j=0; j<colCnt; j++) {
overflows[j] = 0;
colHeights[j] = 0;
}
while (i<segCnt && (seg = segs[i]).row == rowI) {
// loop through segs in a row
top = arrayMax(colHeights.slice(seg.startCol, seg.endCol));
if (maxHeight && top + seg.outerHeight > maxHeight) {
seg.overflow = true;
}
else {
seg.top = top;
top += seg.outerHeight;
}
for (k=seg.startCol; k<seg.endCol; k++) {
if (overflows[k])
seg.overflow = true;
if (seg.overflow) {
if (seg.isStart && !overflowLinks[k])
overflowLinks[k] = { seg:seg, top:top, date:cloneDate(seg.start, true), count:0 };
if (overflowLinks[k])
overflowLinks[k].count++;
overflows[k]++;
}
else
colHeights[k] = top;
}
i++;
}
rowDivs[rowI].height(arrayMax(colHeights));
renderOverflowLinks(overflowLinks, rowDivs[rowI]);
}
daySegSetTops(segs, getRowTops(rowDivs));
}
function renderOverflowLinks(overflowLinks, rowDiv) {
var container = getDaySegmentContainer();
var colCnt = getColCnt();
var element, triggerRes, link;
for (var j=0; j<colCnt; j++) {
if ((link = overflowLinks[j])) {
if (link.count > 1) {
element = $('<a>').addClass('fc-more-link').html('+'+link.count).appendTo(container);
element[0].style.position = 'absolute';
element[0].style.left = link.seg.left + 'px';
element[0].style.top = (link.top + rowDiv[0].offsetTop) + 'px';
triggerRes = trigger('overflowRender', link, { count:link.count, date:link.date }, element);
if (triggerRes === false)
element.remove();
}
else {
link.seg.top = link.top;
link.seg.overflow = false;
}
}
}
}
function renderTempDaySegs(segs, adjustRow, adjustTop) {
var tempContainer = $("<div/>");
var elements;
var segmentContainer = getDaySegmentContainer();
var i;
var segCnt = segs.length;
var element;
tempContainer[0].innerHTML = daySegHTML(segs); // faster than .html()
elements = tempContainer.children();
segmentContainer.append(elements);
daySegElementResolve(segs, elements);
daySegCalcHSides(segs);
daySegSetWidths(segs);
daySegCalcHeights(segs);
daySegSetTops(segs, getRowTops(getRowDivs()));
elements = [];
for (i=0; i<segCnt; i++) {
element = segs[i].element;
if (element) {
if (segs[i].row === adjustRow) {
element.css('top', adjustTop);
}
elements.push(element[0]);
}
}
return $(elements);
}
function daySegHTML(segs) { // also sets seg.left and seg.outerWidth
var rtl = opt('isRTL');
var i;
var segCnt=segs.length;
var seg;
var event;
var url;
var classes;
var bounds = allDayBounds();
var minLeft = bounds.left;
var maxLeft = bounds.right;
var leftCol;
var rightCol;
var left;
var right;
var skinCss;
var html = '';
// calculate desired position/dimensions, create html
for (i=0; i<segCnt; i++) {
seg = segs[i];
event = seg.event;
classes = ['fc-event', 'fc-event-skin', 'fc-event-hori'];
if (isEventDraggable(event)) {
classes.push('fc-event-draggable');
}
if (rtl) {
if (seg.isStart) {
classes.push('fc-corner-right');
}
if (seg.isEnd) {
classes.push('fc-corner-left');
}
leftCol = dayOfWeekCol(seg.end.getDay()-1);
rightCol = dayOfWeekCol(seg.start.getDay());
left = seg.isEnd ? colContentLeft(leftCol) : minLeft;
right = seg.isStart ? colContentRight(rightCol) : maxLeft;
}else{
if (seg.isStart) {
classes.push('fc-corner-left');
}
if (seg.isEnd) {
classes.push('fc-corner-right');
}
leftCol = dayOfWeekCol(seg.start.getDay());
rightCol = dayOfWeekCol(seg.end.getDay()-1);
left = seg.isStart ? colContentLeft(leftCol) : minLeft;
right = seg.isEnd ? colContentRight(rightCol) : maxLeft;
}
classes = classes.concat(event.className);
if (event.source) {
classes = classes.concat(event.source.className || []);
}
url = event.url;
skinCss = getSkinCss(event, opt);
if (url) {
html += "<a href='" + htmlEscape(url) + "'";
}else{
html += "<div";
}
html +=
" class='" + classes.join(' ') + "'" +
" style='position:absolute;z-index:8;left:"+left+"px;" + skinCss + "'" +
">" +
"<div" +
" class='fc-event-inner fc-event-skin'" +
(skinCss ? " style='" + skinCss + "'" : '') +
">";
if (!event.allDay && seg.isStart) {
html +=
"<span class='fc-event-time'>" +
htmlEscape(formatDates(event.start, event.end, opt('timeFormat'))) +
"</span>";
}
html +=
"<span class='fc-event-title'>" + htmlEscape(event.title) + "</span>" +
"</div>";
if (seg.isEnd && isEventResizable(event)) {
html +=
"<div class='ui-resizable-handle ui-resizable-" + (rtl ? 'w' : 'e') + "'>" +
"&nbsp;&nbsp;&nbsp;" + // makes hit area a lot better for IE6/7
"</div>";
}
html +=
"</" + (url ? "a" : "div" ) + ">";
seg.left = left;
seg.outerWidth = right - left;
seg.startCol = leftCol;
seg.endCol = rightCol + 1; // needs to be exclusive
}
return html;
}
function daySegElementResolve(segs, elements) { // sets seg.element
var i;
var segCnt = segs.length;
var seg;
var event;
var element;
var triggerRes;
for (i=0; i<segCnt; i++) {
seg = segs[i];
event = seg.event;
element = $(elements[i]); // faster than .eq()
triggerRes = trigger('eventRender', event, event, element);
if (triggerRes === false) {
element.remove();
}else{
if (triggerRes && triggerRes !== true) {
triggerRes = $(triggerRes)
.css({
position: 'absolute',
left: seg.left
});
element.replaceWith(triggerRes);
element = triggerRes;
}
seg.element = element;
}
}
}
function daySegElementReport(segs) {
var i;
var segCnt = segs.length;
var seg;
var element;
for (i=0; i<segCnt; i++) {
seg = segs[i];
element = seg.element;
if (element) {
reportEventElement(seg.event, element);
}
}
}
function daySegHandlers(segs, segmentContainer, modifiedEventId) {
var i;
var segCnt = segs.length;
var seg;
var element;
var event;
// retrieve elements, run through eventRender callback, bind handlers
for (i=0; i<segCnt; i++) {
seg = segs[i];
element = seg.element;
if (element) {
event = seg.event;
if (event._id === modifiedEventId) {
bindDaySeg(event, element, seg);
}else{
element[0]._fci = i; // for lazySegBind
}
}
}
lazySegBind(segmentContainer, segs, bindDaySeg);
}
function daySegCalcHSides(segs) { // also sets seg.key
var i;
var segCnt = segs.length;
var seg;
var element;
var key, val;
var hsideCache = {};
// record event horizontal sides
for (i=0; i<segCnt; i++) {
seg = segs[i];
element = seg.element;
if (element) {
key = seg.key = cssKey(element[0]);
val = hsideCache[key];
if (val === undefined) {
val = hsideCache[key] = hsides(element, true);
}
seg.hsides = val;
}
}
}
function daySegSetWidths(segs) {
var i;
var segCnt = segs.length;
var seg;
var element;
for (i=0; i<segCnt; i++) {
seg = segs[i];
element = seg.element;
if (element) {
element[0].style.width = Math.max(0, seg.outerWidth - seg.hsides) + 'px';
}
}
}
function daySegCalcHeights(segs) {
var i;
var segCnt = segs.length;
var seg;
var element;
var key, val;
var vmarginCache = {};
// record event heights
for (i=0; i<segCnt; i++) {
seg = segs[i];
element = seg.element;
if (element) {
key = seg.key; // created in daySegCalcHSides
val = vmarginCache[key];
if (val === undefined) {
val = vmarginCache[key] = vmargins(element);
}
seg.outerHeight = element[0].offsetHeight + val;
}
- else
+ else // always set a value (issue #1108 )
seg.outerHeight = 0;
}
}
function getRowDivs() {
var i;
var rowCnt = getRowCnt();
var rowDivs = [];
for (i=0; i<rowCnt; i++) {
rowDivs[i] = allDayRow(i)
.find('td:first div.fc-day-content > div'); // optimal selector?
}
return rowDivs;
}
function getRowTops(rowDivs) {
var i;
var rowCnt = rowDivs.length;
var tops = [];
for (i=0; i<rowCnt; i++) {
tops[i] = rowDivs[i][0].offsetTop; // !!?? but this means the element needs position:relative if in a table cell!!!!
}
return tops;
}
function daySegSetTops(segs, rowTops) { // also triggers eventAfterRender
var i;
var segCnt = segs.length;
var seg;
var element;
var event;
for (i=0; i<segCnt; i++) {
seg = segs[i];
element = seg.element;
if (element && !seg.overflow) {
element[0].style.top = rowTops[seg.row] + (seg.top||0) + 'px';
event = seg.event;
trigger('eventAfterRender', event, event, element);
}
else if (element)
element.hide();
}
}
/* Resizing
-----------------------------------------------------------------------------------*/
function resizableDayEvent(event, element, seg) {
var rtl = opt('isRTL');
var direction = rtl ? 'w' : 'e';
var handle = element.find('div.ui-resizable-' + direction);
var isResizing = false;
// TODO: look into using jquery-ui mouse widget for this stuff
disableTextSelection(element); // prevent native <a> selection for IE
element
.mousedown(function(ev) { // prevent native <a> selection for others
ev.preventDefault();
})
.click(function(ev) {
if (isResizing) {
ev.preventDefault(); // prevent link from being visited (only method that worked in IE6)
ev.stopImmediatePropagation(); // prevent fullcalendar eventClick handler from being called
// (eventElementHandlers needs to be bound after resizableDayEvent)
}
});
handle.mousedown(function(ev) {
if (ev.which != 1) {
return; // needs to be left mouse button
}
isResizing = true;
var hoverListener = t.getHoverListener();
var rowCnt = getRowCnt();
var colCnt = getColCnt();
var dis = rtl ? -1 : 1;
var dit = rtl ? colCnt-1 : 0;
var elementTop = element.css('top');
var dayDelta;
var helpers;
var eventCopy = $.extend({}, event);
var minCell = dateCell(event.start);
clearSelection();
$('body')
.css('cursor', direction + '-resize')
.one('mouseup', mouseup);
trigger('eventResizeStart', this, event, ev);
hoverListener.start(function(cell, origCell) {
if (cell) {
var r = Math.max(minCell.row, cell.row);
var c = cell.col;
if (rowCnt == 1) {
r = 0; // hack for all-day area in agenda views
}
if (r == minCell.row) {
if (rtl) {
c = Math.min(minCell.col, c);
}else{
c = Math.max(minCell.col, c);
}
}
dayDelta = (r*7 + c*dis+dit) - (origCell.row*7 + origCell.col*dis+dit);
var newEnd = addDays(eventEnd(event), dayDelta, true);
if (dayDelta) {
eventCopy.end = newEnd;
var oldHelpers = helpers;
helpers = renderTempDaySegs(compileDaySegs([eventCopy]), seg.row, elementTop);
helpers.find('*').css('cursor', direction + '-resize');
if (oldHelpers) {
oldHelpers.remove();
}
hideEvents(event);
}else{
if (helpers) {
showEvents(event);
helpers.remove();
helpers = null;
}
}
clearOverlays();
renderDayOverlay(event.start, addDays(cloneDate(newEnd), 1)); // coordinate grid already rebuild at hoverListener.start
}
}, ev);
function mouseup(ev) {
trigger('eventResizeStop', this, event, ev);
$('body').css('cursor', '');
hoverListener.stop();
clearOverlays();
if (dayDelta) {
eventResize(this, event, dayDelta, 0, ev);
// event redraw will clear helpers
}
// otherwise, the drag handler already restored the old events
setTimeout(function() { // make this happen after the element's click event
isResizing = false;
},0);
}
});
}
}
//BUG: unselect needs to be triggered when events are dragged+dropped
function SelectionManager() {
var t = this;
// exports
t.select = select;
t.unselect = unselect;
t.reportSelection = reportSelection;
t.daySelectionMousedown = daySelectionMousedown;
// imports
var opt = t.opt;
var trigger = t.trigger;
var defaultSelectionEnd = t.defaultSelectionEnd;
var renderSelection = t.renderSelection;
var clearSelection = t.clearSelection;
// locals
var selected = false;
// unselectAuto
if (opt('selectable') && opt('unselectAuto')) {
$(document).mousedown(function(ev) {
var ignore = opt('unselectCancel');
if (ignore) {
if ($(ev.target).parents(ignore).length) { // could be optimized to stop after first match
return;
}
}
unselect(ev);
});
}
function select(startDate, endDate, allDay) {
unselect();
if (!endDate) {
endDate = defaultSelectionEnd(startDate, allDay);
}
renderSelection(startDate, endDate, allDay);
reportSelection(startDate, endDate, allDay);
}
function unselect(ev) {
if (selected) {
selected = false;
clearSelection();
trigger('unselect', null, ev);
}
}
function reportSelection(startDate, endDate, allDay, ev) {
selected = true;
trigger('select', null, startDate, endDate, allDay, ev);
}
function daySelectionMousedown(ev) { // not really a generic manager method, oh well
var cellDate = t.cellDate;
var cellIsAllDay = t.cellIsAllDay;
var hoverListener = t.getHoverListener();
var reportDayClick = t.reportDayClick; // this is hacky and sort of weird
if (ev.which == 1 && opt('selectable')) { // which==1 means left mouse button
unselect(ev);
var _mousedownElement = this;
var dates;
hoverListener.start(function(cell, origCell) { // TODO: maybe put cellDate/cellIsAllDay info in cell
clearSelection();
if (cell && cellIsAllDay(cell)) {
dates = [ cellDate(origCell), cellDate(cell) ].sort(cmp);
renderSelection(dates[0], dates[1], true);
}else{
dates = null;
}
}, ev);
$(document).one('mouseup', function(ev) {
hoverListener.stop();
if (dates) {
if (+dates[0] == +dates[1]) {
reportDayClick(dates[0], true, ev);
}
reportSelection(dates[0], dates[1], true, ev);
}
});
}
}
}
function OverlayManager() {
var t = this;
// exports
t.renderOverlay = renderOverlay;
t.clearOverlays = clearOverlays;
// locals
var usedOverlays = [];
var unusedOverlays = [];
function renderOverlay(rect, parent) {
var e = unusedOverlays.shift();
if (!e) {
e = $("<div class='fc-cell-overlay' style='position:absolute;z-index:3'/>");
}
if (e[0].parentNode != parent[0]) {
e.appendTo(parent);
}
usedOverlays.push(e.css(rect).show());
return e;
}
function clearOverlays() {
var e;
while (e = usedOverlays.shift()) {
unusedOverlays.push(e.hide().unbind());
}
}
}
function CoordinateGrid(buildFunc) {
var t = this;
var rows;
var cols;
t.build = function() {
rows = [];
cols = [];
buildFunc(rows, cols);
};
t.cell = function(x, y) {
var rowCnt = rows.length;
var colCnt = cols.length;
var i, r=-1, c=-1;
for (i=0; i<rowCnt; i++) {
if (y >= rows[i][0] && y < rows[i][1]) {
r = i;
break;
}
}
for (i=0; i<colCnt; i++) {
if (x >= cols[i][0] && x < cols[i][1]) {
c = i;
break;
}
}
return (r>=0 && c>=0) ? { row:r, col:c } : null;
};
t.rect = function(row0, col0, row1, col1, originElement) { // row1,col1 is inclusive
var origin = originElement.offset();
return {
top: rows[row0][0] - origin.top,
left: cols[col0][0] - origin.left,
width: cols[col1][1] - cols[col0][0],
height: rows[row1][1] - rows[row0][0]
};
};
}
function HoverListener(coordinateGrid) {
var t = this;
var bindType;
var change;
var firstCell;
var cell;
t.start = function(_change, ev, _bindType) {
change = _change;
firstCell = cell = null;
coordinateGrid.build();
mouse(ev);
bindType = _bindType || 'mousemove';
$(document).bind(bindType, mouse);
};
function mouse(ev) {
+ _fixUIEvent(ev); // see below
var newCell = coordinateGrid.cell(ev.pageX, ev.pageY);
if (!newCell != !cell || newCell && (newCell.row != cell.row || newCell.col != cell.col)) {
if (newCell) {
if (!firstCell) {
firstCell = newCell;
}
change(newCell, firstCell, newCell.row-firstCell.row, newCell.col-firstCell.col);
}else{
change(newCell, firstCell);
}
cell = newCell;
}
}
t.stop = function() {
$(document).unbind(bindType, mouse);
return cell;
};
}
+
+
+// this fix was only necessary for jQuery UI 1.8.16 (and jQuery 1.7 or 1.7.1)
+// upgrading to jQuery UI 1.8.17 (and using either jQuery 1.7 or 1.7.1) fixed the problem
+// but keep this in here for 1.8.16 users
+// and maybe remove it down the line
+
+function _fixUIEvent(event) { // for issue 1168
+ if (event.pageX === undefined) {
+ event.pageX = event.originalEvent.pageX;
+ event.pageY = event.originalEvent.pageY;
+ }
+}
function HorizontalPositionCache(getElement) {
var t = this,
elements = {},
lefts = {},
rights = {};
function e(i) {
return elements[i] = elements[i] || getElement(i);
}
t.left = function(i) {
return lefts[i] = lefts[i] === undefined ? e(i).position().left : lefts[i];
};
t.right = function(i) {
return rights[i] = rights[i] === undefined ? t.left(i) + e(i).width() : rights[i];
};
t.clear = function() {
elements = {};
lefts = {};
rights = {};
};
}
-
/* Additional view: list (by bruederli@kolabsys.com)
---------------------------------------------------------------------------------*/
function ListEventRenderer() {
var t = this;
// exports
t.renderEvents = renderEvents;
t.renderEventTime = renderEventTime;
t.compileDaySegs = compileSegs; // for DayEventRenderer
t.clearEvents = clearEvents;
t.lazySegBind = lazySegBind;
t.sortCmp = sortCmp;
// imports
DayEventRenderer.call(t);
var opt = t.opt;
var trigger = t.trigger;
var reportEvents = t.reportEvents;
var reportEventClear = t.reportEventClear;
var reportEventElement = t.reportEventElement;
var eventElementHandlers = t.eventElementHandlers;
var showEvents = t.showEvents;
var hideEvents = t.hideEvents;
var getListContainer = t.getDaySegmentContainer;
var calendar = t.calendar;
var formatDate = calendar.formatDate;
var formatDates = calendar.formatDates;
/* Rendering
--------------------------------------------------------------------*/
function clearEvents() {
reportEventClear();
getListContainer().empty();
}
function renderEvents(events, modifiedEventId) {
events.sort(sortCmp);
reportEvents(events);
renderSegs(compileSegs(events), modifiedEventId);
}
function compileSegs(events) {
var segs = [];
var colFormat = opt('titleFormat', 'day');
var firstDay = opt('firstDay');
var segmode = opt('listSections');
var event, i, dd, wd, md, seg, segHash, curSegHash, segDate, curSeg = -1;
var today = clearTime(new Date());
var weekstart = addDays(cloneDate(today), -((today.getDay() - firstDay + 7) % 7));
for (i=0; i < events.length; i++) {
event = events[i];
// skip events out of range
if (event.end < t.start || event.start > t.visEnd)
continue;
// define sections of this event
// create smart sections such as today, tomorrow, this week, next week, next month, ect.
segDate = cloneDate(event.start < t.start && event.end > t.start ? t.start : event.start, true);
dd = dayDiff(segDate, today);
wd = Math.floor(dayDiff(segDate, weekstart) / 7);
md = segDate.getMonth() + ((segDate.getYear() - today.getYear()) * 12) - today.getMonth();
// build section title
if (segmode == 'smart') {
if (dd < 0) {
segHash = opt('listTexts', 'past');
} else if (dd == 0) {
segHash = opt('listTexts', 'today');
} else if (dd == 1) {
segHash = opt('listTexts', 'tomorrow');
} else if (wd == 0) {
segHash = opt('listTexts', 'thisWeek');
} else if (wd == 1) {
segHash = opt('listTexts', 'nextWeek');
} else if (md == 0) {
segHash = opt('listTexts', 'thisMonth');
} else if (md == 1) {
segHash = opt('listTexts', 'nextMonth');
} else if (md > 1) {
segHash = opt('listTexts', 'future');
}
} else if (segmode == 'month') {
segHash = formatDate(segDate, 'MMMM yyyy');
} else if (segmode == 'week') {
segHash = opt('listTexts', 'week') + formatDate(segDate, ' W');
} else if (segmode == 'day') {
segHash = formatDate(segDate, colFormat);
} else {
segHash = '';
}
// start new segment
if (segHash != curSegHash) {
segs[++curSeg] = { events: [], start: segDate, title: segHash, daydiff: dd, weekdiff: wd, monthdiff: md };
curSegHash = segHash;
}
segs[curSeg].events.push(event);
}
return segs;
}
function sortCmp(a, b) {
var sd = a.start.getTime() - b.start.getTime();
return sd + (sd ? 0 : a.end.getTime() - b.end.getTime());
}
function renderSegs(segs, modifiedEventId) {
var tm = opt('theme') ? 'ui' : 'fc';
var headerClass = tm + "-widget-header";
var contentClass = tm + "-widget-content";
- var i, j, seg, event, times, s, skinCss, skinCssAttr, classes, segContainer, eventElements;
+ var i, j, seg, event, times, s, skinCss, skinCssAttr, classes, segContainer, eventElement, eventElements, triggerRes;
for (j=0; j < segs.length; j++) {
seg = segs[j];
if (seg.title) {
$('<div class="fc-list-header ' + headerClass + '">' + htmlEscape(seg.title) + '</div>').appendTo(getListContainer());
}
segContainer = $('<div>').addClass('fc-list-section ' + contentClass).appendTo(getListContainer());
s = '';
for (i=0; i < seg.events.length; i++) {
event = seg.events[i];
times = renderEventTime(event, seg);
skinCss = getSkinCss(event, opt);
skinCssAttr = (skinCss ? " style='" + skinCss + "'" : '');
classes = ['fc-event', 'fc-event-skin', 'fc-event-vert', 'fc-corner-top', 'fc-corner-bottom'].concat(event.className);
if (event.source && event.source.className) {
classes = classes.concat(event.source.className);
}
s +=
"<div class='" + classes.join(' ') + "'" + skinCssAttr + ">" +
"<div class='fc-event-inner fc-event-skin'" + skinCssAttr + ">" +
"<div class='fc-event-head fc-event-skin'" + skinCssAttr + ">" +
"<div class='fc-event-time'>" +
(times[0] ? '<span class="fc-col-date">' + times[0] + '</span> ' : '') +
(times[1] ? '<span class="fc-col-time">' + times[1] + '</span>' : '') +
"</div>" +
"</div>" +
"<div class='fc-event-content'>" +
"<div class='fc-event-title'>" +
htmlEscape(event.title) +
"</div>" +
"</div>" +
"<div class='fc-event-bg'></div>" +
"</div>" + // close inner
"</div>"; // close outer
}
segContainer[0].innerHTML = s;
eventElements = segContainer.children();
// retrieve elements, run through eventRender callback, bind event handlers
for (i=0; i < seg.events.length; i++) {
event = seg.events[i];
eventElement = $(eventElements[i]); // faster than eq()
triggerRes = trigger('eventRender', event, event, eventElement);
if (triggerRes === false) {
eventElement.remove();
} else {
if (triggerRes && triggerRes !== true) {
eventElement.remove();
eventElement = $(triggerRes).appendTo(segContainer);
}
if (event._id === modifiedEventId) {
eventElementHandlers(event, eventElement, seg);
} else {
eventElement[0]._fci = i; // for lazySegBind
}
reportEventElement(event, eventElement);
}
}
lazySegBind(segContainer, seg, eventElementHandlers);
}
markFirstLast(getListContainer());
}
// event time/date range to display
function renderEventTime(event, seg) {
var timeFormat = opt('timeFormat');
var dateFormat = opt('columnFormat');
var segmode = opt('listSections');
var duration = event.end.getTime() - event.start.getTime();
var datestr = '', timestr = '';
if (segmode == 'smart') {
if (event.start < seg.start) {
datestr = opt('listTexts', 'until') + ' ' + formatDate(event.end, (event.allDay || event.end.getDate() != seg.start.getDate()) ? dateFormat : timeFormat);
} else if (duration > DAY_MS) {
datestr = formatDates(event.start, event.end, dateFormat + '{ - ' + dateFormat + '}');
} else if (seg.daydiff == 0) {
datestr = opt('listTexts', 'today');
} else if (seg.daydiff == 1) {
datestr = opt('listTexts', 'tomorrow');
} else if (seg.weekdiff == 0 || seg.weekdiff == 1) {
datestr = formatDate(event.start, 'dddd');
} else if (seg.daydiff > 1 || seg.daydiff < 0) {
datestr = formatDate(event.start, dateFormat);
}
} else if (segmode != 'day') {
datestr = formatDates(event.start, event.end, dateFormat + (duration > DAY_MS ? '{ - ' + dateFormat + '}' : ''));
}
if (!datestr && event.allDay) {
timestr = opt('allDayText');
} else if ((duration < DAY_MS || !datestr) && !event.allDay) {
timestr = formatDates(event.start, event.end, timeFormat);
}
return [datestr, timestr];
}
function lazySegBind(container, seg, bindHandlers) {
container.unbind('mouseover').mouseover(function(ev) {
var parent = ev.target, e = parent, i, event;
while (parent != this) {
e = parent;
parent = parent.parentNode;
}
if ((i = e._fci) !== undefined) {
e._fci = undefined;
event = seg.events[i];
bindHandlers(event, container.children().eq(i), seg);
$(ev.target).trigger(ev);
}
ev.stopPropagation();
});
}
}
fcViews.list = ListView;
function ListView(element, calendar) {
var t = this;
// exports
t.render = render;
t.select = dummy;
t.unselect = dummy;
t.getDaySegmentContainer = function(){ return body; };
// imports
View.call(t, element, calendar, 'list');
ListEventRenderer.call(t);
var opt = t.opt;
var trigger = t.trigger;
var clearEvents = t.clearEvents;
var reportEventClear = t.reportEventClear;
var formatDates = calendar.formatDates;
var formatDate = calendar.formatDate;
// overrides
t.setWidth = setWidth;
t.setHeight = setHeight;
// locals
var body;
var firstDay;
var nwe;
var tm;
var colFormat;
function render(date, delta) {
if (delta) {
addDays(date, opt('listPage') * delta);
}
t.start = t.visStart = cloneDate(date, true);
t.end = addDays(cloneDate(t.start), opt('listPage'));
t.visEnd = addDays(cloneDate(t.start), opt('listRange'));
addMinutes(t.visEnd, -1); // set end to 23:59
t.title = formatDates(date, t.visEnd, opt('titleFormat'));
updateOptions();
if (!body) {
buildSkeleton();
} else {
clearEvents();
}
}
function updateOptions() {
firstDay = opt('firstDay');
nwe = opt('weekends') ? 0 : 1;
tm = opt('theme') ? 'ui' : 'fc';
colFormat = opt('columnFormat', 'day');
}
function buildSkeleton() {
body = $('<div>').addClass('fc-list-content').appendTo(element);
}
function setHeight(height, dateChanged) {
body.css('height', (height-1)+'px').css('overflow', 'auto');
}
function setWidth(width) {
// nothing to be done here
}
function dummy() {
// Stub.
}
}
-
/* Additional view: table (by bruederli@kolabsys.com)
---------------------------------------------------------------------------------*/
function TableEventRenderer() {
var t = this;
// imports
ListEventRenderer.call(t);
var opt = t.opt;
var sortCmp = t.sortCmp;
var trigger = t.trigger;
var compileSegs = t.compileDaySegs;
var reportEvents = t.reportEvents;
var reportEventClear = t.reportEventClear;
var reportEventElement = t.reportEventElement;
var eventElementHandlers = t.eventElementHandlers;
var renderEventTime = t.renderEventTime;
var showEvents = t.showEvents;
var hideEvents = t.hideEvents;
var getListContainer = t.getDaySegmentContainer;
var lazySegBind = t.lazySegBind;
var calendar = t.calendar;
var formatDate = calendar.formatDate;
var formatDates = calendar.formatDates;
// exports
t.renderEvents = renderEvents;
t.clearEvents = clearEvents;
/* Rendering
--------------------------------------------------------------------*/
function clearEvents() {
reportEventClear();
getListContainer().children('tbody').remove();
}
function renderEvents(events, modifiedEventId) {
events.sort(sortCmp);
reportEvents(events);
renderSegs(compileSegs(events), modifiedEventId);
getListContainer().removeClass('fc-list-smart fc-list-day fc-list-month fc-list-week').addClass('fc-list-' + opt('listSections'));
}
function renderSegs(segs, modifiedEventId) {
var tm = opt('theme') ? 'ui' : 'fc';
var table = getListContainer();
var headerClass = tm + "-widget-header";
var contentClass = tm + "-widget-content";
var tableCols = opt('tableCols');
var timecol = $.inArray('time', tableCols) >= 0;
- var i, j, seg, event, times, s, skinCss, skinCssAttr, skinClasses, rowClasses, segContainer, eventElements;
+ var i, j, seg, event, times, s, skinCss, skinCssAttr, skinClasses, rowClasses, segContainer, eventElements, eventElement, triggerRes;
for (j=0; j < segs.length; j++) {
seg = segs[j];
if (seg.title) {
$('<tbody class="fc-list-header"><tr><td class="fc-list-header ' + headerClass + '" colspan="' + tableCols.length + '">' + htmlEscape(seg.title) + '</td></tr></tbody>').appendTo(table);
}
segContainer = $('<tbody>').addClass('fc-list-section ' + contentClass).appendTo(table);
s = '';
for (i=0; i < seg.events.length; i++) {
event = seg.events[i];
times = renderEventTime(event, seg);
skinCss = getSkinCss(event, opt);
skinCssAttr = (skinCss ? " style='" + skinCss + "'" : '');
skinClasses = ['fc-event-skin', 'fc-corner-left', 'fc-corner-right', 'fc-corner-top', 'fc-corner-bottom'].concat(event.className);
if (event.source && event.source.className) {
skinClasses = skinClasses.concat(event.source.className);
}
rowClasses = ['fc-event', 'fc-event-row', 'fc-'+dayIDs[event.start.getDay()]];
if (seg.daydiff == 0) {
rowClasses.push('fc-today');
}
s += "<tr class='" + rowClasses.join(' ') + "'>";
for (var col, c=0; c < tableCols.length; c++) {
col = tableCols[c];
if (col == 'handle') {
s += "<td class='fc-event-handle'>" +
"<div class='" + skinClasses.join(' ') + "'" + skinCssAttr + ">" +
"<span class='fc-event-inner'></span>" +
"</div></td>";
} else if (col == 'date') {
s += "<td class='fc-event-date' colspan='" + (times[1] || !timecol ? 1 : 2) + "'>" + htmlEscape(times[0]) + "</td>";
} else if (col == 'time') {
if (times[1]) {
s += "<td class='fc-event-time'>" + htmlEscape(times[1]) + "</td>";
}
} else {
s += "<td class='fc-event-" + col + "'>" + (htmlEscape(event[col]) || '&nbsp;') + "</td>";
}
}
s += "</tr>";
// IE doesn't like innerHTML on tbody elements so we insert every row individually
if (document.all) {
$(s).appendTo(segContainer);
s = '';
}
}
if (!document.all)
segContainer[0].innerHTML = s;
eventElements = segContainer.children();
// retrieve elements, run through eventRender callback, bind event handlers
for (i=0; i < seg.events.length; i++) {
event = seg.events[i];
eventElement = $(eventElements[i]); // faster than eq()
triggerRes = trigger('eventRender', event, event, eventElement);
if (triggerRes === false) {
eventElement.remove();
} else {
if (triggerRes && triggerRes !== true) {
eventElement.remove();
eventElement = $(triggerRes).appendTo(segContainer);
}
if (event._id === modifiedEventId) {
eventElementHandlers(event, eventElement, seg);
} else {
eventElement[0]._fci = i; // for lazySegBind
}
reportEventElement(event, eventElement);
}
}
lazySegBind(segContainer, seg, eventElementHandlers);
markFirstLast(segContainer);
}
//markFirstLast(table);
}
}
fcViews.table = TableView;
function TableView(element, calendar) {
var t = this;
// exports
t.render = render;
t.select = dummy;
t.unselect = dummy;
t.getDaySegmentContainer = function(){ return table; };
// imports
View.call(t, element, calendar, 'table');
TableEventRenderer.call(t);
var opt = t.opt;
var trigger = t.trigger;
var clearEvents = t.clearEvents;
var reportEventClear = t.reportEventClear;
var formatDates = calendar.formatDates;
var formatDate = calendar.formatDate;
// overrides
t.setWidth = setWidth;
t.setHeight = setHeight;
// locals
var div;
var table;
var firstDay;
var nwe;
var tm;
var colFormat;
function render(date, delta) {
if (delta) {
addDays(date, opt('listPage') * delta);
}
t.start = t.visStart = cloneDate(date, true);
t.end = addDays(cloneDate(t.start), opt('listPage'));
t.visEnd = addDays(cloneDate(t.start), opt('listRange'));
addMinutes(t.visEnd, -1); // set end to 23:59
t.title = (t.visEnd.getTime() - t.visStart.getTime() < DAY_MS) ? formatDate(date, opt('titleFormat')) : formatDates(date, t.visEnd, opt('titleFormat'));
updateOptions();
if (!table) {
buildSkeleton();
} else {
clearEvents();
}
}
function updateOptions() {
firstDay = opt('firstDay');
nwe = opt('weekends') ? 0 : 1;
tm = opt('theme') ? 'ui' : 'fc';
colFormat = opt('columnFormat');
}
function buildSkeleton() {
var tableCols = opt('tableCols');
var s =
"<table class='fc-border-separate' style='width:100%' cellspacing='0'>" +
"<colgroup>";
for (var c=0; c < tableCols.length; c++) {
s += "<col class='fc-event-" + tableCols[c] + "' />";
}
s += "</colgroup>" +
"</table>";
div = $('<div>').addClass('fc-list-content').appendTo(element);
table = $(s).appendTo(div);
}
function setHeight(height, dateChanged) {
div.css('height', (height-1)+'px').css('overflow', 'auto');
}
function setWidth(width) {
// nothing to be done here
}
function dummy() {
// Stub.
}
}
-
-
-})(jQuery);
\ No newline at end of file
+
+})(jQuery);
diff --git a/plugins/calendar/localization/de_CH.inc b/plugins/calendar/localization/de_CH.inc
index 34f89099..ee8383df 100644
--- a/plugins/calendar/localization/de_CH.inc
+++ b/plugins/calendar/localization/de_CH.inc
@@ -1,245 +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'] = 'Kalender drucken';
+$labels['print'] = 'Drucken';
+$labels['printtitle'] = 'Kalender drucken';
$labels['title'] = 'Titel';
$labels['description'] = 'Beschrieb';
$labels['all-day'] = 'ganztägig';
-$labels['export'] = 'Exportieren...';
+$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 9a16e87d..3c3a4bdf 100644
--- a/plugins/calendar/localization/de_DE.inc
+++ b/plugins/calendar/localization/de_DE.inc
@@ -1,245 +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'] = 'Kalender drucken';
+$labels['print'] = 'Drucken';
+$labels['printtitle'] = 'Kalender drucken';
$labels['title'] = 'Titel';
$labels['description'] = 'Beschrieb';
$labels['all-day'] = 'ganztägig';
-$labels['export'] = 'Exportieren...';
+$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 8050bb8b..e13cd88b 100644
--- a/plugins/calendar/localization/en_US.inc
+++ b/plugins/calendar/localization/en_US.inc
@@ -1,246 +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 calendars';
+$labels['print'] = 'Print';
+$labels['printtitle'] = 'Print calendars';
$labels['title'] = 'Summary';
$labels['description'] = 'Description';
$labels['all-day'] = 'all-day';
-$labels['export'] = 'Export to iCalendar';
+$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/package.xml b/plugins/calendar/package.xml
index 3fc38c20..ed42622a 100644
--- a/plugins/calendar/package.xml
+++ b/plugins/calendar/package.xml
@@ -1,189 +1,189 @@
<?xml version="1.0" encoding="UTF-8"?>
<package xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" packagerversion="1.9.0" version="2.0" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
http://pear.php.net/dtd/tasks-1.0.xsd
http://pear.php.net/dtd/package-2.0
http://pear.php.net/dtd/package-2.0.xsd">
<name>calendar</name>
- <uri>http://kolabsys.com</uri>
+ <uri>http://git.kolab.org/roundcube-plugins-kolab/</uri>
<summary>Calendar plugin</summary>
<description>-</description>
<lead>
<name>Thomas Bruederli</name>
<user>bruederli</user>
<email>bruederli@kolabsys.com</email>
<active>yes</active>
</lead>
<developer>
<name>Alensader Machniak</name>
<user>machniak</user>
<email>machniak@kolabsys.com</email>
<active>yes</active>
</developer>
<date>2011-11-01</date>
<version>
<release>0.8</release>
<api>0.8</api>
</version>
<stability>
<release>stable</release>
<api>stable</api>
</stability>
<license uri="http://www.gnu.org/licenses/agpl.html">GNU AGPLv3</license>
<notes>-</notes>
<contents>
<dir baseinstalldir="/" name="/">
<file name="calendar.php" role="php">
<tasks:replace from="@name@" to="name" type="package-info"/>
<tasks:replace from="@package_version@" to="version" type="package-info"/>
</file>
<file name="calendar_base.js" role="data">
<tasks:replace from="@name@" to="name" type="package-info"/>
<tasks:replace from="@package_version@" to="version" type="package-info"/>
</file>
<file name="calendar_ui.js" role="data">
<tasks:replace from="@name@" to="name" type="package-info"/>
<tasks:replace from="@package_version@" to="version" type="package-info"/>
</file>
<file name="print.js" role="data">
<tasks:replace from="@name@" to="name" type="package-info"/>
<tasks:replace from="@package_version@" to="version" type="package-info"/>
</file>
<file name="lib/calendar_ical.php" role="php">
<tasks:replace from="@name@" to="name" type="package-info"/>
<tasks:replace from="@package_version@" to="version" type="package-info"/>
</file>
<file name="lib/calendar_itip.php" role="php">
<tasks:replace from="@name@" to="name" type="package-info"/>
<tasks:replace from="@package_version@" to="version" type="package-info"/>
</file>
<file name="lib/calendar_recurrence.php" role="php">
<tasks:replace from="@name@" to="name" type="package-info"/>
<tasks:replace from="@package_version@" to="version" type="package-info"/>
</file>
<file name="lib/calendar_ui.php" role="php">
<tasks:replace from="@name@" to="name" type="package-info"/>
<tasks:replace from="@package_version@" to="version" type="package-info"/>
</file>
<file name="lib/Horde_Date_Recurrence.php" role="php">
<tasks:replace from="@name@" to="name" type="package-info"/>
<tasks:replace from="@package_version@" to="version" type="package-info"/>
</file>
<file name="lib/fullcalendar-rc.patch" role="data">
<tasks:replace from="@name@" to="name" type="package-info"/>
<tasks:replace from="@package_version@" to="version" type="package-info"/>
</file>
<file name="lib/js/fullcalendar.js" role="data">
<tasks:replace from="@name@" to="name" type="package-info"/>
<tasks:replace from="@package_version@" to="version" type="package-info"/>
</file>
<file name="lib/jquery.miniColors.min.js" role="data">
<tasks:replace from="@name@" to="name" type="package-info"/>
<tasks:replace from="@package_version@" to="version" type="package-info"/>
</file>
<file name="drivers/calendar_driver.php" role="php">
<tasks:replace from="@name@" to="name" type="package-info"/>
<tasks:replace from="@package_version@" to="version" type="package-info"/>
</file>
<file name="drivers/kolab/kolab_calendar.php" role="php">
<tasks:replace from="@name@" to="name" type="package-info"/>
<tasks:replace from="@package_version@" to="version" type="package-info"/>
</file>
<file name="drivers/kolab/kolab_driver.php" role="php">
<tasks:replace from="@name@" to="name" type="package-info"/>
<tasks:replace from="@package_version@" to="version" type="package-info"/>
</file>
<file name="drivers/database/database_driver.php" role="php">
<tasks:replace from="@name@" to="name" type="package-info"/>
<tasks:replace from="@package_version@" to="version" type="package-info"/>
</file>
<file name="drivers/database/SQL/mysql.sql" role="data">
<tasks:replace from="@name@" to="name" type="package-info"/>
<tasks:replace from="@package_version@" to="version" type="package-info"/>
</file>
<file name="drivers/database/SQL/postgresql.sql" role="data">
<tasks:replace from="@name@" to="name" type="package-info"/>
<tasks:replace from="@package_version@" to="version" type="package-info"/>
</file>
<file name="drivers/database/SQL/sqlite.sql" role="data">
<tasks:replace from="@name@" to="name" type="package-info"/>
<tasks:replace from="@package_version@" to="version" type="package-info"/>
</file>
<file name="drivers/kolab/SQL/mysql.sql" role="data">
<tasks:replace from="@name@" to="name" type="package-info"/>
<tasks:replace from="@package_version@" to="version" type="package-info"/>
</file>
<file name="skins/default/calendar.css" role="data"></file>
<file name="skins/default/fullcalendar.css" role="data"></file>
<file name="skins/default/fullcalendar.print.css" role="data"></file>
<file name="skins/default/iehacks.css" role="data"></file>
<file name="skins/default/jquery.miniColors.css" role="data"></file>
<file name="skins/default/print.css" role="data"></file>
<file name="skins/default/print.iehacks.css" role="data"></file>
<file name="skins/default/images/attendee-status.gif" role="data"></file>
<file name="skins/default/images/badge_confidential.gif" role="data"></file>
<file name="skins/default/images/badge_confidential.png" role="data"></file>
<file name="skins/default/images/badge_private.gif" role="data"></file>
<file name="skins/default/images/badge_private.png" role="data"></file>
<file name="skins/default/images/calendar-blue.png" role="data"></file>
<file name="skins/default/images/calendar.gif" role="data"></file>
<file name="skins/default/images/calendar.png" role="data"></file>
<file name="skins/default/images/calendars.gif" role="data"></file>
<file name="skins/default/images/calendars.png" role="data"></file>
<file name="skins/default/images/eventicons.gif" role="data"></file>
<file name="skins/default/images/export.png" role="data"></file>
<file name="skins/default/images/freebusy-colors.gif" role="data"></file>
<file name="skins/default/images/freebusy-colors.png" role="data"></file>
<file name="skins/default/images/invitation.png" role="data"></file>
<file name="skins/default/images/listheader.gif" role="data"></file>
<file name="skins/default/images/loading_blue.gif" role="data"></file>
<file name="skins/default/images/minicolors-all.png" role="data"></file>
<file name="skins/default/images/minicolors-handles.gif" role="data"></file>
<file name="skins/default/images/preview.png" role="data"></file>
<file name="skins/default/images/print.png" role="data"></file>
<file name="skins/default/images/spacer.gif" role="data"></file>
<file name="skins/default/images/toggle.gif" role="data"></file>
<file name="skins/default/images/toolbar.gif" role="data"></file>
<file name="skins/default/images/toolbar.png" role="data"></file>
<file name="skins/default/templates/attachment.html" role="data"></file>
<file name="skins/default/templates/calendar.html" role="data"></file>
<file name="skins/default/templates/eventedit.html" role="data"></file>
<file name="skins/default/templates/freebusylegend.html" role="data"></file>
<file name="skins/default/templates/itipattend.html" role="data"></file>
<file name="skins/default/templates/kolabacl.html" role="data"></file>
<file name="skins/default/templates/kolabform.html" role="data"></file>
<file name="skins/default/templates/print.html" role="data"></file>
<file name="config.inc.php.dist" role="data"></file>
<file name="LICENSE" role="data"></file>
<file name="TODO" role="data"></file>
<file name="localization/bg_BG.inc" role="data"></file>
<file name="localization/cs_CZ.inc" role="data"></file>
<file name="localization/de_CH.inc" role="data"></file>
<file name="localization/de_DE.inc" role="data"></file>
<file name="localization/en_US.inc" role="data"></file>
<file name="localization/es_ES.inc" role="data"></file>
<file name="localization/fr_FR.inc" role="data"></file>
<file name="localization/hu_HU.inc" role="data"></file>
<file name="localization/it_IT.inc" role="data"></file>
<file name="localization/nl_NL.inc" role="data"></file>
<file name="localization/pl_PL.inc" role="data"></file>
<file name="localization/pt_BR.inc" role="data"></file>
<file name="localization/ru_RU.inc" role="data"></file>
</dir>
<!-- / -->
</contents>
<dependencies>
<required>
<php>
<min>5.2.1</min>
</php>
<pearinstaller>
<min>1.7.0</min>
</pearinstaller>
</required>
</dependencies>
<phprelease/>
</package>
diff --git a/plugins/calendar/skins/default/calendar.css b/plugins/calendar/skins/default/calendar.css
index aa1d5507..21dcb1b9 100644
--- a/plugins/calendar/skins/default/calendar.css
+++ b/plugins/calendar/skins/default/calendar.css
@@ -1,1257 +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 {
+#calendarslist li span.handle {
cursor: default;
background: url(images/calendars.png) 0 -2px no-repeat;
- padding-left: 18px;
+ 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 {
+#calendarslist li.readonly span.handle {
background-position: 0 -20px;
}
-#calendarslist li.other span {
+#calendarslist li.other span.handle {
background-position: 0 -38px;
}
-#calendarslist li.other.readonly span {
+#calendarslist li.other.readonly span.handle {
background-position: 0 -56px;
}
-#calendarslist li.shared span {
+#calendarslist li.shared span.handle {
background-position: 0 -74px;
}
-#calendarslist li.shared.readonly span {
+#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: url(images/calendars.png) 7px -109px no-repeat;
+ 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/fullcalendar.css b/plugins/calendar/skins/default/fullcalendar.css
index 404e2826..17f1b37b 100644
--- a/plugins/calendar/skins/default/fullcalendar.css
+++ b/plugins/calendar/skins/default/fullcalendar.css
@@ -1,724 +1,724 @@
/*
- * FullCalendar v1.5.2 Stylesheet
+ * FullCalendar v1.5.3-rcube-0.7.1 Stylesheet
*
* Copyright (c) 2011 Adam Shaw
+ * Copyright (c) 2011, Kolab Systems AG
* Dual licensed under the MIT and GPL licenses, located in
* MIT-LICENSE.txt and GPL-LICENSE.txt respectively.
*
- * Date: Sun Aug 21 22:06:09 2011 -0700
+ * Date: Mon Feb 13 23:00:46 2012 +0100
*
*/
.fc {
direction: ltr;
text-align: left;
}
.fc table {
border-collapse: collapse;
border-spacing: 0;
}
html .fc,
.fc table {
font-size: 1em;
}
.fc td,
.fc th {
padding: 0;
vertical-align: top;
}
/* Header
------------------------------------------------------------------------*/
.fc-header td {
white-space: nowrap;
}
.fc-header-left {
width: 25%;
text-align: left;
}
.fc-header-center {
text-align: center;
}
.fc-header-right {
width: 25%;
text-align: right;
}
.fc-header-title {
display: inline-block;
vertical-align: top;
}
.fc-header-title h2 {
margin-top: 0;
white-space: nowrap;
}
.fc .fc-header-space {
padding-left: 10px;
}
.fc-header .fc-button {
margin-bottom: 1em;
vertical-align: top;
}
/* buttons edges butting together */
.fc-header .fc-button {
margin-right: -1px;
}
.fc-header .fc-corner-right {
margin-right: 1px; /* back to normal */
}
.fc-header .ui-corner-right {
margin-right: 0; /* back to normal */
}
/* button layering (for border precedence) */
.fc-header .fc-state-hover,
.fc-header .ui-state-hover {
z-index: 2;
}
.fc-header .fc-state-down {
z-index: 3;
}
.fc-header .fc-state-active,
.fc-header .ui-state-active {
z-index: 4;
}
/* Content
------------------------------------------------------------------------*/
.fc-content {
clear: both;
}
.fc-view {
width: 100%; /* needed for view switching (when view is absolute) */
overflow: hidden;
}
/* Cell Styles
------------------------------------------------------------------------*/
.fc-widget-header, /* <th>, usually */
.fc-widget-content { /* <td>, usually */
border: 1px solid #ccc;
}
.fc-state-highlight { /* <td> today cell */ /* TODO: add .fc-today to <th> */
background: #ffc;
}
.fc-cell-overlay { /* semi-transparent rectangle while dragging */
background: #9cf;
opacity: .2;
filter: alpha(opacity=20); /* for IE */
}
/* Buttons
------------------------------------------------------------------------*/
.fc-button {
position: relative;
display: inline-block;
cursor: pointer;
}
.fc-state-default { /* non-theme */
border-style: solid;
border-width: 1px 0;
}
.fc-button-inner {
position: relative;
float: left;
overflow: hidden;
}
.fc-state-default .fc-button-inner { /* non-theme */
border-style: solid;
border-width: 0 1px;
}
.fc-button-content {
position: relative;
float: left;
height: 1.9em;
line-height: 1.9em;
padding: 0 .6em;
white-space: nowrap;
}
/* icon (for jquery ui) */
.fc-button-content .fc-icon-wrap {
position: relative;
float: left;
top: 50%;
}
.fc-button-content .ui-icon {
position: relative;
float: left;
margin-top: -50%;
*margin-top: 0;
*top: -50%;
}
/* gloss effect */
.fc-state-default .fc-button-effect {
position: absolute;
top: 50%;
left: 0;
}
.fc-state-default .fc-button-effect span {
position: absolute;
top: -100px;
left: 0;
width: 500px;
height: 100px;
border-width: 100px 0 0 1px;
border-style: solid;
border-color: #fff;
background: #444;
opacity: .09;
filter: alpha(opacity=9);
}
/* button states (determines colors) */
.fc-state-default,
.fc-state-default .fc-button-inner {
border-style: solid;
border-color: #ccc #bbb #aaa;
background: #F3F3F3;
color: #000;
}
.fc-state-hover,
.fc-state-hover .fc-button-inner {
border-color: #999;
}
.fc-state-down,
.fc-state-down .fc-button-inner {
border-color: #555;
background: #777;
}
.fc-state-active,
.fc-state-active .fc-button-inner {
border-color: #555;
background: #777;
color: #fff;
}
.fc-state-disabled,
.fc-state-disabled .fc-button-inner {
color: #999;
border-color: #ddd;
}
.fc-state-disabled {
cursor: default;
}
.fc-state-disabled .fc-button-effect {
display: none;
}
/* Global Event Styles
------------------------------------------------------------------------*/
.fc-event {
border-style: solid;
border-width: 0;
font-size: .85em;
cursor: default;
}
a.fc-event,
.fc-event-draggable {
cursor: pointer;
}
a.fc-event {
text-decoration: none;
}
.fc-rtl .fc-event {
text-align: right;
}
.fc-event-skin {
border-color: #36c; /* default BORDER color */
background-color: #36c; /* default BACKGROUND color */
color: #fff; /* default TEXT color */
}
.fc-event-inner {
position: relative;
width: 100%;
height: 100%;
border-style: solid;
border-width: 0;
overflow: hidden;
}
.fc-event-time,
.fc-event-title {
padding: 0 1px;
}
.fc .ui-resizable-handle { /*** TODO: don't use ui-resizable anymore, change class ***/
display: block;
position: absolute;
z-index: 99999;
overflow: hidden; /* hacky spaces (IE6/7) */
font-size: 300%; /* */
line-height: 50%; /* */
}
/* Horizontal Events
------------------------------------------------------------------------*/
.fc-event-hori {
border-width: 1px 0;
margin-bottom: 1px;
}
/* resizable */
.fc-event-hori .ui-resizable-e {
top: 0 !important; /* importants override pre jquery ui 1.7 styles */
right: -3px !important;
width: 7px !important;
height: 100% !important;
cursor: e-resize;
}
.fc-event-hori .ui-resizable-w {
top: 0 !important;
left: -3px !important;
width: 7px !important;
height: 100% !important;
cursor: w-resize;
}
.fc-event-hori .ui-resizable-handle {
_padding-bottom: 14px; /* IE6 had 0 height */
}
/* Fake Rounded Corners (for buttons and events)
------------------------------------------------------------*/
.fc-corner-left {
margin-left: 1px;
}
.fc-corner-left .fc-button-inner,
.fc-corner-left .fc-event-inner {
margin-left: -1px;
}
.fc-corner-right {
margin-right: 1px;
}
.fc-corner-right .fc-button-inner,
.fc-corner-right .fc-event-inner {
margin-right: -1px;
}
.fc-corner-top {
margin-top: 1px;
}
.fc-corner-top .fc-event-inner {
margin-top: -1px;
}
.fc-corner-bottom {
margin-bottom: 1px;
}
.fc-corner-bottom .fc-event-inner {
margin-bottom: -1px;
}
/* Fake Rounded Corners SPECIFICALLY FOR EVENTS
-----------------------------------------------------------------*/
.fc-corner-left .fc-event-inner {
border-left-width: 1px;
}
.fc-corner-right .fc-event-inner {
border-right-width: 1px;
}
.fc-corner-top .fc-event-inner {
border-top-width: 1px;
}
.fc-corner-bottom .fc-event-inner {
border-bottom-width: 1px;
}
/* Reusable Separate-border Table
------------------------------------------------------------*/
table.fc-border-separate {
border-collapse: separate;
}
.fc-border-separate th,
.fc-border-separate td {
border-width: 1px 0 0 1px;
}
.fc-border-separate th.fc-last,
.fc-border-separate td.fc-last {
border-right-width: 1px;
}
.fc-border-separate tr.fc-last th,
.fc-border-separate tr.fc-last td {
border-bottom-width: 1px;
}
.fc-border-separate tbody tr.fc-first td,
.fc-border-separate tbody tr.fc-first th {
border-top-width: 0;
}
/* Month View, Basic Week View, Basic Day View
------------------------------------------------------------------------*/
.fc-grid th {
text-align: center;
}
.fc-grid .fc-day-number {
float: right;
padding: 0 2px;
}
.fc-grid .fc-other-month .fc-day-number {
opacity: 0.3;
filter: alpha(opacity=30); /* for IE */
/* opacity with small font can sometimes look too faded
might want to set the 'color' property instead
making day-numbers bold also fixes the problem */
}
.fc-grid .fc-day-content {
clear: both;
padding: 2px 2px 1px; /* distance between events and day edges */
}
/* event styles */
.fc-grid .fc-event-time {
font-weight: bold;
}
/* right-to-left */
.fc-rtl .fc-grid .fc-day-number {
float: left;
}
.fc-rtl .fc-grid .fc-event-time {
float: right;
}
.fc-more-link {
font-size: 0.85em;
white-space: nowrap;
text-decoration: none;
cursor: pointer;
padding: 1px;
-}
+}
/* Agenda Week View, Agenda Day View
------------------------------------------------------------------------*/
.fc-agenda table {
border-collapse: separate;
}
.fc-agenda-days th {
text-align: center;
}
.fc-agenda .fc-agenda-axis {
width: 50px;
padding: 0 4px;
vertical-align: middle;
text-align: right;
white-space: nowrap;
font-weight: normal;
}
.fc-agenda .fc-day-content {
padding: 2px 2px 1px;
}
/* make axis border take precedence */
.fc-agenda-days .fc-agenda-axis {
border-right-width: 1px;
}
.fc-agenda-days .fc-col0 {
border-left-width: 0;
}
/* all-day area */
.fc-agenda-allday th {
border-width: 0 1px;
}
.fc-agenda-allday .fc-day-content {
min-height: 34px; /* TODO: doesnt work well in quirksmode */
_height: 34px;
}
/* divider (between all-day and slots) */
.fc-agenda-divider-inner {
height: 2px;
overflow: hidden;
}
.fc-widget-header .fc-agenda-divider-inner {
background: #eee;
}
/* slot rows */
.fc-agenda-slots th {
border-width: 1px 1px 0;
}
.fc-agenda-slots td {
border-width: 1px 0 0;
background: none;
}
.fc-agenda-slots td div {
height: 20px;
}
.fc-agenda-slots tr.fc-slot0 th,
.fc-agenda-slots tr.fc-slot0 td {
border-top-width: 0;
}
.fc-agenda-slots tr.fc-minor th,
.fc-agenda-slots tr.fc-minor td {
border-top-style: dotted;
}
.fc-agenda-slots tr.fc-minor th.ui-widget-header {
*border-top-style: solid; /* doesn't work with background in IE6/7 */
}
/* Vertical Events
------------------------------------------------------------------------*/
.fc-event-vert {
border-width: 0 1px;
}
.fc-event-vert .fc-event-head,
.fc-event-vert .fc-event-content {
position: relative;
z-index: 2;
width: 100%;
overflow: hidden;
}
.fc-event-vert .fc-event-time {
white-space: nowrap;
font-size: 10px;
}
.fc-event-vert .fc-event-bg { /* makes the event lighter w/ a semi-transparent overlay */
position: absolute;
z-index: 1;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #fff;
opacity: .3;
filter: alpha(opacity=30);
}
.fc .ui-draggable-dragging .fc-event-bg, /* TODO: something nicer like .fc-opacity */
.fc-select-helper .fc-event-bg {
display: none\9; /* for IE6/7/8. nested opacity filters while dragging don't work */
}
/* resizable */
.fc-event-vert .ui-resizable-s {
bottom: 0 !important; /* importants override pre jquery ui 1.7 styles */
width: 100% !important;
height: 8px !important;
overflow: hidden !important;
line-height: 8px !important;
font-size: 11px !important;
font-family: monospace;
text-align: center;
cursor: s-resize;
}
.fc-agenda .ui-resizable-resizing { /* TODO: better selector */
_overflow: hidden;
}
-
-
+
.fc-timeline {
position: absolute;
width: 100%;
left: 0;
margin: 0;
padding: 0;
border: none;
border-top: 2px solid #3ec400;
z-index: 999;
}
/* List view (by bruederli@kolabsys.com)
------------------------------------------------------------------------*/
.fc-view-list,
.fc-view-table {
border: 1px solid #ccc;
width: auto;
}
.fc-view-list .fc-list-header,
.fc-view-table td.fc-list-header {
border-width: 0;
border-bottom-width: 1px;
padding: 3px 5px;
}
.fc-view-table .fc-first td.fc-list-header {
border-top-width: 0;
}
.fc-list-section {
padding: 4px 2px;
border-width: 0;
border-bottom-width: 1px;
}
.fc-view-list .fc-last {
border-bottom-width: 0;
}
.fc-list-section .fc-event {
position: relative;
margin: 1px 2px 3px 2px;
}
.fc-view-table tr.fc-event td {
padding: 2px;
border-bottom: 1px solid #ccc;
}
.fc-view-table tr.fc-event td.fc-event-handle {
padding: 3px 8px 3px 3px;
}
.fc-view-table .fc-event-handle .fc-event-skin {
border-radius: 2px;
-moz-border-radius: 2px;
}
.fc-view-table .fc-event-handle .fc-event-inner {
display: block;
width: 8px;
height: 10px;
border-radius: 2px;
-moz-border-radius: 2px;
}
.fc-view-table table {
table-layout: fixed;
width: 100%;
}
.fc-view-table col.fc-event-handle {
width: 18px;
}
.fc-event-handle .fc-event-inner {
border-color: inherit;
background-color: inherit;
}
.fc-view-table col.fc-event-date {
width: 7em;
}
.fc-view-table .fc-list-day col.fc-event-date {
width: 1px;
}
.fc-view-table col.fc-event-time {
width: 8em;
}
.fc-view-table td.fc-event-date,
.fc-view-table td.fc-event-time {
white-space: nowrap;
padding-right: 1em;
}
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 6253ab7c..a22056dd 100644
--- a/plugins/calendar/skins/default/templates/calendar.html
+++ b/plugins/calendar/skins/default/templates/calendar.html
@@ -1,195 +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/.htaccess b/plugins/calendar/skins/larry/.htaccess
new file mode 100644
index 00000000..d9731059
--- /dev/null
+++ b/plugins/calendar/skins/larry/.htaccess
@@ -0,0 +1,5 @@
+# get all unknown files from default skin folder
+RewriteEngine On
+RewriteCond %{REQUEST_FILENAME} !-f
+RewriteCond %{REQUEST_FILENAME} !-d
+RewriteRule (.*) ../default/$1 [qsappend,last]
diff --git a/plugins/calendar/skins/default/calendar.css b/plugins/calendar/skins/larry/calendar.css
similarity index 63%
copy from plugins/calendar/skins/default/calendar.css
copy to plugins/calendar/skins/larry/calendar.css
index aa1d5507..488f37ca 100644
--- a/plugins/calendar/skins/default/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -1,1257 +1,1332 @@
-/*** Style for Calendar plugin ***/
+/**
+ * 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;
}
-#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;
+body.calendarmain #mainscreen {
left: 0;
- right: 0;
- bottom: 10px;
}
-#sidebar {
+#calendarsidebar {
position: absolute;
- top: 37px;
+ top: 0;
left: 10px;
bottom: 0;
- width: 230px;
+ width: 240px;
}
#datepicker {
+ margin-top: 12px;
width: 100%;
+ min-height: 190px;
}
#datepicker .ui-datepicker {
- width: 97% !important;
+ 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 {
- border-color: #c33;
- color: #a22;
+ 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;
-}
-
-#datepicker .ui-priority-secondary {
- opacity: 0.4;
+ background: #00acd4;
+ background: -moz-linear-gradient(top, #00acd4 0%, #008fc7 100%);
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#00acd4), color-stop(100%,#008fc7));
+ background: -o-linear-gradient(top, #00acd4 0%, #008fc7 100%);
+ background: -ms-linear-gradient(top, #00acd4 0%, #008fc7 100%);
+ background: linear-gradient(top, #00acd4 0%, #008fc7 100%);
+ filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00acd4', endColorstr='#008fc7', GradientType=0);
}
#datepicker td.ui-datepicker-week-col {
cursor: pointer;
}
-#sidebartoggle {
+#datepicker .ui-datepicker-title {
+ margin: 2px 2.3em 3px 2.3em;
+}
+
+#datepicker .ui-datepicker .ui-datepicker-prev,
+#datepicker .ui-datepicker .ui-datepicker-next {
+ top: 4px;
+}
+
+#calendarsidebartoggle {
position: absolute;
- left: 244px;
+ 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: 256px;
- right: 10px;
+ 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: 220px;
+ top: 276px;
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;
+ height: 20px;
+ padding: 6px 8px 2px;
display: block;
- background: #fff;
- border-bottom: 1px solid #EBEBEB;
+ position: relative;
white-space: nowrap;
}
#calendarslist li label {
display: block;
}
-#calendarslist li span {
+#calendarslist li span.calname {
cursor: default;
background: url(images/calendars.png) 0 -2px no-repeat;
- padding-left: 18px;
+ 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 {
- margin-right: 5px;
+ position: absolute;
+ top: 3px;
+ right: 5px;
}
#calendarslist li.selected {
- background-color: #ccc;
- border-bottom: 1px solid #bbb;
+ background-color: #c7e3ef;
}
-#calendarslist li.selected span {
+#calendarslist li.selected span.calname {
font-weight: bold;
}
-#calendarslist li.readonly span {
- background-position: 0 -20px;
+#calendarslist li.readonly span.calname {
+ background-position: 0 -20px;
+}
+
+#calendarslist li.other span.calname {
+ background-position: 0 -38px;
}
-#calendarslist li.other span {
- background-position: 0 -38px;
+#calendarslist li.other.readonly span.calname {
+ background-position: 0 -56px;
}
-#calendarslist li.other.readonly span {
- background-position: 0 -56px;
+#calendarslist li.shared span.calname {
+ background-position: 0 -74px;
}
-#calendarslist li.shared span {
- background-position: 0 -74px;
+#calendarslist li.shared.readonly span.calname {
+ background-position: 0 -92px;
}
-#calendarslist li.shared.readonly span {
- 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;
+ top: -6px;
+ right: 0;
+ height: 40px;
+ z-index: 200;
}
#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;
}
+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: #C33;
+ 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;
}
-.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;
+#edit-attachments {
+ margin-top: 0.6em;
}
-.attachments-list ul li a {
+#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;
}
-.attachments-list ul li a:hover {
- text-decoration: underline;
+#edit-attachments ul li a.file {
+ padding: 0;
}
-#attachmentlist {
- margin: 0 -0.8em;
+#edit-attachments-form {
+ padding-top: 1.2em;
}
-#attachmentlist li {
- padding: 2px 2px 3px 0.8em;
+#edit-attachments-form .buttons {
+ margin: 0.5em 0;
}
-#eventshow .attachments-list ul li {
+#event-attachments .attachmentslist li {
float: left;
+ margin-right: 1em;
}
-#edit-attachments-form {
- padding-top: 1.2em;
-}
-
-#edit-attachments-form .buttons {
- margin: 0.5em 0;
+#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: 20px;
- margin: 0.1em 0 0.4em 0;
+ font-size: 18px;
+ margin: -0.3em 0 0.4em 0;
}
#eventshow label,
#eventshow h5.label {
font-weight: normal;
- font-size: 0.9em;
+ 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 .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;
+ 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%;
- display: table;
- table-layout: fixed;
- border-collapse: collapse;
- border: 1px solid #ccc;
-}
-
-#edit-attendees-table td {
- padding: 3px;
- border-bottom: 1px solid #ccc;
+ margin-top: 0.5em;
}
#edit-attendees-table td.role {
- width: 8em;
+ width: 9em;
}
#edit-attendees-table td.availability,
#edit-attendees-table td.confirmstate {
width: 4em;
}
#edit-attendees-table td.options {
width: 3em;
text-align: right;
padding-right: 4px;
}
#edit-attendees-table td.name {
width: auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
-#edit-attendees-table 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 {
+ border: 0;
+ color: #999;
+ font-size: 90%;
text-align: right;
- padding-right: 0.5em;
+ 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;
+ bottom: 28px;
left: 0;
right: 0;
- bottom: 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 table td.title {
+ font-weight: bold;
+ white-space: nowrap;
+ color: #666;
+ padding-right: 10px;
}
-#calendar-kolabform fieldset.tabbed {
- background-color: #fff;
- margin-top: 0;
+.propform fieldset.tab {
+ background: #efefef;
+ display: block;
+ margin-top: 0.5em;
+ padding: 0.5em 1em;
}
-#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;
+/* 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;
}
-#calendar-kolabform span.tablink-selected {
+.fc-content {
+ position: absolute !important;
+ top: 38px;
+ left: 0;
+ right: 0;
+ bottom: 28px;
background: #fff;
- height: 25px !important;
}
-#calendar-kolabform span.tablink a,
-#calendar-kolabform span.tablink-selected a {
+#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;
- 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;
+.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;
+}
-/* fullcalendar style overrides */
+.calendarmain .fc-header .fc-button {
+ margin-left: -1px;
+ margin-right: 0;
+}
-.rcube-fc-content {
- position: absolute !important;
- top: 37px;
- left: 0;
- right: 0;
- bottom: 0;
- overflow: hidden;
+.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: 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-mon, .fc-tue, .fc-wed, .fc-thu, .fc-fri {
+.fc-sat, .fc-sun {
background-color: #fdfdfd;
}
.fc-widget-header {
- background-color: #fff;
+ 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;
+ 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-content {
- background: #F6F6F6;
- padding: 2px;
+.calendarmain .fc-view-table tr.fc-event td {
+ border-color: #ddd;
+ padding: 4px 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 col.fc-event-location {
+ width: 20%;
}
-.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 {
- background: url(images/calendars.png) 7px -109px no-repeat;
+#messagemenu li a.calendarlink span.calendar {
+ background-position: 0px -1948px;
}
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;
+ 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,
-div.calendar-invitebox select {
- font-size: 11px;
+div.calendar-invitebox input.button {
+ font-weight: bold;
margin-right: 0.5em;
}
div.calendar-invitebox .calendar-select {
- font-size: 11px;
+ 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/attachment.html b/plugins/calendar/skins/larry/templates/attachment.html
new file mode 100644
index 00000000..439afd40
--- /dev/null
+++ b/plugins/calendar/skins/larry/templates/attachment.html
@@ -0,0 +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">
+
+<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 id="partheader" class="uibox">
+ <roundcube:object name="plugin.attachmentcontrols" class="headers-table" />
+ </div>
+
+ <div id="attachmentcontainer" class="uibox">
+ <roundcube:object name="plugin.attachmentframe" id="attachmentframe" style="width:100%; height:100%" />
+ </div>
+
+</div>
+
+</body>
+</html>
+
diff --git a/plugins/calendar/skins/larry/templates/calendar.html b/plugins/calendar/skins/larry/templates/calendar.html
new file mode 100644
index 00000000..c072e352
--- /dev/null
+++ b/plugins/calendar/skins/larry/templates/calendar.html
@@ -0,0 +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/$__skin_path/iehacks.css" /><![endif]-->
+</head>
+<body class="calendarmain noscroll">
+
+<roundcube:include file="/includes/header.html" />
+
+<div id="mainscreen">
+ <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="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
+ $('#calendarsidebartoggle').click(function() {
+ var width = $(this).data('sidebarwidth');
+ var offset = $(this).data('offset');
+ var $sidebar = $('#calendarsidebar'), time = 250;
+
+ if ($sidebar.is(':visible')) {
+ $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(){ $('#calendarsidebartoggle').removeClass('sidebarclosed'); });
+ $('#calendar').animate({ left:(width+16)+'px'}, time, function(){ $(this).fullCalendar('render'); });
+ }
+ })
+ .data('offset', $('#calendarsidebartoggle').position().left)
+ .data('sidebarwidth', $('#calendarsidebar').width() + $('#calendarsidebar').position().left);
+});
+
+</script>
+
+</body>
+</html>
diff --git a/plugins/calendar/skins/larry/templates/eventedit.html b/plugins/calendar/skins/larry/templates/eventedit.html
new file mode 100644
index 00000000..a3f58e8b
--- /dev/null
+++ b/plugins/calendar/skins/larry/templates/eventedit.html
@@ -0,0 +1,100 @@
+<div id="eventedit" class="uidialog uidialog-tabbed">
+ <form id="eventtabs" action="#" method="post" enctype="multipart/form-data">
+ <ul>
+ <li><a href="#event-tab-1"><roundcube:label name="calendar.tabsummary" /></a></li><li id="edit-tab-recurrence"><a href="#event-tab-2"><roundcube:label name="calendar.tabrecurrence" /></a></li><li id="edit-tab-attendees"><a href="#event-tab-3"><roundcube:label name="calendar.tabattendees" /></a></li><li id="edit-tab-attachments"><a href="#event-tab-4"><roundcube:label name="calendar.tabattachments" /></a></li>
+ </ul>
+ <!-- basic info -->
+ <div id="event-tab-1">
+ <div class="event-section">
+ <label for="edit-title"><roundcube:label name="calendar.title" /></label>
+ <br />
+ <input type="text" class="text" name="title" id="edit-title" size="40" />
+ </div>
+ <div class="event-section">
+ <label for="edit-location"><roundcube:label name="calendar.location" /></label>
+ <br />
+ <input type="text" class="text" name="location" id="edit-location" size="40" />
+ </div>
+ <div class="event-section">
+ <label for="edit-description"><roundcube:label name="calendar.description" /></label>
+ <br />
+ <textarea name="description" id="edit-description" class="text" rows="5" cols="40"></textarea>
+ </div>
+ <div class="event-section">
+ <label style="float:right;padding-right:0.5em"><input type="checkbox" name="allday" id="edit-allday" value="1" /><roundcube:label name="calendar.all-day" /></label>
+ <label for="edit-startdate"><roundcube:label name="calendar.start" /></label>
+ <input type="text" name="startdate" size="10" id="edit-startdate" /> &nbsp;
+ <input type="text" name="starttime" size="6" id="edit-starttime" />
+ </div>
+ <div class="event-section">
+ <label for="edit-enddate"><roundcube:label name="calendar.end" /></label>
+ <input type="text" name="enddate" size="10" id="edit-enddate" /> &nbsp;
+ <input type="text" name="endtime" size="6" id="edit-endtime" />
+ </div>
+ <div class="event-section" id="edit-alarms">
+ <label for="edit-alarm"><roundcube:label name="calendar.alarms" /></label>
+ <roundcube:object name="plugin.alarm_select" />
+ </div>
+ <div class="event-section" id="calendar-select">
+ <label for="edit-calendar"><roundcube:label name="calendar.calendar" /></label>
+ <roundcube:object name="plugin.calendar_select" id="edit-calendar" />
+ </div>
+ <div class="event-section">
+ <label for="edit-categories"><roundcube:label name="calendar.category" /></label>
+ <roundcube:object name="plugin.category_select" id="edit-categories" />
+ </div>
+ <div class="event-section">
+ <label for="edit-free-busy"><roundcube:label name="calendar.freebusy" /></label>
+ <roundcube:object name="plugin.freebusy_select" id="edit-free-busy" />
+ </div>
+ <div class="event-section">
+ <label for="edit-priority"><roundcube:label name="calendar.priority" /></label>
+ <roundcube:object name="plugin.priority_select" id="edit-priority" />
+ </div>
+ <div class="event-section">
+ <label for="edit-sensitivity"><roundcube:label name="calendar.sensitivity" /></label>
+ <roundcube:object name="plugin.sensitivity_select" id="edit-sensitivity" />
+ </div>
+ </div>
+ <!-- recurrence settings -->
+ <div id="event-tab-2">
+ <div class="event-section border-after">
+ <roundcube:object name="plugin.recurrence_form" part="frequency" />
+ </div>
+ <div class="recurrence-form border-after" id="recurrence-form-daily">
+ <roundcube:object name="plugin.recurrence_form" part="daily" class="event-section" />
+ </div>
+ <div class="recurrence-form border-after" id="recurrence-form-weekly">
+ <roundcube:object name="plugin.recurrence_form" part="weekly" class="event-section" />
+ </div>
+ <div class="recurrence-form border-after" id="recurrence-form-monthly">
+ <roundcube:object name="plugin.recurrence_form" part="monthly" class="event-section" />
+ </div>
+ <div class="recurrence-form border-after" id="recurrence-form-yearly">
+ <roundcube:object name="plugin.recurrence_form" part="yearly" class="event-section" />
+ </div>
+ <div class="recurrence-form" id="recurrence-form-until">
+ <roundcube:object name="plugin.recurrence_form" part="until" class="event-section" />
+ </div>
+ </div>
+ <!-- attendees list -->
+ <div id="event-tab-3">
+ <roundcube:object name="plugin.attendees_list" id="edit-attendees-table" class="records-table" />
+ <roundcube:object name="plugin.attendees_form" id="edit-attendees-form" />
+ <roundcube:include file="/templates/freebusylegend.html" />
+ </div>
+ <!-- attachments list (with upload form) -->
+ <div id="event-tab-4">
+ <div id="edit-attachments">
+ <roundcube:object name="plugin.attachments_list" id="attachment-list" class="attachmentslist" cancelIcon="/images/0.gif" />
+ </div>
+ <div id="edit-attachments-form">
+ <roundcube:object name="plugin.attachments_form" id="calendar-attachment-form" attachmentFieldSize="30" />
+ </div>
+ </div>
+ </form>
+
+ <roundcube:object name="plugin.edit_attendees_notify" id="edit-attendees-notify" class="event-dialog-message" style="display:none" />
+ <roundcube:object name="plugin.edit_recurring_warning" class="event-dialog-message edit-recurring-warning" style="display:none" />
+ <div id="edit-localchanges-warning" class="event-dialog-message" style="display:none"><roundcube:label name="calendar.localchangeswarning" /></div>
+</div>
\ No newline at end of file
diff --git a/plugins/calendar/skins/larry/templates/freebusylegend.html b/plugins/calendar/skins/larry/templates/freebusylegend.html
new file mode 100644
index 00000000..51e5ddc4
--- /dev/null
+++ b/plugins/calendar/skins/larry/templates/freebusylegend.html
@@ -0,0 +1,7 @@
+ <div id="edit-attendees-legend" class="availability">
+ <span class="legend"><img class="availabilityicon free" src="./program/blank.gif" /> <roundcube:label name="calendar.availfree" /></span>
+ <span class="legend"><img class="availabilityicon busy" src="./program/blank.gif" /> <roundcube:label name="calendar.availbusy" /></span>
+ <span class="legend"><img class="availabilityicon tentative" src="./program/blank.gif" /> <roundcube:label name="calendar.availtentative" /></span>
+ <span class="legend"><img class="availabilityicon out-of-office" src="./program/blank.gif" /> <roundcube:label name="calendar.availoutofoffice" /></span>
+ <span class="legend"><img class="availabilityicon unknown" src="./program/blank.gif" /> <roundcube:label name="calendar.availunknown" /></span>
+ </div>
diff --git a/plugins/calendar/skins/larry/templates/itipattend.html b/plugins/calendar/skins/larry/templates/itipattend.html
new file mode 100644
index 00000000..bfbb08f5
--- /dev/null
+++ b/plugins/calendar/skins/larry/templates/itipattend.html
@@ -0,0 +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 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>
+
+</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
new file mode 100644
index 00000000..05a279ad
--- /dev/null
+++ b/plugins/calendar/skins/larry/templates/kolabacl.html
@@ -0,0 +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 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
new file mode 100644
index 00000000..77a1c30e
--- /dev/null
+++ b/plugins/calendar/skins/larry/templates/kolabform.html
@@ -0,0 +1,9 @@
+<div id="calendar-kolabform" class="propform tabbed">
+ <roundcube:object name="calendarform" />
+</div>
+<style type="text/css">
+#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/calendar/skins/larry/templates/print.html b/plugins/calendar/skins/larry/templates/print.html
new file mode 100644
index 00000000..1c31ca29
--- /dev/null
+++ b/plugins/calendar/skins/larry/templates/print.html
@@ -0,0 +1,28 @@
+<roundcube:object name="doctype" value="html5" />
+<html>
+<head>
+<title><roundcube:object name="pagetitle" /></title>
+</head>
+<body class="calendarprint">
+
+<div id="printconfig" class="noprint">
+ <div class="calwidth">
+ <a href="#close" onclick="window.close()" class="rightalign"><roundcube:label name="close" /></a>
+ <span class="prop"><input type="button" id="printme" value="<roundcube:label name='print' />" onclick="window.print()"></span>
+ <span class="prop"><label><input type="checkbox" id="propdescription" checked="checked" /> <roundcube:label name="calendar.printdescriptions" /></label></span>
+ </div>
+</div>
+
+<roundcube:object name="message" id="message" class="noprint" />
+
+<div id="calendar" class="calwidth"></div>
+
+<div class="calwidth">
+ <roundcube:object name="plugin.calendar_list" activeonly="true" id="calendarlist" />
+</div>
+
+<roundcube:object name="plugin.calendar_css" printmode="true" />
+
+<!--[if lte IE 7]><link rel="stylesheet" type="text/css" href="./plugins/calendar/skins/default/print.iehacks.css" /><![endif]-->
+</body>
+</html>
\ No newline at end of file
diff --git a/plugins/kolab_addressbook/kolab_addressbook.php b/plugins/kolab_addressbook/kolab_addressbook.php
index f33a0372..c03c4263 100644
--- a/plugins/kolab_addressbook/kolab_addressbook.php
+++ b/plugins/kolab_addressbook/kolab_addressbook.php
@@ -1,592 +1,590 @@
<?php
/**
* Kolab address book
*
* Sample plugin to add a new address book source with data from Kolab storage
* It provides also a possibilities to manage contact folders
* (create/rename/delete/acl) directly in Addressbook UI.
*
* @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/>.
*/
class kolab_addressbook extends rcube_plugin
{
public $task = 'mail|settings|addressbook|calendar';
private $folders;
private $sources;
private $rc;
private $ui;
const GLOBAL_FIRST = 0;
const PERSONAL_FIRST = 1;
const GLOBAL_ONLY = 2;
const PERSONAL_ONLY = 3;
/**
* Startup method of a Roundcube plugin
*/
public function init()
{
require_once(dirname(__FILE__) . '/lib/rcube_kolab_contacts.php');
$this->rc = rcmail::get_instance();
// load required plugin
$this->require_plugin('kolab_core');
// register hooks
$this->add_hook('addressbooks_list', array($this, 'address_sources'));
$this->add_hook('addressbook_get', array($this, 'get_address_book'));
$this->add_hook('config_get', array($this, 'config_get'));
if ($this->rc->task == 'addressbook') {
$this->add_texts('localization');
$this->add_hook('contact_form', array($this, 'contact_form'));
// Plugin actions
$this->register_action('plugin.book', array($this, 'book_actions'));
$this->register_action('plugin.book-save', array($this, 'book_save'));
// Load UI elements
if ($this->api->output->type == 'html') {
require_once($this->home . '/lib/kolab_addressbook_ui.php');
$this->ui = new kolab_addressbook_ui($this);
}
}
else if ($this->rc->task == 'settings') {
$this->add_texts('localization');
$this->add_hook('preferences_list', array($this, 'prefs_list'));
$this->add_hook('preferences_save', array($this, 'prefs_save'));
}
}
/**
* Handler for the addressbooks_list hook.
*
* This will add all instances of available Kolab-based address books
* to the list of address sources of Roundcube.
* This will also hide some addressbooks according to kolab_addressbook_prio setting.
*
* @param array $p Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function address_sources($p)
{
// Load configuration
$this->load_config();
$abook_prio = (int) $this->rc->config->get('kolab_addressbook_prio');
$undelete = $this->rc->config->get('undo_timeout');
// Disable all global address books
// Assumes that all non-kolab_addressbook sources are global
if ($abook_prio == self::PERSONAL_ONLY) {
$p['sources'] = array();
}
$sources = array();
$names = array();
foreach ($this->_list_sources() as $abook_id => $abook) {
$name = $origname = $abook->get_name();
// find folder prefix to truncate
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;
// register this address source
$sources[$abook_id] = array(
'id' => $abook_id,
'name' => $name,
'readonly' => $abook->readonly,
'editable' => $abook->editable,
'groups' => $abook->groups,
'undelete' => $abook->undelete && $undelete,
- 'realname' => rcube_charset_convert($abook->get_realname(), 'UTF7-IMAP'), // IMAP folder name
+ 'realname' => rcube_charset::convert($abook->get_realname(), 'UTF7-IMAP'), // IMAP folder name
'class_name' => $abook->get_namespace(),
'kolab' => true,
);
}
// Add personal address sources to the list
if ($abook_prio == self::PERSONAL_FIRST) {
// $p['sources'] = array_merge($sources, $p['sources']);
// Don't use array_merge(), because if you have folders name
// that resolve to numeric identifier it will break output array keys
foreach ($p['sources'] as $idx => $value)
$sources[$idx] = $value;
$p['sources'] = $sources;
}
else {
// $p['sources'] = array_merge($p['sources'], $sources);
foreach ($sources as $idx => $value)
$p['sources'][$idx] = $value;
}
return $p;
}
/**
* Sets autocomplete_addressbooks option according to
* kolab_addressbook_prio setting extending list of address sources
* to be used for autocompletion.
*/
public function config_get($args)
{
if ($args['name'] != 'autocomplete_addressbooks') {
return $args;
}
// Load configuration
$this->load_config();
$abook_prio = (int) $this->rc->config->get('kolab_addressbook_prio');
// here we cannot use rc->config->get()
$sources = $GLOBALS['CONFIG']['autocomplete_addressbooks'];
// Disable all global address books
// Assumes that all non-kolab_addressbook sources are global
if ($abook_prio == self::PERSONAL_ONLY) {
$sources = array();
}
if (!is_array($sources)) {
$sources = array();
}
$kolab_sources = array();
foreach ($this->_list_sources() as $abook_id => $abook) {
if (!in_array($abook_id, $sources))
$kolab_sources[] = $abook_id;
}
// Add personal address sources to the list
if (!empty($kolab_sources)) {
if ($abook_prio == self::PERSONAL_FIRST) {
$sources = array_merge($kolab_sources, $sources);
}
else {
$sources = array_merge($sources, $kolab_sources);
}
}
$args['result'] = $sources;
return $args;
}
/**
* Getter for the rcube_addressbook instance
*
* @param array $p Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function get_address_book($p)
{
if ($p['id']) {
$this->_list_sources();
if ($this->sources[$p['id']]) {
$p['instance'] = $this->sources[$p['id']];
}
}
return $p;
}
private function _list_sources()
{
// already read sources
if (isset($this->sources))
return $this->sources;
$this->sources = array();
// Load configuration
$this->load_config();
$abook_prio = (int) $this->rc->config->get('kolab_addressbook_prio');
// Personal address source(s) disabled?
if ($abook_prio == self::GLOBAL_ONLY) {
return $this->sources;
}
// get all folders that have "contact" type
$this->folders = rcube_kolab::get_folders('contact');
if (PEAR::isError($this->folders)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed to list contact folders from Kolab server:" . $this->folders->getMessage()),
true, false);
}
else {
// convert to UTF8 and sort
$names = array();
foreach ($this->folders as $c_folder)
- $names[$c_folder->name] = rcube_charset_convert($c_folder->name, 'UTF7-IMAP');
+ $names[$c_folder->name] = rcube_charset::convert($c_folder->name, 'UTF7-IMAP');
asort($names, SORT_LOCALE_STRING);
foreach ($names as $utf7name => $name) {
// create instance of rcube_contacts
$abook_id = rcube_kolab::folder_id($utf7name);
$abook = new rcube_kolab_contacts($utf7name);
$this->sources[$abook_id] = $abook;
}
}
return $this->sources;
}
/**
* Plugin hook called before rendering the contact form or detail view
*
* @param array $p Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function contact_form($p)
{
// none of our business
if (!is_object($GLOBALS['CONTACTS']) || !is_a($GLOBALS['CONTACTS'], 'rcube_kolab_contacts'))
return $p;
// extend the list of contact fields to be displayed in the 'personal' section
if (is_array($p['form']['personal'])) {
$p['form']['contact']['content']['officelocation'] = array('size' => 40);
$p['form']['personal']['content']['initials'] = array('size' => 6);
$p['form']['personal']['content']['profession'] = array('size' => 40);
$p['form']['personal']['content']['children'] = array('size' => 40);
$p['form']['personal']['content']['pgppublickey'] = array('size' => 40);
$p['form']['personal']['content']['freebusyurl'] = array('size' => 40);
// re-order fields according to the coltypes list
$p['form']['contact']['content'] = $this->_sort_form_fields($p['form']['contact']['content']);
$p['form']['personal']['content'] = $this->_sort_form_fields($p['form']['personal']['content']);
/* define a separate section 'settings'
$p['form']['settings'] = array(
'name' => $this->gettext('settings'),
'content' => array(
'pgppublickey' => array('size' => 40, 'visible' => true),
'freebusyurl' => array('size' => 40, 'visible' => true),
)
);
*/
}
return $p;
}
private function _sort_form_fields($contents)
{
$block = array();
$contacts = reset($this->sources);
foreach ($contacts->coltypes as $col => $prop) {
if (isset($contents[$col]))
$block[$col] = $contents[$col];
}
return $block;
}
/**
* Handler for user preferences form (preferences_list hook)
*
* @param array $args Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function prefs_list($args)
{
if ($args['section'] != 'addressbook') {
return $args;
}
// Load configuration
$this->load_config();
// Load localization
$this->add_texts('localization');
// Check that configuration is not disabled
$dont_override = (array) $this->rc->config->get('dont_override', array());
if (!in_array('kolab_addressbook_prio', $dont_override)) {
$field_id = '_kolab_addressbook_prio';
$select = new html_select(array('name' => $field_id, 'id' => $field_id));
$select->add($this->gettext('globalfirst'), self::GLOBAL_FIRST);
$select->add($this->gettext('personalfirst'), self::PERSONAL_FIRST);
$select->add($this->gettext('globalonly'), self::GLOBAL_ONLY);
$select->add($this->gettext('personalonly'), self::PERSONAL_ONLY);
$args['blocks']['main']['options']['kolab_addressbook_prio'] = array(
'title' => html::label($field_id, Q($this->gettext('addressbookprio'))),
'content' => $select->show((int)$this->rc->config->get('kolab_addressbook_prio')),
);
}
return $args;
}
/**
* Handler for user preferences save (preferences_save hook)
*
* @param array $args Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function prefs_save($args)
{
if ($args['section'] != 'addressbook') {
return $args;
}
// Load configuration
$this->load_config();
// Check that configuration is not disabled
$dont_override = (array) $this->rc->config->get('dont_override', array());
if (!in_array('kolab_addressbook_prio', $dont_override)) {
$key = 'kolab_addressbook_prio';
$args['prefs'][$key] = (int) get_input_value('_'.$key, RCUBE_INPUT_POST);
}
return $args;
}
/**
* Handler for plugin actions
*/
public function book_actions()
{
$action = trim(get_input_value('_act', RCUBE_INPUT_GPC));
if ($action == 'create') {
$this->ui->book_edit();
}
else if ($action == 'edit') {
$this->ui->book_edit();
}
else if ($action == 'delete') {
$this->book_delete();
}
}
/**
* Handler for address book create/edit form submit
*/
public function book_save()
{
+ $storage = $this->rc->get_storage();
$folder = trim(get_input_value('_name', RCUBE_INPUT_POST, true, 'UTF7-IMAP'));
$oldfolder = trim(get_input_value('_oldname', RCUBE_INPUT_POST, true)); // UTF7-IMAP
$path = trim(get_input_value('_parent', RCUBE_INPUT_POST, true)); // UTF7-IMAP
- $delimiter = $_SESSION['imap_delimiter'];
+ $delimiter = $storage->get_hierarchy_delimiter();
if (strlen($oldfolder)) {
- $this->rc->imap_connect();
- $options = $this->rc->imap->mailbox_info($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)) {
$error = rcube_label('cannotbeempty');
}
else if (strlen($folder) > 128) {
$error = rcube_label('nametoolong');
}
else {
// these characters are problematic e.g. when used in LIST/LSUB
foreach (array($delimiter, '%', '*') as $char) {
if (strpos($folder, $delimiter) !== false) {
$error = rcube_label('forbiddencharacter') . " ($char)";
break;
}
}
}
if (!$error) {
if (!empty($options) && ($options['protected'] || $options['norename'])) {
$folder = $oldfolder;
}
else if (strlen($path)) {
$folder = $path . $delimiter . $folder;
}
else {
// add namespace prefix (when needed)
- $this->rc->imap_init();
- $folder = $this->rc->imap->mod_mailbox($folder, 'in');
+ $folder = $storage->mod_folder($folder, 'in');
}
// Check access rights to the parent folder
if (strlen($path) && (!strlen($oldfolder) || $oldfolder != $folder)) {
- $this->rc->imap_connect();
- $parent_opts = $this->rc->imap->mailbox_info($path);
+ $parent_opts = $storage->folder_info($path);
if ($parent_opts['namespace'] != 'personal'
&& (empty($parent_opts['rights']) || !preg_match('/[ck]/', implode($parent_opts['rights'])))
) {
$error = rcube_label('parentnotwritable');
}
}
}
if (!$error) {
// update the folder name
if (strlen($oldfolder)) {
$type = 'update';
$plugin = $this->rc->plugins->exec_hook('addressbook_update', array(
'name' => $folder, 'oldname' => $oldfolder));
if (!$plugin['abort']) {
if ($oldfolder != $folder)
$result = rcube_kolab::folder_rename($oldfolder, $folder);
else
$result = true;
}
else {
$result = $plugin['result'];
}
}
// create new folder
else {
$type = 'create';
$plugin = $this->rc->plugins->exec_hook('addressbook_create', array('name' => $folder));
$folder = $plugin['name'];
if (!$plugin['abort']) {
$result = rcube_kolab::folder_create($folder, 'contact', false);
}
else {
$result = $plugin['result'];
}
}
}
if ($result) {
$kolab_folder = new rcube_kolab_contacts($folder);
// create display name for the folder (see self::address_sources())
if (strpos($folder, $delimiter)) {
$names = array();
foreach ($this->_list_sources() as $abook_id => $abook) {
$realname = $abook->get_realname();
// The list can be not updated yet, handle old folder name
if ($type == 'update' && $realname == $oldfolder) {
$abook = $kolab_folder;
$realname = $folder;
}
$name = $origname = $abook->get_name();
// find folder prefix to truncate
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;
if ($realname == $folder) {
break;
}
}
}
else {
$name = $kolab_folder->get_name();
}
$this->rc->output->show_message('kolab_addressbook.book'.$type.'d', 'confirmation');
$this->rc->output->command('set_env', 'delimiter', $delimiter);
$this->rc->output->command('book_update', array(
'id' => rcube_kolab::folder_id($folder),
'name' => $name,
'readonly' => false,
'editable' => true,
'groups' => true,
- 'realname' => rcube_charset_convert($folder, 'UTF7-IMAP'), // IMAP folder name
+ 'realname' => rcube_charset::convert($folder, 'UTF7-IMAP'), // IMAP folder name
'class_name' => $kolab_folder->get_namespace(),
'kolab' => true,
), rcube_kolab::folder_id($oldfolder));
$this->rc->output->send('iframe');
}
if (!$error)
$error = $plugin['message'] ? $plugin['message'] : 'kolab_addressbook.book'.$type.'error';
$this->rc->output->show_message($error, 'error');
// display the form again
$this->ui->book_edit();
}
/**
* Handler for address book delete action (AJAX)
*/
private function book_delete()
{
$folder = trim(get_input_value('_source', RCUBE_INPUT_GPC, true, 'UTF7-IMAP'));
if (rcube_kolab::folder_delete($folder)) {
$this->rc->output->show_message('kolab_addressbook.bookdeleted', 'confirmation');
$this->rc->output->set_env('pagecount', 0);
$this->rc->output->command('set_rowcount', rcmail_get_rowcount_text(new rcube_result_set()));
$this->rc->output->command('list_contacts_clear');
$this->rc->output->command('book_delete_done', rcube_kolab::folder_id($folder));
}
else {
$this->rc->output->show_message('kolab_addressbook.bookdeleteerror', 'error');
}
$this->rc->output->send();
}
}
diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
index 9c40afb9..f028f522 100644
--- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
+++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
@@ -1,1223 +1,1232 @@
<?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)
+ 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)
+ 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'];
- }
- }
- 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'];
- }
+ $a_value = $b_value = '';
+
+ switch ($this->sort_col) {
+ case 'name':
+ $a_value = $a['name'] . $a['prefix'];
+ $b_value = $b['name'] . $b['prefix'];
+ case 'firstname':
+ $a_value .= $a['firstname'] . $a['middlename'] . $a['surname'];
+ $b_value .= $b['firstname'] . $b['middlename'] . $b['surname'];
+ break;
+
+ case 'surname':
+ $a_value = $a['surname'] . $a['firstname'] . $a['middlename'];
+ $b_value = $b['surname'] . $b['firstname'] . $b['middlename'];
+ break;
+
+ default:
+ $a_value = $a[$this->sort_col];
+ $b_value = $b[$this->sort_col];
+ break;
}
- // return strcasecmp($a_name, $b_name);
+ $a_value .= is_array($a['email']) ? $a['email'][0] : $a['email'];
+ $b_value .= is_array($b['email']) ? $b['email'][0] : $b['email'];
+
+ // 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_addressbook/package.xml b/plugins/kolab_addressbook/package.xml
index 7e7b84c7..e077dcb5 100644
--- a/plugins/kolab_addressbook/package.xml
+++ b/plugins/kolab_addressbook/package.xml
@@ -1,74 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<package xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" packagerversion="1.9.0" version="2.0" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
http://pear.php.net/dtd/tasks-1.0.xsd
http://pear.php.net/dtd/package-2.0
http://pear.php.net/dtd/package-2.0.xsd">
<name>kolab_addressbook</name>
- <uri>http://kolabsys.com</uri>
+ <uri>http://git.kolab.org/roundcube-plugins-kolab/</uri>
<summary>Kolab addressbook</summary>
<description>
Sample plugin to add a new address book source with data from Kolab storage.
It provides also a possibilities to manage contact folders
(create/rename/delete/acl) directly in Addressbook UI.
</description>
<lead>
<name>Aleksander Machniak</name>
<user>machniak</user>
<email>machniak@kolabsys.com</email>
<active>yes</active>
</lead>
<lead>
<name>Thomas Bruederli</name>
<user>bruederli</user>
<email>bruederli@kolabsys.com</email>
<active>yes</active>
</lead>
<date>2011-11-01</date>
<version>
<release>0.6</release>
<api>0.6</api>
</version>
<stability>
<release>stable</release>
<api>stable</api>
</stability>
<license uri="http://www.gnu.org/licenses/agpl.html">GNU AGPLv3</license>
<notes>-</notes>
<contents>
<dir baseinstalldir="/" name="/">
<file name="kolab_addressbook.php" role="php">
<tasks:replace from="@name@" to="name" type="package-info"/>
<tasks:replace from="@package_version@" to="version" type="package-info"/>
</file>
<file name="lib/kolab_addressbook_ui.php" role="php"></file>
<file name="lib/rcube_kolab_contacts.php" role="php"></file>
<file name="config.inc.php.dist" role="data"></file>
<file name="LICENSE" role="data"></file>
<file name="skins/default/kolab_addressbook.css" role="data"></file>
<file name="skins/default/kolab_folders.gif" role="data"></file>
<file name="skins/default/kolab_folders.png" role="data"></file>
<file name="skins/default/templates/bookedit.html" role="data"></file>
<file name="localization/en_US.inc" role="data"></file>
<file name="localization/ja_JP.inc" role="data"></file>
<file name="localization/pl_PL.inc" role="data"></file>
<file name="localization/ru_RU.inc" role="data"></file>
</dir>
<!-- / -->
</contents>
<dependencies>
<required>
<php>
<min>5.2.1</min>
</php>
<pearinstaller>
<min>1.7.0</min>
</pearinstaller>
<package>
<name>kolab_core</name>
<uri>http://kolabsys.com</uri>
</package>
</required>
</dependencies>
<phprelease/>
</package>
diff --git a/plugins/kolab_auth/kolab_auth.php b/plugins/kolab_auth/kolab_auth.php
index 3fa85248..691f5422 100644
--- a/plugins/kolab_auth/kolab_auth.php
+++ b/plugins/kolab_auth/kolab_auth.php
@@ -1,488 +1,500 @@
<?php
/**
* Kolab Authentication (based on ldap_authentication plugin)
*
* Authenticates on LDAP server, finds canonized authentication ID for IMAP
* and for new users creates identity based on LDAP information.
*
* Supports impersonate feature (login as another user). To use this feature
* imap_auth_type/smtp_auth_type must be set to DIGEST-MD5 or PLAIN.
*
* @version @package_version@
* @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/>.
*/
class kolab_auth extends rcube_plugin
{
private $ldap;
private $data = array();
public function init()
{
$rcmail = rcmail::get_instance();
$this->add_hook('authenticate', array($this, 'authenticate'));
$this->add_hook('startup', array($this, 'startup'));
$this->add_hook('user_create', array($this, 'user_create'));
// Hooks related to "Login As" feature
$this->add_hook('template_object_loginform', array($this, 'login_form'));
- $this->add_hook('imap_connect', array($this, 'imap_connect'));
+ $this->add_hook('storage_connect', array($this, 'imap_connect'));
$this->add_hook('managesieve_connect', array($this, 'imap_connect'));
$this->add_hook('smtp_connect', array($this, 'smtp_connect'));
$this->add_hook('write_log', array($this, 'write_log'));
// TODO: This section does not actually seem to work
if ($rcmail->config->get('kolab_auth_auditlog', false)) {
$rcmail->config->set('debug_level', 1);
$rcmail->config->set('devel_mode', true);
$rcmail->config->set('smtp_log', true);
$rcmail->config->set('log_logins', true);
$rcmail->config->set('log_session', true);
$rcmail->config->set('sql_debug', true);
$rcmail->config->set('memcache_debug', true);
$rcmail->config->set('imap_debug', true);
$rcmail->config->set('ldap_debug', true);
$rcmail->config->set('smtp_debug', true);
}
}
public function startup($args) {
// Arguments are task / action, not interested
if (!empty($_SESSION['user_roledns'])) {
$this->load_user_role_plugins_and_settings($_SESSION['user_roledns']);
}
return $args;
}
public function load_user_role_plugins_and_settings($role_dns) {
$rcmail = rcmail::get_instance();
$this->load_config();
// Check role dependent plugins to enable and settings to modify
// Example 'kolab_auth_role_plugins' =
//
// Array(
// '<role_dn>' => Array('plugin1', 'plugin2'),
// );
$role_plugins = $rcmail->config->get('kolab_auth_role_plugins');
// Example $rcmail_config['kolab_auth_role_settings'] =
//
// Array(
// '<role_dn>' => Array(
// '$setting' => Array(
// 'mode' => '(override|merge)', (default: override)
// 'value' => <>,
// 'allow_override' => (true|false) (default: false)
// ),
// ),
// );
$role_settings = $rcmail->config->get('kolab_auth_role_settings');
foreach ($role_dns as $role_dn) {
if (isset($role_plugins[$role_dn]) && is_array($role_plugins[$role_dn])) {
foreach ($role_plugins[$role_dn] as $plugin) {
$this->require_plugin($plugin);
}
}
if (isset($role_settings[$role_dn]) && is_array($role_settings[$role_dn])) {
foreach ($role_settings[$role_dn] as $setting_name => $setting) {
if (!isset($setting['mode'])) {
$setting['mode'] = 'override';
}
if ($setting['mode'] == "override") {
$rcmail->config->set($setting_name, $setting['value']);
} elseif ($setting['mode'] == "merge") {
$orig_setting = $rcmail->config->get($setting_name);
if (!empty($orig_setting)) {
if (is_array($orig_setting)) {
$rcmail->config->set($setting_name, array_merge($orig_setting, $setting['value']));
}
} else {
$rcmail->config->set($setting_name, $setting['value']);
}
}
if (!isset($setting['allow_override']) || !$setting['allow_override']) {
$rcmail->config->set('dont_override', array_merge($rcmail->config->get('dont_override', Array()), Array($setting_name)));
} else {
$dont_override = $rcmail->config->get('dont_override');
if (in_array($setting_name, $dont_override)) {
- $_dont_override = Array();
+ $_dont_override = array();
foreach ($dont_override as $_setting) {
if ($_setting != $setting_name) {
$_dont_override[] = $_setting;
}
}
$rcmail->config->set('dont_override', $_dont_override);
}
}
}
}
}
}
public function write_log($args) {
$rcmail = rcmail::get_instance();
if (!$rcmail->config->get('kolab_auth_auditlog', false)) {
return $args;
}
$args['abort'] = true;
if ($rcmail->config->get('log_driver') == 'syslog') {
$prio = $args['name'] == 'errors' ? LOG_ERR : LOG_INFO;
syslog($prio, $args['line']);
return $args;
} else {
$line = sprintf("[%s]: %s\n", $args['date'], $args['line']);
// log_driver == 'file' is assumed here
$log_dir = $rcmail->config->get('log_dir', INSTALL_PATH . 'logs');
// Append original username + target username
if (!is_dir($log_dir.'/'.strtolower($_SESSION['kolab_auth_admin']).'/'.strtolower($_SESSION['username']))) {
// Attempt to create the directory
if (@mkdir($log_dir.'/'.strtolower($_SESSION['kolab_auth_admin']).'/'.strtolower($_SESSION['username']), 0750, true)) {
$log_dir = $log_dir.'/'.strtolower($_SESSION['kolab_auth_admin']).'/'.strtolower($_SESSION['username']);
}
} else {
$log_dir = $log_dir.'/'.strtolower($_SESSION['kolab_auth_admin']).'/'.strtolower($_SESSION['username']);
}
// try to open specific log file for writing
$logfile = $log_dir.'/'.$args['name'];
if ($fp = fopen($logfile, 'a')) {
fwrite($fp, $line);
fflush($fp);
fclose($fp);
return $args;
}
else
trigger_error("Error writing to log file $logfile; Please check permissions", E_USER_WARNING);
}
return $args;
}
/**
* Sets defaults for new user.
*/
public function user_create($args)
{
if (!empty($this->data['user_email']))
$args['user_email'] = $this->data['user_email'];
if (!empty($this->data['user_name']))
$args['user_name'] = $this->data['user_name'];
if (!empty($this->data['user_alias']))
$args['user_alias'] = $this->data['user_alias'];
return $args;
}
/**
* Modifies login form adding additional "Login As" field
*/
public function login_form($args)
{
$this->load_config();
$this->add_texts('localization/');
$rcmail = rcmail::get_instance();
$admin_login = $rcmail->config->get('kolab_auth_admin_login');
$group = $rcmail->config->get('kolab_auth_group');
$role_attr = $rcmail->config->get('kolab_auth_role');
// Show "Login As" input
if (empty($admin_login) || (empty($group) && empty($role_attr))) {
return $args;
}
$input = new html_inputfield(array('name' => '_loginas', 'id' => 'rcmloginas',
'type' => 'text', 'autocomplete' => 'off'));
$row = html::tag('tr', null,
html::tag('td', 'title', html::label('rcmloginas', Q($this->gettext('loginas'))))
. html::tag('td', 'input', $input->show(trim(get_input_value('_loginas', RCUBE_INPUT_POST))))
);
$args['content'] = preg_replace('/<\/tbody>/i', $row . '</tbody>', $args['content']);
return $args;
}
/**
* Find user credentials In LDAP.
*/
public function authenticate($args)
{
$this->load_config();
if (!$this->init_ldap()) {
return $args;
}
$rcmail = rcmail::get_instance();
$admin_login = $rcmail->config->get('kolab_auth_admin_login');
$admin_pass = $rcmail->config->get('kolab_auth_admin_password');
$login_attr = $rcmail->config->get('kolab_auth_login');
$alias_attr = $rcmail->config->get('kolab_auth_alias');
$name_attr = $rcmail->config->get('kolab_auth_name');
// get username and host
$host = rcube_parse_host($args['host']);
$user = $args['user'];
$pass = $args['pass'];
$loginas = trim(get_input_value('_loginas', RCUBE_INPUT_POST));
if (empty($user) || empty($pass)) {
return $args;
}
// Find user record in LDAP
$record = $this->get_user_record($user, $host);
if (empty($record)) {
return $args;
}
$role_attr = $rcmail->config->get('kolab_auth_role');
if (!empty($role_attr) && !empty($record[$role_attr])) {
$_SESSION['user_roledns'] = (array)($record[$role_attr]);
}
// Login As...
if (!empty($loginas) && $admin_login) {
// Authenticate to LDAP
$dn = $this->ldap->dn_decode($record['ID']);
$result = $this->ldap->bind($dn, $pass);
if (!$result) {
return $args;
}
// check if the original user has/belongs to administrative role/group
$isadmin = false;
$group = $rcmail->config->get('kolab_auth_group');
$role_attr = $rcmail->config->get('kolab_auth_role');
$role_dn = $rcmail->config->get('kolab_auth_role_value');
// check role attribute
if (!empty($role_attr) && !empty($role_dn) && !empty($record[$role_attr])) {
$role_dn = $this->parse_vars($role_dn, $user, $host);
foreach ((array)$record[$role_attr] as $role) {
if ($role == $role_dn) {
$isadmin = true;
break;
}
}
}
// check group
if (!$isadmin && !empty($group)) {
$groups = $this->ldap->get_record_groups($record['ID']);
foreach ($groups as $g) {
if ($group == $this->ldap->dn_decode($g)) {
$isadmin = true;
break;
}
}
}
// Save original user login for log (see below)
if ($login_attr)
$origname = is_array($record[$login_attr]) ? $record[$login_attr][0] : $record[$login_attr];
else
$origname = $user;
$record = null;
// user has the privilage, get "login as" user credentials
if ($isadmin) {
$record = $this->get_user_record($loginas, $host);
}
if (empty($record)) {
$args['valid'] = false;
return $args;
}
$args['user'] = $loginas;
// Mark session to use SASL proxy for IMAP authentication
$_SESSION['kolab_auth_admin'] = strtolower($origname);
$_SESSION['kolab_auth_login'] = $rcmail->encrypt($admin_login);
$_SESSION['kolab_auth_password'] = $rcmail->encrypt($admin_pass);
}
// Set credentials
if ($record) {
+ // Store UID in session for use by other plugins
+ $_SESSION['kolab_uid'] = is_array($record['uid']) ? $record['uid'][0] : $record['uid'];
+
if ($login_attr)
$this->data['user_login'] = is_array($record[$login_attr]) ? $record[$login_attr][0] : $record[$login_attr];
if ($alias_attr)
$this->data['user_alias'] = is_array($record[$alias_attr]) ? $record[$alias_attr][0] : $record[$alias_attr];
if ($name_attr)
$this->data['user_name'] = is_array($record[$name_attr]) ? $record[$name_attr][0] : $record[$name_attr];
if ($this->data['user_login'])
$args['user'] = $this->data['user_login'];
}
// Log "Login As" usage
if (!empty($origname)) {
write_log('userlogins', sprintf('Admin login for %s by %s from %s',
$args['user'], $origname, rcmail_remote_ip()));
}
return $args;
}
/**
* Sets SASL Proxy login/password for IMAP and Managesieve auth
*/
public function imap_connect($args)
{
if (!empty($_SESSION['kolab_auth_admin'])) {
$rcmail = rcmail::get_instance();
$admin_login = $rcmail->decrypt($_SESSION['kolab_auth_login']);
$admin_pass = $rcmail->decrypt($_SESSION['kolab_auth_password']);
$args['auth_cid'] = $admin_login;
$args['auth_pw'] = $admin_pass;
}
return $args;
}
/**
* Sets SASL Proxy login/password for SMTP auth
*/
public function smtp_connect($args)
{
if (!empty($_SESSION['kolab_auth_admin'])) {
$rcmail = rcmail::get_instance();
$admin_login = $rcmail->decrypt($_SESSION['kolab_auth_login']);
$admin_pass = $rcmail->decrypt($_SESSION['kolab_auth_password']);
$args['options']['smtp_auth_cid'] = $admin_login;
$args['options']['smtp_auth_pw'] = $admin_pass;
}
return $args;
}
/**
* Initializes LDAP object and connects to LDAP server
*/
private function init_ldap()
{
- if ($this->ldap)
+ if ($this->ldap) {
return $this->ldap->ready;
+ }
$rcmail = rcmail::get_instance();
$addressbook = $rcmail->config->get('kolab_auth_addressbook');
if (!is_array($addressbook)) {
$ldap_config = (array)$rcmail->config->get('ldap_public');
$addressbook = $ldap_config[$addressbook];
}
if (empty($addressbook)) {
return false;
}
$this->ldap = new kolab_auth_ldap_backend(
$addressbook,
$rcmail->config->get('ldap_debug'),
$rcmail->config->mail_domain($_SESSION['imap_host'])
);
return $this->ldap->ready;
}
/**
* Fetches user data from LDAP addressbook
*/
private function get_user_record($user, $host)
{
$rcmail = rcmail::get_instance();
$filter = $rcmail->config->get('kolab_auth_filter');
$filter = $this->parse_vars($filter, $user, $host);
// reset old result
$this->ldap->reset();
// get record
$this->ldap->set_filter($filter);
$results = $this->ldap->list_records();
- if (count($results->records) == 1)
+ if (count($results->records) == 1) {
return $results->records[0];
+ }
}
/**
* Prepares filter query for LDAP search
*/
private function parse_vars($str, $user, $host)
{
$rcmail = rcmail::get_instance();
$domain = $rcmail->config->get('username_domain');
if (!empty($domain) && strpos($user, '@') === false) {
if (is_array($domain) && isset($domain[$host]))
$user .= '@'.rcube_parse_host($domain[$host], $host);
else if (is_string($domain))
$user .= '@'.rcube_parse_host($domain, $host);
}
// replace variables in filter
list($u, $d) = explode('@', $user);
$dc = 'dc='.strtr($d, array('.' => ',dc=')); // hierarchal domain string
$replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $user, '%u' => $u);
return strtr($str, $replaces);
}
}
/**
* Wrapper class for rcube_ldap addressbook
*/
class kolab_auth_ldap_backend extends rcube_ldap
{
+ function __construct($p, $debug=false, $mail_domain=null)
+ {
+ parent::__construct($p, $debug, $mail_domain);
+ $this->fieldmap['uid'] = 'uid';
+ }
+
function set_filter($filter)
{
- if ($filter)
+ if ($filter) {
$this->prop['filter'] = $filter;
+ }
}
}
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_auth/package.xml b/plugins/kolab_auth/package.xml
index f333dd4b..937798d9 100644
--- a/plugins/kolab_auth/package.xml
+++ b/plugins/kolab_auth/package.xml
@@ -1,57 +1,59 @@
<?xml version="1.0" encoding="UTF-8"?>
<package xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" packagerversion="1.9.0" version="2.0" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
http://pear.php.net/dtd/tasks-1.0.xsd
http://pear.php.net/dtd/package-2.0
http://pear.php.net/dtd/package-2.0.xsd">
<name>kolab_auth</name>
- <uri>http://kolabsys.com</uri>
+ <uri>http://git.kolab.org/roundcube-plugins-kolab/</uri>
<summary>Kolab Authentication</summary>
<description>
Authenticates on LDAP server, finds canonized authentication ID for IMAP
and for new users creates identity based on LDAP information.
Supports impersonate feature (login as another user). To use this feature
imap_auth_type/smtp_auth_type must be set to DIGEST-MD5 or PLAIN.
</description>
<lead>
<name>Aleksander Machniak</name>
<user>machniak</user>
<email>machniak@kolabsys.com</email>
<active>yes</active>
</lead>
- <date>2011-11-01</date>
+ <date>2012-02-29</date>
<version>
- <release>0.1</release>
+ <release>0.2</release>
<api>0.1</api>
</version>
<stability>
<release>stable</release>
<api>stable</api>
</stability>
<license uri="http://www.gnu.org/licenses/agpl.html">GNU AGPLv3</license>
<notes>-</notes>
<contents>
<dir baseinstalldir="/" name="/">
<file name="kolab_auth.php" role="php">
<tasks:replace from="@name@" to="name" type="package-info"/>
<tasks:replace from="@package_version@" to="version" type="package-info"/>
</file>
<file name="config.inc.php.dist" role="data"></file>
<file name="LICENSE" role="data"></file>
+ <file name="localization/de_CH.inc" role="data"></file>
+ <file name="localization/de_DE.inc" role="data"></file>
<file name="localization/en_US.inc" role="data"></file>
<file name="localization/pl_PL.inc" role="data"></file>
</dir>
<!-- / -->
</contents>
<dependencies>
<required>
<php>
<min>5.2.1</min>
</php>
<pearinstaller>
<min>1.7.0</min>
</pearinstaller>
</required>
</dependencies>
<phprelease/>
</package>
diff --git a/plugins/kolab_config/package.xml b/plugins/kolab_config/package.xml
index 73291c8d..85c7faa4 100644
--- a/plugins/kolab_config/package.xml
+++ b/plugins/kolab_config/package.xml
@@ -1,57 +1,57 @@
<?xml version="1.0" encoding="UTF-8"?>
<package xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" packagerversion="1.9.0" version="2.0" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
http://pear.php.net/dtd/tasks-1.0.xsd
http://pear.php.net/dtd/package-2.0
http://pear.php.net/dtd/package-2.0.xsd">
<name>kolab_config</name>
- <uri>http://kolabsys.com</uri>
+ <uri>http://git.kolab.org/roundcube-plugins-kolab/</uri>
<summary>Kolab configuration storage</summary>
<description>
Plugin to use Kolab server as a configuration storage. Provides an API to handle
configuration according to http://wiki.kolab.org/KEP:9.
</description>
<lead>
<name>Aleksander Machniak</name>
<user>machniak</user>
<email>machniak@kolabsys.com</email>
<active>yes</active>
</lead>
<date>2011-11-01</date>
<version>
<release>1.0</release>
<api>1.0</api>
</version>
<stability>
<release>stable</release>
<api>stable</api>
</stability>
<license uri="http://www.gnu.org/licenses/agpl.html">GNU AGPLv3</license>
<notes>-</notes>
<contents>
<dir baseinstalldir="/" name="/">
<file name="kolab_config.php" role="php">
<tasks:replace from="@name@" to="name" type="package-info"/>
<tasks:replace from="@package_version@" to="version" type="package-info"/>
</file>
<file name="LICENSE" role="data"></file>
<file name="lib/configuration.php" role="php"></file>
<file name="lib/kolab_configuration.php" role="php"></file>
</dir>
<!-- / -->
</contents>
<dependencies>
<required>
<php>
<min>5.2.1</min>
</php>
<pearinstaller>
<min>1.7.0</min>
</pearinstaller>
<package>
<name>kolab_folders</name>
<uri>http://kolabsys.com</uri>
</package>
</required>
</dependencies>
<phprelease/>
</package>
diff --git a/plugins/kolab_core/package.xml b/plugins/kolab_core/package.xml
index 46aa29fe..fa407546 100644
--- a/plugins/kolab_core/package.xml
+++ b/plugins/kolab_core/package.xml
@@ -1,63 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<package xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" packagerversion="1.9.0" version="2.0" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
http://pear.php.net/dtd/tasks-1.0.xsd
http://pear.php.net/dtd/package-2.0
http://pear.php.net/dtd/package-2.0.xsd">
<name>kolab_core</name>
- <uri>http://kolabsys.com</uri>
+ <uri>http://git.kolab.org/roundcube-plugins-kolab/</uri>
<summary>Kolab API</summary>
<description>
Plugin to setup a basic environment for interaction with a Kolab server.
Other Kolab-related plugins will depend on it and can use the static API rcube_kolab.
</description>
<lead>
<name>Thomas Bruederli</name>
<user>bruederli</user>
<email>bruederli@kolabsys.com</email>
<active>yes</active>
</lead>
<lead>
<name>Aleksander Machniak</name>
<user>machniak</user>
<email>machniak@kolabsys.com</email>
<active>yes</active>
</lead>
<date>2011-11-01</date>
<version>
<release>1.0</release>
<api>1.0</api>
</version>
<stability>
<release>stable</release>
<api>stable</api>
</stability>
<license uri="http://www.gnu.org/licenses/agpl.html">GNU AGPLv3</license>
<notes>-</notes>
<contents>
<dir baseinstalldir="/" name="/">
<file name="kolab_core.php" role="php">
<tasks:replace from="@name@" to="name" type="package-info"/>
<tasks:replace from="@package_version@" to="version" type="package-info"/>
</file>
<file name="rcube_kolab.php" role="php">
<tasks:replace from="@name@" to="name" type="package-info"/>
<tasks:replace from="@package_version@" to="version" type="package-info"/>
</file>
<file name="config.inc.php.dist" role="data"></file>
<file name="README" role="data"></file>
<file name="LICENSE" role="data"></file>
</dir>
<!-- / -->
</contents>
<dependencies>
<required>
<php>
<min>5.2.1</min>
</php>
<pearinstaller>
<min>1.7.0</min>
</pearinstaller>
</required>
</dependencies>
<phprelease/>
</package>
diff --git a/plugins/kolab_core/rcube_kolab.php b/plugins/kolab_core/rcube_kolab.php
index 7f2702fc..6bd5f603 100644
--- a/plugins/kolab_core/rcube_kolab.php
+++ b/plugins/kolab_core/rcube_kolab.php
@@ -1,572 +1,567 @@
<?php
/**
* Utilities providing Kolab functionality using Kolab_* classes
* from the Horde project.
*
* @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/>.
*/
ini_set('error_reporting', E_ALL&~(E_DEPRECATED | E_NOTICE));
require_once 'Horde/Kolab/Storage/List.php';
require_once 'Horde/Kolab/Format.php';
require_once 'Horde/Auth.php';
require_once 'Horde/Auth/kolab.php';
require_once 'Horde/Perms.php';
/**
* Glue class to handle access to the Kolab data using the Kolab_* classes
* from the Horde project.
*/
class rcube_kolab
{
public static $last_error;
private static $horde_auth;
private static $config;
private static $ready = false;
private static $list;
private static $cache;
/**
* Setup the environment needed by the Kolab_* classes to access Kolab data
*/
public static function setup()
{
global $conf;
// setup already done
if (self::$horde_auth)
return;
$rcmail = rcmail::get_instance();
// get password of logged user
$pwd = $rcmail->decrypt($_SESSION['password']);
// load ldap credentials from local config
$conf['kolab'] = (array) $rcmail->config->get('kolab');
// Set global Horde config (e.g. Cache settings)
if (!empty($conf['kolab']['global'])) {
$conf = array_merge($conf, $conf['kolab']['global']);
unset($conf['kolab']['global']);
}
// Set Horde configuration (for cache db)
$dsnw = $rcmail->config->get('db_dsnw');
$dsnr = $rcmail->config->get('db_dsnr');
$conf['sql'] = MDB2::parseDSN($dsnw);
$conf['sql']['charset'] = 'utf-8';
if (!empty($dsnr) && $dsnr != $dsnw) {
$conf['sql']['read'] = MDB2::parseDSN($dsnr);
$conf['sql']['read']['charset'] = 'utf-8';
$conf['sql']['splitread'] = true;
}
// Re-set LDAP/IMAP host config
$ldap = array('server' => 'ldap://' . $_SESSION['imap_host'] . ':389');
$imap = array('server' => $_SESSION['imap_host'], 'port' => $_SESSION['imap_port']);
$freebusy = array('server' => 'https://' . $_SESSION['imap_host'] . '/freebusy');
$conf['kolab']['ldap'] = array_merge($ldap, (array)$conf['kolab']['ldap']);
$conf['kolab']['imap'] = array_merge($imap, (array)$conf['kolab']['imap']);
$conf['kolab']['freebusy'] = array_merge($freebusy, (array)$conf['kolab']['freebusy']);
self::$config = &$conf;
// pass the current IMAP authentication credentials to the Horde auth system
self::$horde_auth = Auth::singleton('kolab');
$username = $_SESSION['username'];
$credentials = array('password' => $pwd);
// Hack proxy auth for "Login As" feature of kolab_auth plugin
if (!empty($_SESSION['kolab_auth_admin'])) {
$username = $_SESSION['kolab_auth_admin'];
$conf['kolab']['imap']['user'] = $_SESSION['username'];
$conf['kolab']['imap']['authuser'] = $rcmail->decrypt($_SESSION['kolab_auth_login']);
$conf['kolab']['imap']['password'] = $rcmail->decrypt($_SESSION['kolab_auth_password']);
$conf['kolab']['user_mail'] = $_SESSION['username'];
}
if (self::$horde_auth->isAuthenticated()) {
self::$ready = true;
}
else if (self::$horde_auth->authenticate($username, $credentials, false)) {
// we could use Auth::setAuth() here, but it requires the whole bunch
// of includes and global objects, do it as simple as possible
$_SESSION['__auth'] = array(
'authenticated' => true,
'userId' => $_SESSION['username'],
'timestamp' => time(),
'remote_addr' => $_SERVER['REMOTE_ADDR'],
);
Auth::setCredential('password', $pwd);
self::$ready = true;
}
else {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => sprintf("Unable to authenticate user %s!", $_SESSION['username'])),
true, true);
}
// Register shutdown function for saving cache/session objects
$rcmail->add_shutdown_function(array('rcube_kolab', 'shutdown'));
NLS::setCharset('UTF-8');
String::setDefaultCharset('UTF-8');
}
/**
* Get instance of Kolab_List object
*
* @return object Kolab_List Folders list object
*/
public static function get_folders_list()
{
self::setup();
if (self::$list)
return self::$list;
if (!self::$ready)
return null;
$rcmail = rcmail::get_instance();
$imap_cache = $rcmail->config->get('imap_cache');
if ($imap_cache) {
self::$cache = $rcmail->get_cache('IMAP', $imap_cache);
self::$list = self::$cache->get('mailboxes.kolab');
// Disable Horde folders caching, we're using our own cache
self::$config['kolab']['imap']['cache_folders'] = false;
}
if (empty(self::$list)) {
self::$list = Kolab_List::singleton();
}
return self::$list;
}
/**
* Get instance of a Kolab (XML) format object
*
* @param string Data type (contact,event,task,note)
*
* @return object Horde_Kolab_Format_XML The format object
*/
public static function get_format($type)
{
self::setup();
return Horde_Kolab_Format::factory('XML', $type);
}
/**
* Get a list of storage folders for the given data type
*
* @param string Data type to list folders for (contact,event,task,note)
*
* @return array List of Kolab_Folder objects (folder names in UTF7-IMAP)
*/
public static function get_folders($type)
{
$kolab = self::get_folders_list();
return self::$ready ? $kolab->getByType($type) : array();
}
/**
* Getter for a specific storage folder
*
* @param string IMAP folder to access (UTF7-IMAP)
* @return object Kolab_Folder The folder object
*/
public static function get_folder($folder)
{
$kolab = self::get_folders_list();
return self::$ready ? $kolab->getFolder($folder) : null;
}
/**
* Checks if the given folder is subscribed.
* Much nicer would be if the Kolab_Folder object could tell this information
*
* @param string Full IMAP folder name
* @return boolean True if in the list of subscribed folders, False if not
*/
public static function is_subscribed($folder)
{
static $subscribed; // local cache
if (!$subscribed) {
- $rcmail = rcmail::get_instance();
- // try without connection first (list could be served from cache)
- $subscribed = $rcmail->imap ? $rcmail->imap->list_mailboxes() : array();
-
- // now really get the list from the IMAP server
- if (empty($subscribed) || $subscribed == array('INBOX')) {
- $rcmail->imap_connect();
- $subscribed = $rcmail->imap->list_mailboxes();
- }
+ $rcmail = rcmail::get_instance();
+ $storage = $rcmail->get_storage();
+ $subscribed = $storage->list_folders();
}
return in_array($folder, $subscribed);
}
/**
* Get storage object for read/write access to the Kolab backend
*
* @param string IMAP folder to access (UTF7-IMAP)
* @param string Object type to deal with (leave empty for auto-detection using annotations)
*
* @return object Kolab_Data The data storage object
*/
public static function get_storage($folder, $data_type = null)
{
$kolab = self::get_folders_list();
return self::$ready ? $kolab->getFolder($folder)->getData($data_type) : null;
}
/**
* Compose an URL to query the free/busy status for the given user
*/
public static function get_freebusy_url($email)
{
return unslashify(self::$config['kolab']['freebusy']['server']) . '/' . $email . '.ifb';
}
/**
* Do session/cache operations on shutdown
*/
public static function shutdown()
{
if (self::$ready) {
// Horde's shutdown function doesn't work with Roundcube session
// save Horde_Kolab_Session object in session here
require_once 'Horde/SessionObjects.php';
$session = Horde_SessionObjects::singleton();
$kolab = Horde_Kolab_Session::singleton();
$session->overwrite('kolab_session', $kolab, false);
// Write Kolab_List object to cache
if (self::$cache && self::$list) {
self::$cache->set('mailboxes.kolab', self::$list);
}
}
}
/**
* Creates folder ID from folder name
*
* @param string $folder Folder name (UTF7-IMAP)
*
* @return string Folder ID string
*/
public static function folder_id($folder)
{
return asciiwords(strtr($folder, '/.-', '___'));
}
/**
* Deletes IMAP folder
*
* @param string $name Folder name (UTF7-IMAP)
*
* @return bool True on success, false on failure
*/
public static function folder_delete($name)
{
$kolab = self::get_folders_list();
$folder = $kolab->getFolder($name);
$result = $folder->delete();
if (is_object($result) && is_a($result, 'PEAR_Error')) {
self::$last_error = $result->getMessage();
return false;
}
return true;
}
/**
* Creates IMAP folder
*
* @param string $name Folder name (UTF7-IMAP)
* @param string $type Folder type
* @param bool $default True if older is default (for specified type)
*
* @return bool True on success, false on failure
*/
public static function folder_create($name, $type=null, $default=false)
{
$kolab = self::get_folders_list();
$folder = new Kolab_Folder();
$folder->setList($kolab);
$folder->setFolder($name);
$result = $folder->save(array(
'type' => $type,
'default' => $default,
));
if (is_object($result) && is_a($result, 'PEAR_Error')) {
self::$last_error = $result->getMessage();
return false;
}
return true;
}
/**
* Renames IMAP folder
*
* @param string $oldname Old folder name (UTF7-IMAP)
* @param string $newname New folder name (UTF7-IMAP)
*
* @return bool True on success, false on failure
*/
public static function folder_rename($oldname, $newname)
{
$kolab = self::get_folders_list();
$folder = $kolab->getFolder($oldname);
$folder->setFolder($newname);
$result = $kolab->rename($folder);
if (is_object($result) && is_a($result, 'PEAR_Error')) {
self::$last_error = $result->getMessage();
return false;
}
// @TODO: Horde doesn't update subfolders cache nor subscriptions
// but we cannot use Roundcube imap object here, because
// when two connections are used in one request and we have
// multi-server configuration, updating the cache after all
// would get wrong information (e.g. annotations)
// Reset the List object and cache
$kolab = null;
if (self::$cache) {
self::$list = null;
self::$cache->remove('mailboxes', true);
}
return true;
}
/**
* Getter for human-readable name of Kolab object (folder)
* See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference
*
* @param string $folder IMAP folder name (UTF7-IMAP)
* @param string $folder_ns Will be set to namespace name of the folder
*
* @return string Name of the folder-object
*/
public static function object_name($folder, &$folder_ns=null)
{
- $rcmail = rcmail::get_instance();
- $rcmail->imap_init();
-
- $namespace = $rcmail->imap->get_namespace();
+ $rcmail = rcmail::get_instance();
+ $storage = $rcmail->get_storage();
+ $namespace = $storage->get_namespace();
$found = false;
if (!empty($namespace['shared'])) {
foreach ($namespace['shared'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
$prefix = '';
$folder = substr($folder, strlen($ns[0]));
$delim = $ns[1];
$found = true;
$folder_ns = 'shared';
break;
}
}
}
if (!$found && !empty($namespace['other'])) {
foreach ($namespace['other'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
// remove namespace prefix
$folder = substr($folder, strlen($ns[0]));
$delim = $ns[1];
// get username
$pos = strpos($folder, $delim);
if ($pos) {
$prefix = '('.substr($folder, 0, $pos).') ';
$folder = substr($folder, $pos+1);
}
else {
$prefix = '('.$folder.')';
$folder = '';
}
$found = true;
$folder_ns = 'other';
break;
}
}
}
if (!$found && !empty($namespace['personal'])) {
foreach ($namespace['personal'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
// remove namespace prefix
$folder = substr($folder, strlen($ns[0]));
$prefix = '';
$delim = $ns[1];
$found = true;
break;
}
}
}
if (empty($delim))
- $delim = $rcmail->imap->get_hierarchy_delimiter();
+ $delim = $storage->get_hierarchy_delimiter();
- $folder = rcube_charset_convert($folder, 'UTF7-IMAP');
+ $folder = rcube_charset::convert($folder, 'UTF7-IMAP');
$folder = str_replace($delim, ' &raquo; ', $folder);
if ($prefix)
$folder = $prefix . ' ' . $folder;
if (!$folder_ns)
$folder_ns = 'personal';
return $folder;
}
/**
* Getter for the name of the namespace to which the IMAP folder belongs
*
* @param string $folder IMAP folder name (UTF7-IMAP)
*
* @return string Name of the namespace (personal, other, shared)
*/
public static function folder_namespace($folder)
{
- $rcmail = rcmail::get_instance();
- $rcmail->imap_init();
-
- $namespace = $rcmail->imap->get_namespace();
+ $rcmail = rcmail::get_instance();
+ $storage = $rcmail->get_storage();
+ $namespace = $storage->get_namespace();
if (!empty($namespace)) {
foreach ($namespace as $nsname => $nsvalue) {
if (in_array($nsname, array('personal', 'other', 'shared')) && !empty($nsvalue)) {
foreach ($nsvalue as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
return $namespace = $nsname;
}
}
}
}
}
return 'personal';
}
/**
* Creates a SELECT field with folders list
*
* @param string $type Folder type
* @param array $attrs SELECT field attributes (e.g. name)
* @param string $current The name of current folder (to skip it)
*
* @return html_select SELECT object
*/
public static function folder_selector($type, $attrs, $current = '')
{
+ $rcmail = rcmail::get_instance();
+ $storage = $rcmail->get_storage();
+
// get all folders of specified type
$folders = self::get_folders($type);
- $delim = $_SESSION['imap_delimiter'];
+ $delim = $storage->get_hierarchy_delimiter();
$names = array();
$len = strlen($current);
if ($len && ($rpos = strrpos($current, $delim))) {
$parent = substr($current, 0, $rpos);
$p_len = strlen($parent);
}
// Filter folders list
foreach ($folders as $c_folder) {
$name = $c_folder->name;
// skip current folder and it's subfolders
if ($len && ($name == $current || strpos($name, $current.$delim) === 0)) {
continue;
}
// always show the parent of current folder
if ($p_len && $name == $parent) { }
// skip folders where user have no rights to create subfolders
else if ($c_folder->getOwner() != $_SESSION['username']) {
$rights = $c_folder->getMyRights();
if (PEAR::IsError($rights) || !preg_match('/[ck]/', $rights)) {
continue;
}
}
- $names[$name] = rcube_charset_convert($name, 'UTF7-IMAP');
+ $names[$name] = rcube_charset::convert($name, 'UTF7-IMAP');
}
// Make sure parent folder is listed (might be skipped e.g. if it's namespace root)
if ($p_len && !isset($names[$parent])) {
- $names[$parent] = rcube_charset_convert($parent, 'UTF7-IMAP');
+ $names[$parent] = rcube_charset::convert($parent, 'UTF7-IMAP');
}
// Sort folders list
asort($names, SORT_LOCALE_STRING);
$folders = array_keys($names);
$names = array();
// Build SELECT field of parent folder
$select = new html_select($attrs);
$select->add('---', '');
foreach ($folders as $name) {
$imap_name = $name;
$name = $origname = self::object_name($name);
// find folder prefix to truncate
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;
$select->add($name, $imap_name);
}
return $select;
}
}
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/kolab_folders.php b/plugins/kolab_folders/kolab_folders.php
index 446a091d..e1b2e634 100644
--- a/plugins/kolab_folders/kolab_folders.php
+++ b/plugins/kolab_folders/kolab_folders.php
@@ -1,643 +1,668 @@
<?php
/**
* Type-aware folder management/listing for Kolab
*
* @version @package_version@
* @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/>.
*/
class kolab_folders extends rcube_plugin
{
public $task = '?(?!login).*';
public $types = array('mail', 'event', 'journal', 'task', 'note', 'contact', 'configuration');
public $mail_types = array('inbox', 'drafts', 'sentitems', 'outbox', 'wastebasket', 'junkemail');
private $rc;
const CTYPE_KEY = '/shared/vendor/kolab/folder-type';
/**
* Plugin initialization.
*/
function init()
{
$this->rc = rcmail::get_instance();
// Folder listing hooks
- $this->add_hook('mailboxes_list', array($this, 'mailboxes_list'));
+ $this->add_hook('storage_folders', array($this, 'mailboxes_list'));
// Folder manager hooks
$this->add_hook('folder_form', array($this, 'folder_form'));
$this->add_hook('folder_update', array($this, 'folder_save'));
$this->add_hook('folder_create', array($this, 'folder_save'));
$this->add_hook('folder_delete', array($this, 'folder_save'));
$this->add_hook('folder_rename', array($this, 'folder_save'));
$this->add_hook('folders_list', array($this, 'folders_list'));
}
/**
* Handler for mailboxes_list hook. Enables type-aware lists filtering.
*/
function mailboxes_list($args)
{
if (!$this->metadata_support()) {
return $args;
}
$filter = $args['filter'];
// all-folders request, use core method
if (!$filter) {
return $args;
}
// get folders types
$folderdata = $this->get_folder_type_list($args['root'].$args['name'], true);
if (!is_array($folderdata)) {
- $args['folders'] = false;
return $args;
}
$regexp = '/^' . preg_quote($filter, '/') . '(\..+)?$/';
// In some conditions we can skip LIST command (?)
if ($args['mode'] == 'LIST' && $filter != 'mail'
&& $args['root'] == '' && $args['name'] == '*'
) {
foreach ($folderdata as $folder => $type) {
if (!preg_match($regexp, $type)) {
unset($folderdata[$folder]);
}
}
$args['folders'] = array_keys($folderdata);
return $args;
}
+ $storage = $this->rc->get_storage();
+
// Get folders list
if ($args['mode'] == 'LIST') {
- $args['folders'] = $this->rc->imap->conn->listMailboxes($args['root'], $args['name']);
+ if (!$storage->check_connection()) {
+ return $args;
+ }
+ $args['folders'] = $storage->conn->listMailboxes($args['root'], $args['name']);
}
else {
$args['folders'] = $this->list_subscribed($args['root'], $args['name']);
}
// In case of an error, return empty list
if (!is_array($args['folders'])) {
$args['folders'] = array();
return $args;
}
// Filter folders list
foreach ($args['folders'] as $idx => $folder) {
$type = $folderdata[$folder];
if ($filter == 'mail' && empty($type)) {
continue;
}
if (empty($type) || !preg_match($regexp, $type)) {
unset($args['folders'][$idx]);
}
}
return $args;
}
/**
* Handler for folders_list hook. Add css classes to folder rows.
*/
function folders_list($args)
{
if (!$this->metadata_support()) {
return $args;
}
$table = $args['table'];
// get folders types
$folderdata = $this->get_folder_type_list('*');
if (!is_array($folderdata)) {
return $args;
}
// Add type-based style for table rows
// See kolab_folders::folder_class_name()
for ($i=1, $cnt=$table->size(); $i<=$cnt; $i++) {
$attrib = $table->get_row_attribs($i);
$folder = $attrib['foldername']; // UTF7-IMAP
$type = $folderdata[$folder];
if (!$type)
$type = 'mail';
$class_name = self::folder_class_name($type);
$attrib['class'] = trim($attrib['class'] . ' ' . $class_name);
$table->set_row_attribs($attrib, $i);
}
return $args;
}
/**
* Handler for folder info/edit form (folder_form hook).
* Adds folder type selector.
*/
function folder_form($args)
{
if (!$this->metadata_support()) {
return $args;
}
// load translations
$this->add_texts('localization/', false);
// INBOX folder is of type mail.inbox and this cannot be changed
if ($args['name'] == 'INBOX') {
$args['form']['props']['fieldsets']['settings']['content']['foldertype'] = array(
'label' => $this->gettext('folderctype'),
'value' => sprintf('%s (%s)', $this->gettext('foldertypemail'), $this->gettext('inbox')),
);
return $args;
}
if ($args['options']['is_root']) {
return $args;
}
$mbox = strlen($args['name']) ? $args['name'] : $args['parent_name'];
if (isset($_POST['_ctype'])) {
$new_ctype = trim(get_input_value('_ctype', RCUBE_INPUT_POST));
$new_subtype = trim(get_input_value('_subtype', RCUBE_INPUT_POST));
}
// Get type of the folder or the parent
if (strlen($mbox)) {
list($ctype, $subtype) = $this->get_folder_type($mbox);
if (strlen($args['parent_name']) && $subtype == 'default')
$subtype = ''; // there can be only one
}
if (!$ctype) {
$ctype = 'mail';
}
+ $storage = $this->rc->get_storage();
+
// Don't allow changing type of shared folder, according to ACL
if (strlen($mbox)) {
- $options = $this->rc->imap->mailbox_info($mbox);
+ $options = $storage->folder_info($mbox);
if ($options['namespace'] != 'personal' && !in_array('a', $options['rights'])) {
- if (in_array($ctype, $this->types))
+ if (in_array($ctype, $this->types)) {
$value = $this->gettext('foldertype'.$ctype);
- else
+ }
+ else {
$value = $ctype;
- if ($subtype)
+ }
+ if ($subtype) {
$value .= ' ('. ($subtype == 'default' ? $this->gettext('default') : $subtype) .')';
+ }
$args['form']['props']['fieldsets']['settings']['content']['foldertype'] = array(
'label' => $this->gettext('folderctype'),
'value' => $value,
);
return $args;
}
}
// Add javascript script to the client
$this->include_script('kolab_folders.js');
// build type SELECT fields
$type_select = new html_select(array('name' => '_ctype', 'id' => '_ctype'));
$sub_select = new html_select(array('name' => '_subtype', 'id' => '_subtype'));
foreach ($this->types as $type) {
$type_select->add($this->gettext('foldertype'.$type), $type);
}
// add non-supported type
if (!in_array($ctype, $this->types)) {
$type_select->add($ctype, $ctype);
}
$sub_select->add('', '');
$sub_select->add($this->gettext('default'), 'default');
foreach ($this->mail_types as $type) {
$sub_select->add($this->gettext($type), $type);
}
$args['form']['props']['fieldsets']['settings']['content']['foldertype'] = array(
'label' => $this->gettext('folderctype'),
'value' => $type_select->show(isset($new_ctype) ? $new_ctype : $ctype)
. $sub_select->show(isset($new_subtype) ? $new_subtype : $subtype),
);
return $args;
}
/**
* Handler for folder update/create action (folder_update/folder_create hook).
*/
function folder_save($args)
{
// Folder actions from folders list
if (empty($args['record'])) {
// Just clear Horde folders cache and return
$this->clear_folders_cache();
return $args;
}
// Folder create/update with form
$ctype = trim(get_input_value('_ctype', RCUBE_INPUT_POST));
$subtype = trim(get_input_value('_subtype', RCUBE_INPUT_POST));
$mbox = $args['record']['name'];
$old_mbox = $args['record']['oldname'];
$subscribe = $args['record']['subscribe'];
if (empty($ctype)) {
return $args;
}
// load translations
$this->add_texts('localization/', false);
// Skip folder creation/rename in core
// @TODO: Maybe we should provide folder_create_after and folder_update_after hooks?
// Using create_mailbox/rename_mailbox here looks bad
$args['abort'] = true;
// There can be only one default folder of specified type
if ($subtype == 'default') {
$default = $this->get_default_folder($ctype);
if ($default !== null && $old_mbox != $default) {
$args['result'] = false;
$args['message'] = $this->gettext('defaultfolderexists');
return $args;
}
}
// Subtype sanity-checks
else if ($subtype && ($ctype != 'mail' || !in_array($subtype, $this->mail_types))) {
$subtype = '';
}
$ctype .= $subtype ? '.'.$subtype : '';
+ $storage = $this->rc->get_storage();
+
// Create folder
if (!strlen($old_mbox)) {
// By default don't subscribe to non-mail folders
if ($subscribe)
$subscribe = (bool) preg_match('/^mail/', $ctype);
- $result = $this->rc->imap->create_mailbox($mbox, $subscribe);
+ $result = $storage->create_folder($mbox, $subscribe);
// Set folder type
if ($result) {
$this->set_folder_type($mbox, $ctype);
}
}
// Rename folder
else {
if ($old_mbox != $mbox) {
- $result = $this->rc->imap->rename_mailbox($old_mbox, $mbox);
+ $result = $storage->rename_folder($old_mbox, $mbox);
}
else {
$result = true;
}
if ($result) {
list($oldtype, $oldsubtype) = $this->get_folder_type($mbox);
$oldtype .= $oldsubtype ? '.'.$oldsubtype : '';
if ($ctype != $oldtype) {
$this->set_folder_type($mbox, $ctype);
}
}
}
// Clear Horde folders cache
if ($result) {
$this->clear_folders_cache();
}
$args['record']['class'] = self::folder_class_name($ctype);
$args['record']['subscribe'] = $subscribe;
$args['result'] = $result;
return $args;
}
/**
* Checks if IMAP server supports any of METADATA, ANNOTATEMORE, ANNOTATEMORE2
*
* @return boolean
*/
function metadata_support()
{
- return $this->rc->imap->get_capability('METADATA') ||
- $this->rc->imap->get_capability('ANNOTATEMORE') ||
- $this->rc->imap->get_capability('ANNOTATEMORE2');
+ $storage = $this->rc->get_storage();
+
+ return $storage->get_capability('METADATA') ||
+ $storage->get_capability('ANNOTATEMORE') ||
+ $storage->get_capability('ANNOTATEMORE2');
}
/**
* Checks if IMAP server supports any of METADATA, ANNOTATEMORE, ANNOTATEMORE2
*
* @param string $folder Folder name
*
* @return array Folder content-type
*/
function get_folder_type($folder)
{
- $folderdata = $this->rc->imap->get_metadata($folder, array(kolab_folders::CTYPE_KEY));
+ $storage = $this->rc->get_storage();
+ $folderdata = $storage->get_metadata($folder, array(kolab_folders::CTYPE_KEY));
return explode('.', $folderdata[$folder][kolab_folders::CTYPE_KEY]);
}
/**
* Sets folder content-type.
*
* @param string $folder Folder name
* @param string $type Content type
*
* @return boolean True on success
*/
function set_folder_type($folder, $type='mail')
{
- return $this->rc->imap->set_metadata($folder, array(kolab_folders::CTYPE_KEY => $type));
+ $storage = $this->rc->get_storage();
+
+ return $storage->set_metadata($folder, array(kolab_folders::CTYPE_KEY => $type));
}
/**
* Returns list of subscribed folders (directly from IMAP server)
*
* @param string $root Optional root folder
* @param string $name Optional name pattern
*
* @return array List of mailboxes/folders
*/
private function list_subscribed($root='', $name='*')
{
- $imap = $this->rc->imap;
+ $storage = $this->rc->get_storage();
+
+ if (!$storage->check_connection()) {
+ return null;
+ }
// Code copied from rcube_imap::_list_mailboxes()
// Server supports LIST-EXTENDED, we can use selection options
// #1486225: Some dovecot versions returns wrong result using LIST-EXTENDED
if (!$this->rc->config->get('imap_force_lsub') && $imap->get_capability('LIST-EXTENDED')) {
// This will also set mailbox options, LSUB doesn't do that
- $a_folders = $imap->conn->listMailboxes($root, $name,
+ $a_folders = $storage->conn->listMailboxes($root, $name,
NULL, array('SUBSCRIBED'));
// remove non-existent folders
- if (is_array($a_folders) && $name = '*') {
+ if (is_array($a_folders) && $name = '*' && !empty($storage->conn->data['LIST'])) {
foreach ($a_folders as $idx => $folder) {
- if ($imap->conn->data['LIST'] && ($opts = $imap->conn->data['LIST'][$folder])
+ if (($opts = $storage->conn->data['LIST'][$folder])
&& in_array('\\NonExistent', $opts)
) {
- $imap->conn->unsubscribe($folder);
+ $storage->conn->unsubscribe($folder);
unset($a_folders[$idx]);
}
}
}
}
// retrieve list of folders from IMAP server using LSUB
else {
- $a_folders = $imap->conn->listSubscribed($root, $name);
+ $a_folders = $storage->conn->listSubscribed($root, $name);
// unsubscribe non-existent folders, remove from the list
- if (is_array($a_folders) && $name == '*') {
+ if (is_array($a_folders) && $name == '*' && !empty($storage->conn->data['LIST'])) {
foreach ($a_folders as $idx => $folder) {
- if ($imap->conn->data['LIST'] && ($opts = $imap->conn->data['LIST'][$folder])
- && in_array('\\Noselect', $opts)
+ if (!isset($storage->conn->data['LIST'][$folder])
+ || in_array('\\Noselect', $storage->conn->data['LIST'][$folder])
) {
// Some servers returns \Noselect for existing folders
- if (!$imap->mailbox_exists($folder)) {
- $imap->conn->unsubscribe($folder);
+ if (!$storage->folder_exists($folder)) {
+ $storage->conn->unsubscribe($folder);
unset($a_folders[$idx]);
}
}
}
}
}
return $a_folders;
}
/**
* Returns list of folder(s) type(s)
*
* @param string $mbox Folder name or pattern
* @param bool $defaults Enables creation of configured default folders
*
* @return array List of folders data, indexed by folder name
*/
function get_folder_type_list($mbox, $create_defaults = false)
{
+ $storage = $this->rc->get_storage();
+
// Use mailboxes. prefix so the cache will be cleared by core
// together with other mailboxes-related cache data
$cache_key = 'mailboxes.folder-type.'.$mbox;
// get cached metadata
- $metadata = $this->rc->imap->get_cache($cache_key);
+ $metadata = $storage->get_cache($cache_key);
if (!is_array($metadata)) {
- $metadata = $this->rc->imap->get_metadata($mbox, kolab_folders::CTYPE_KEY);
+ $metadata = $storage->get_metadata($mbox, kolab_folders::CTYPE_KEY);
$need_update = true;
}
if (!is_array($metadata)) {
return false;
}
// make the result more flat
if ($need_update) {
$metadata = array_map('implode', $metadata);
}
// create default folders if needed
if ($create_defaults) {
$this->create_default_folders($metadata, $cache_key);
}
// write mailboxlist to cache
if ($need_update) {
- $this->rc->imap->update_cache($cache_key, $metadata);
+ $storage->update_cache($cache_key, $metadata);
}
return $metadata;
}
/**
* Returns the name of default folder
*
* @param string $type Folder type
*
* @return string Folder name
*/
function get_default_folder($type)
{
+ $storage = $this->rc->get_storage();
$folderdata = $this->get_folder_type_list('*');
if (!is_array($folderdata)) {
return null;
}
$type .= '.default';
- $namespace = $this->rc->imap->get_namespace();
+ $namespace = $storage->get_namespace();
// get all folders of specified type
$folderdata = array_intersect($folderdata, array($type));
unset($folders[0]);
foreach ($folderdata as $folder => $data) {
// check if folder is in personal namespace
foreach (array('shared', 'other') as $nskey) {
if (!empty($namespace[$nskey])) {
foreach ($namespace[$nskey] as $ns) {
if ($ns[0] && substr($folder, 0, strlen($ns[0])) == $ns[0]) {
continue 3;
}
}
}
}
// There can be only one default folder of specified type
return $folder;
}
return null;
}
/**
* Returns CSS class name for specified folder type
*
* @param string $type Folder type
*
* @return string Class name
*/
static function folder_class_name($type)
{
list($ctype, $subtype) = explode('.', $type);
$class[] = 'type-' . ($ctype ? $ctype : 'mail');
if ($subtype)
$class[] = 'subtype-' . $subtype;
return implode(' ', $class);
}
/**
* Clear Horde's folder cache. See Kolab_List::singleton().
*/
private function clear_folders_cache()
{
unset($_SESSION['horde_session_objects']['kolab_folderlist']);
}
/**
* Creates default folders if they doesn't exist
*/
private function create_default_folders(&$folderdata, $cache_key = null)
{
- $namespace = $this->rc->imap->get_namespace();
+ $storage = $this->rc->get_storage();
+ $namespace = $storage->get_namespace();
$defaults = array();
$need_update = false;
// Find personal namespace prefix
if (is_array($namespace['personal']) && count($namespace['personal']) == 1) {
$prefix = $namespace['personal'][0][0];
}
else {
$prefix = '';
}
$this->load_config();
// get configured defaults
foreach ($this->types as $type) {
$subtypes = $type == 'mail' ? $this->mail_types : array('default');
foreach ($subtypes as $subtype) {
$opt_name = 'kolab_folders_' . $type . '_' . $subtype;
if ($folder = $this->rc->config->get($opt_name)) {
// convert configuration value to UTF7-IMAP charset
- $folder = rcube_charset_convert($folder, RCMAIL_CHARSET, 'UTF7-IMAP');
+ $folder = rcube_charset::convert($folder, RCMAIL_CHARSET, 'UTF7-IMAP');
// and namespace prefix if needed
if ($prefix && strpos($folder, $prefix) === false && $folder != 'INBOX') {
$folder = $prefix . $folder;
}
$defaults[$type . '.' . $subtype] = $folder;
}
}
}
// find default folders
foreach ($defaults as $type => $foldername) {
// folder exists, do nothing
if (!empty($folderdata[$foldername])) {
continue;
}
// special case, need to set type only
if ($foldername == 'INBOX' || $type == 'mail.inbox') {
$this->set_folder_type($foldername, 'mail.inbox');
continue;
}
// get all folders of specified type
$folders = array_intersect($folderdata, array($type));
unset($folders[0]);
// find folders in personal namespace
foreach ($folders as $folder) {
if ($folder) {
foreach (array('shared', 'other') as $nskey) {
if (!empty($namespace[$nskey])) {
foreach ($namespace[$nskey] as $ns) {
if ($ns[0] && substr($folder, 0, strlen($ns[0])) == $ns[0]) {
continue 3;
}
}
}
}
}
// got folder in personal namespace
continue 2;
}
list($type1, $type2) = explode('.', $type);
// create folder
- if ($type1 != 'mail' || !$this->rc->imap->mailbox_exists($foldername)) {
- $this->rc->imap->create_mailbox($foldername, $type1 == 'mail');
+ if ($type1 != 'mail' || !$storage->folder_exists($foldername)) {
+ $storage->create_folder($foldername, $type1 == 'mail');
}
// set type
$result = $this->set_folder_type($foldername, $type);
// add new folder to the result
if ($result) {
$folderdata[$foldername] = $type;
$need_update = true;
}
}
// update cache
if ($need_update && $cache_key) {
- $this->rc->imap->update_cache($cache_key, $folderdata);
+ $storage->update_cache($cache_key, $folderdata);
}
}
+
}
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_folders/package.xml b/plugins/kolab_folders/package.xml
index 54ff20b5..b1c364a7 100644
--- a/plugins/kolab_folders/package.xml
+++ b/plugins/kolab_folders/package.xml
@@ -1,63 +1,63 @@
<?xml version="1.0" encoding="UTF-8"?>
<package xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" packagerversion="1.9.0" version="2.0" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
http://pear.php.net/dtd/tasks-1.0.xsd
http://pear.php.net/dtd/package-2.0
http://pear.php.net/dtd/package-2.0.xsd">
<name>kolab_folders</name>
- <uri>http://kolabsys.com</uri>
+ <uri>http://git.kolab.org/roundcube-plugins-kolab/</uri>
<summary>Type-aware folder management/listing for Kolab</summary>
<description>
The plugin extends folders handling with features of the Kolab Suite
according to specified format (http://www.kolab.org/doc/kolabformat-2.0-html).
With this plugin enabled it is possible to:
- set/get/change folder's type,
- filter folders list by folder type,
- style folders list rows (in folder manager),
- create default folders with specified type.
</description>
<lead>
<name>Aleksander Machniak</name>
<user>machniak</user>
<email>machniak@kolabsys.com</email>
<active>yes</active>
</lead>
<date>2011-11-01</date>
<version>
<release>1.0</release>
<api>1.0</api>
</version>
<stability>
<release>stable</release>
<api>stable</api>
</stability>
<license uri="http://www.gnu.org/licenses/agpl.html">GNU AGPLv3</license>
<notes>-</notes>
<contents>
<dir baseinstalldir="/" name="/">
<file name="kolab_folders.php" role="php">
<tasks:replace from="@name@" to="name" type="package-info"/>
<tasks:replace from="@package_version@" to="version" type="package-info"/>
</file>
<file name="kolab_folders.js" role="data">
<tasks:replace from="@name@" to="name" type="package-info"/>
<tasks:replace from="@package_version@" to="version" type="package-info"/>
</file>
<file name="config.inc.php.dist" role="data"></file>
<file name="localization/en_US.inc" role="data"></file>
<file name="localization/pl_PL.inc" role="data"></file>
<file name="LICENSE" role="data"></file>
</dir>
<!-- / -->
</contents>
<dependencies>
<required>
<php>
<min>5.2.1</min>
</php>
<pearinstaller>
<min>1.7.0</min>
</pearinstaller>
</required>
</dependencies>
<phprelease />
</package>
diff --git a/plugins/kolab_zpush/kolab_zpush.php b/plugins/kolab_zpush/kolab_zpush.php
index c7469f1d..f19fe3ad 100644
--- a/plugins/kolab_zpush/kolab_zpush.php
+++ b/plugins/kolab_zpush/kolab_zpush.php
@@ -1,344 +1,346 @@
<?php
/**
* Z-Push configuration utility for Kolab accounts
*
* @version 0.2
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
- * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
+ * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_zpush extends rcube_plugin
{
public $task = 'settings';
public $urlbase;
private $rc;
private $ui;
private $cache;
private $devices;
private $folders;
private $folders_meta;
private $root_meta;
const ROOT_MAILBOX = 'INBOX';
const CTYPE_KEY = '/shared/vendor/kolab/folder-type';
const ACTIVESYNC_KEY = '/private/vendor/kolab/activesync';
/**
* Plugin initialization.
*/
public function init()
{
$this->rc = rcmail::get_instance();
$this->require_plugin('jqueryui');
$this->add_texts('localization/', true);
$this->include_script('kolab_zpush.js');
$this->register_action('plugin.zpushconfig', array($this, 'config_view'));
$this->register_action('plugin.zpushjson', array($this, 'json_command'));
if ($this->rc->action == 'plugin.zpushconfig')
$this->require_plugin('kolab_core');
}
/**
* Establish IMAP connection
*/
public function init_imap()
{
- $this->rc->imap_connect();
+ $storage = $this->rc->get_storage();
+
$this->cache = $this->rc->get_cache('zpush', 'db', 900);
$this->cache->expunge();
- if ($meta = $this->rc->imap->get_metadata(self::ROOT_MAILBOX, self::ACTIVESYNC_KEY)) {
+ if ($meta = $storage->get_metadata(self::ROOT_MAILBOX, self::ACTIVESYNC_KEY)) {
// clear cache if device config changed
if (($oldmeta = $this->cache->read('devicemeta')) && $oldmeta != $meta)
$this->cache->remove();
$this->root_meta = $this->unserialize_metadata($meta[self::ROOT_MAILBOX][self::ACTIVESYNC_KEY]);
$this->cache->remove('devicemeta');
$this->cache->write('devicemeta', $meta);
}
}
/**
* Handle JSON requests
*/
public function json_command()
{
- $cmd = get_input_value('cmd', RCUBE_INPUT_GPC);
- $imei = get_input_value('id', RCUBE_INPUT_GPC);
+ $storage = $this->rc->get_storage();
+ $cmd = get_input_value('cmd', RCUBE_INPUT_GPC);
+ $imei = get_input_value('id', RCUBE_INPUT_GPC);
switch ($cmd) {
case 'load':
$result = array();
- $this->init_imap();
$devices = $this->list_devices();
if ($device = $devices[$imei]) {
$result['id'] = $imei;
$result['devicealias'] = $device['ALIAS'];
$result['syncmode'] = intval($device['MODE']);
$result['laxpic'] = intval($device['LAXPIC']);
$result['subscribed'] = array();
foreach ($this->folders_meta() as $folder => $meta) {
if ($meta[$imei]['S'])
$result['subscribed'][$folder] = intval($meta[$imei]['S']);
}
$this->rc->output->command('plugin.zpush_data_ready', $result);
}
else {
$this->rc->output->show_message($this->gettext('devicenotfound'), 'error');
}
break;
case 'save':
- $this->init_imap();
$devices = $this->list_devices();
$syncmode = intval(get_input_value('syncmode', RCUBE_INPUT_POST));
$devicealias = get_input_value('devicealias', RCUBE_INPUT_POST, true);
$laxpic = intval(get_input_value('laxpic', RCUBE_INPUT_POST));
$subsciptions = get_input_value('subscribed', RCUBE_INPUT_POST);
$err = false;
if ($device = $devices[$imei]) {
// update device config if changed
if ($devicealias != $this->root_meta['DEVICE'][$imei]['ALIAS'] ||
$syncmode != $this->root_meta['DEVICE'][$imei]['MODE'] ||
$laxpic != $this->root_meta['DEVICE'][$imei]['LAXPIC'] ||
$subsciptions[self::ROOT_MAILBOX] != $this->root_meta['FOLDER'][$imei]['S']) {
$this->root_meta['DEVICE'][$imei]['MODE'] = $syncmode;
$this->root_meta['DEVICE'][$imei]['ALIAS'] = $devicealias;
$this->root_meta['DEVICE'][$imei]['LAXPIC'] = $laxpic;
$this->root_meta['FOLDER'][$imei]['S'] = intval($subsciptions[self::ROOT_MAILBOX]);
- $err = !$this->rc->imap->set_metadata(self::ROOT_MAILBOX,
+ $err = !$storage->set_metadata(self::ROOT_MAILBOX,
array(self::ACTIVESYNC_KEY => $this->serialize_metadata($this->root_meta)));
// update cached meta data
if (!$err) {
$this->cache->remove('devicemeta');
- $this->cache->write('devicemeta', $this->rc->imap->get_metadata(self::ROOT_MAILBOX, self::ACTIVESYNC_KEY));
+ $this->cache->write('devicemeta', $storage->get_metadata(self::ROOT_MAILBOX, self::ACTIVESYNC_KEY));
}
}
// iterate over folders list and update metadata if necessary
foreach ($this->folders_meta() as $folder => $meta) {
// skip root folder (already handled above)
if ($folder == self::ROOT_MAILBOX)
continue;
if ($subsciptions[$folder] != $meta[$imei]['S']) {
$meta[$imei]['S'] = intval($subsciptions[$folder]);
$this->folders_meta[$folder] = $meta;
unset($meta['TYPE']);
// read metadata first
- $folderdata = $this->rc->imap->get_metadata($folder, array(self::ACTIVESYNC_KEY));
+ $folderdata = $storage->get_metadata($folder, array(self::ACTIVESYNC_KEY));
if ($asyncdata = $folderdata[$folder][self::ACTIVESYNC_KEY])
$metadata = $this->unserialize_metadata($asyncdata);
$metadata['FOLDER'] = $meta;
- $err |= !$this->rc->imap->set_metadata($folder, array(self::ACTIVESYNC_KEY => $this->serialize_metadata($metadata)));
+ $err |= !$storage->set_metadata($folder, array(self::ACTIVESYNC_KEY => $this->serialize_metadata($metadata)));
}
}
// update cache
$this->cache->remove('folders');
$this->cache->write('folders', $this->folders_meta);
$this->rc->output->command('plugin.zpush_save_complete', array('success' => !$err, 'id' => $imei, 'devicealias' => Q($devicealias)));
}
if ($err)
$this->rc->output->show_message($this->gettext('savingerror'), 'error');
else
$this->rc->output->show_message($this->gettext('successfullysaved'), 'confirmation');
break;
case 'delete':
$this->init_imap();
$devices = $this->list_devices();
if ($device = $devices[$imei]) {
unset($this->root_meta['DEVICE'][$imei], $this->root_meta['FOLDER'][$imei]);
// update annotation and cached meta data
- if ($success = $this->rc->imap->set_metadata(self::ROOT_MAILBOX, array(self::ACTIVESYNC_KEY => $this->serialize_metadata($this->root_meta)))) {
+ if ($success = $storage->set_metadata(self::ROOT_MAILBOX, array(self::ACTIVESYNC_KEY => $this->serialize_metadata($this->root_meta)))) {
$this->cache->remove('devicemeta');
- $this->cache->write('devicemeta', $this->rc->imap->get_metadata(self::ROOT_MAILBOX, self::ACTIVESYNC_KEY));
+ $this->cache->write('devicemeta', $storage->get_metadata(self::ROOT_MAILBOX, self::ACTIVESYNC_KEY));
// remove device annotation in every folder
foreach ($this->folders_meta() as $folder => $meta) {
// skip root folder (already handled above)
if ($folder == self::ROOT_MAILBOX)
continue;
if (isset($meta[$imei])) {
$type = $meta['TYPE']; // remember folder type
unset($meta[$imei], $meta['TYPE']);
// read metadata first and update FOLDER property
- $folderdata = $this->rc->imap->get_metadata($folder, array(self::ACTIVESYNC_KEY));
+ $folderdata = $storage->get_metadata($folder, array(self::ACTIVESYNC_KEY));
if ($asyncdata = $folderdata[$folder][self::ACTIVESYNC_KEY])
$metadata = $this->unserialize_metadata($asyncdata);
$metadata['FOLDER'] = $meta;
- if ($this->rc->imap->set_metadata($folder, array(self::ACTIVESYNC_KEY => $this->serialize_metadata($metadata)))) {
+ if ($storage->set_metadata($folder, array(self::ACTIVESYNC_KEY => $this->serialize_metadata($metadata)))) {
$this->folders_meta[$folder] = $metadata;
$this->folders_meta[$folder]['TYPE'] = $type;
}
}
}
// update cache
$this->cache->remove('folders');
$this->cache->write('folders', $this->folders_meta);
}
}
if ($success) {
$this->rc->output->show_message($this->gettext('successfullydeleted'), 'confirmation');
$this->rc->output->redirect(array('action' => 'plugin.zpushconfig')); // reload UI
}
else
$this->rc->output->show_message($this->gettext('savingerror'), 'error');
break;
}
$this->rc->output->send();
}
/**
* Render main UI for device configuration
*/
public function config_view()
{
- require_once($this->home . '/kolab_zpush_ui.php');
+ require_once $this->home . '/kolab_zpush_ui.php';
- $this->init_imap();
+ $storage = $this->rc->get_storage();
// checks if IMAP server supports any of METADATA, ANNOTATEMORE, ANNOTATEMORE2
- if (!($this->rc->imap->get_capability('METADATA') || $this->rc->imap->get_capability('ANNOTATEMORE') || $this->rc->imap->get_capability('ANNOTATEMORE2'))) {
+ if (!($storage->get_capability('METADATA') || $storage->get_capability('ANNOTATEMORE') || $storage->get_capability('ANNOTATEMORE2'))) {
$this->rc->output->show_message($this->gettext('notsupported'), 'error');
}
$this->ui = new kolab_zpush_ui($this);
$this->register_handler('plugin.devicelist', array($this->ui, 'device_list'));
$this->register_handler('plugin.deviceconfigform', array($this->ui, 'device_config_form'));
$this->register_handler('plugin.foldersubscriptions', array($this->ui, 'folder_subscriptions'));
$this->rc->output->set_env('devicecount', count($this->list_devices()));
$this->rc->output->send('kolab_zpush.config');
}
/**
* List known devices
*
* @return array Device list as hash array
*/
public function list_devices()
{
if (!isset($this->devices)) {
$this->devices = (array)$this->root_meta['DEVICE'];
}
return $this->devices;
}
/**
* Get list of all folders available for sync
*
* @return array List of mailbox folders
*/
public function list_folders()
{
if (!isset($this->folders)) {
// read cached folder meta data
if ($cached_folders = $this->cache->read('folders')) {
$this->folders_meta = $cached_folders;
$this->folders = array_keys($this->folders_meta);
}
// fetch folder data from server
else {
- $this->folders = $this->rc->imap->list_unsubscribed();
+ $storage = $this->rc->get_storage();
+ $this->folders = $storage->list_folders();
+
foreach ($this->folders as $folder) {
- $folderdata = $this->rc->imap->get_metadata($folder, array(self::ACTIVESYNC_KEY, self::CTYPE_KEY));
+ $folderdata = $storage->get_metadata($folder, array(self::ACTIVESYNC_KEY, self::CTYPE_KEY));
$foldertype = explode('.', $folderdata[$folder][self::CTYPE_KEY]);
if ($asyncdata = $folderdata[$folder][self::ACTIVESYNC_KEY]) {
$metadata = $this->unserialize_metadata($asyncdata);
$this->folders_meta[$folder] = $metadata['FOLDER'];
}
$this->folders_meta[$folder]['TYPE'] = !empty($foldertype[0]) ? $foldertype[0] : 'mail';
}
// cache it!
$this->cache->write('folders', $this->folders_meta);
}
}
return $this->folders;
}
/**
* Getter for folder metadata
*
* @return array Hash array with meta data for each folder
*/
public function folders_meta()
{
if (!isset($this->folders_meta))
$this->list_folders();
return $this->folders_meta;
}
/**
* Helper method to decode saved IMAP metadata
*/
private function unserialize_metadata($str)
{
if (!empty($str))
return @json_decode(base64_decode($str), true);
return null;
}
/**
* Helper method to encode IMAP metadata for saving
*/
private function serialize_metadata($data)
{
if (is_array($data))
return base64_encode(json_encode($data));
return '';
}
}
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
+?>
diff --git a/plugins/kolab_zpush/localization/en_US.inc b/plugins/kolab_zpush/localization/en_US.inc
index 5160d91c..2537d4d5 100644
--- a/plugins/kolab_zpush/localization/en_US.inc
+++ b/plugins/kolab_zpush/localization/en_US.inc
@@ -1,32 +1,33 @@
<?php
$labels = array();
$labels['tabtitle'] = 'Activesync';
$labels['devices'] = 'Devices';
$labels['devicealias'] = 'Device name';
$labels['syncmode'] = 'Sync Mode';
$labels['modeauto'] = 'Determine automatically';
$labels['modeflat'] = 'Force flat mode';
$labels['modefolder'] = 'Force folder mode';
$labels['synchronize'] = 'Synchronize';
$labels['withalarms'] = 'With alarms';
$labels['syncsettings'] = 'Synchronization settings';
$labels['deviceconfig'] = 'Device configration';
$labels['folderstosync'] = 'Folders to synchronize';
$labels['mail'] = 'Email';
$labels['contact'] = 'Address Books';
$labels['event'] = 'Calendars';
$labels['task'] = 'Tasks';
$labels['note'] = 'Notes';
+$labels['configuration'] = 'Configuration';
$labels['deletedevice'] = 'Delete device';
$labels['imageformat'] = 'Image format';
$labels['laxpiclabel'] = 'Allow PNG and GIF images';
$labels['introtext'] = 'Select a device to configure';
$labels['nodevices'] = 'There are currently no devices registered.<br/><br/>In order to register a device, please connect it to the server first, using <a href="http://wiki.kolab.org/Z_push#Clients">the instructions in the Wiki</a>. Afterwards the device should become available for configuration here.';
$labels['savingdata'] = 'Saving data...';
$labels['savingerror'] = 'Failed to save configuration';
$labels['notsupported'] = 'Your server does not support metadata/annotations';
$labels['devicedeleteconfirm'] = 'Do you really want to delete the configuration for this device?';
$labels['successfullydeleted'] = 'The device configuration was successfully removed';
?>
\ No newline at end of file
diff --git a/plugins/kolab_zpush/package.xml b/plugins/kolab_zpush/package.xml
index 6313d1e3..250d1a66 100644
--- a/plugins/kolab_zpush/package.xml
+++ b/plugins/kolab_zpush/package.xml
@@ -1,64 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<package xmlns="http://pear.php.net/dtd/package-2.0" xmlns:tasks="http://pear.php.net/dtd/tasks-1.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" packagerversion="1.9.0" version="2.0" xsi:schemaLocation="http://pear.php.net/dtd/tasks-1.0
http://pear.php.net/dtd/tasks-1.0.xsd
http://pear.php.net/dtd/package-2.0
http://pear.php.net/dtd/package-2.0.xsd">
<name>kolab_zpush</name>
- <uri>http://kolabsys.com</uri>
+ <uri>http://git.kolab.org/roundcube-plugins-kolab/</uri>
<summary>Z-Push configuration utility for Kolab accounts</summary>
<description></description>
<lead>
<name>Thomas Bruederli</name>
<user>thomasb</user>
<email>bruederli@kolabsys.com</email>
<active>yes</active>
</lead>
<date>2011-11-14</date>
<time>12:12:00</time>
<version>
- <release>0.2</release>
+ <release>0.3</release>
</version>
<stability>
<release>stable</release>
</stability>
<license uri="http://www.gnu.org/licenses/agpl.html">GNU AGPL</license>
<notes>-</notes>
<contents>
<dir baseinstalldir="/" name="/">
<file name="kolab_zpush.php" role="php"></file>
<file name="kolab_zpush_ui.php" role="php"></file>
<file name="kolab_zpush.js" role="data"></file>
<file name="localization/de_CH.inc" role="data"></file>
<file name="localization/en_US.inc" role="data"></file>
<file name="localization/pl_PL.inc" role="data"></file>
<file name="skins/default/templates/config.html" role="data"></file>
<file name="skins/default/config.css" role="data"></file>
<file name="skins/default/alarm-clock.png" role="data"></file>
<file name="skins/default/deviceactions.png" role="data"></file>
<file name="skins/default/foldertypes.png" role="data"></file>
<file name="skins/default/pointer-left.gif" role="data"></file>
<file name="skins/default/synchronize.png" role="data"></file>
+ <file name="skins/larry/templates/config.html" role="data"></file>
+ <file name="skins/larry/config.css" role="data"></file>
+ <file name="skins/larry/alarm-clock.png" role="data"></file>
+ <file name="skins/larry/deviceactions.png" role="data"></file>
+ <file name="skins/larry/foldertypes.png" role="data"></file>
+ <file name="skins/larry/pointer-left.png" role="data"></file>
+ <file name="skins/larry/synchronize.png" role="data"></file>
<file name="README" role="data"></file>
<file name="LICENSE" role="data"></file>
</dir>
</contents>
<dependencies>
<required>
<php>
<min>5.2.1</min>
</php>
<pearinstaller>
<min>1.7.0</min>
</pearinstaller>
<package>
<name>kolab_core</name>
<uri>http://kolabsys.com</uri>
</package>
<package>
<name>jqueryui</name>
<channel>pear.roundcube.net</uri>
</package>
</required>
</dependencies>
<phprelease/>
</package>
diff --git a/plugins/kolab_zpush/skins/larry/alarm-clock.png b/plugins/kolab_zpush/skins/larry/alarm-clock.png
new file mode 100755
index 00000000..518bdc11
Binary files /dev/null and b/plugins/kolab_zpush/skins/larry/alarm-clock.png differ
diff --git a/plugins/kolab_zpush/skins/larry/config.css b/plugins/kolab_zpush/skins/larry/config.css
new file mode 100644
index 00000000..40d5b87a
--- /dev/null
+++ b/plugins/kolab_zpush/skins/larry/config.css
@@ -0,0 +1,135 @@
+/* Stylesheets for the Kolab Z-Push configuration UI */
+
+#configform {
+ padding-top: 15px;
+}
+
+#sectionslist {
+ width: 220px;
+}
+
+#prefs-box {
+ position: absolute;
+ top: 0;
+ left: 232px;
+ right: 0;
+ bottom: 0;
+}
+
+#devices-table {
+ width: 100%;
+ table-layout: fixed;
+}
+
+#devices-table td {
+ cursor: pointer;
+}
+
+#devices-table td span.devicetype {
+ padding-left: 1em;
+ font-style: italic;
+ color: #69939e;
+}
+
+div.subscriptionblock {
+ float: left;
+ margin: 0 3em 2em 0;
+ padding: 0;
+}
+
+div.subscriptionblock h3 {
+ font-size: 14px;
+ color: #333;
+ margin: 0 0 0.6em 0;
+ padding: 2px 4px 2px 30px;
+ background: url(foldertypes.png) 4px 2px no-repeat;
+}
+
+div.subscriptionblock h3.contact {
+ background-position: 4px -18px;
+}
+
+div.subscriptionblock h3.event {
+ background-position: 4px -38px;
+}
+
+div.subscriptionblock h3.task {
+ background-position: 4px -58x;
+}
+
+div.subscriptionblock h3.note {
+ background-position: 4px -78px;
+}
+
+#foldersubscriptions thead td {
+ color: #69939e;
+ font-weight: bold;
+ padding: 3px 5px;
+ min-width: 2em;
+ background: #d6eaf3;
+ border-bottom: 2px solid #fff;
+}
+
+#foldersubscriptions tbody td {
+ background: #eee;
+ padding: 2px 5px;
+ border-bottom: 2px solid #fff;
+}
+
+#foldersubscriptions td label {
+ display: block;
+}
+
+#foldersubscriptions td.mailbox {
+ padding-right: 3em;
+ padding-left: 2px;
+ min-width: 12em;
+}
+
+#foldersubscriptions td.virtual {
+ color: #999;
+}
+
+#foldersubscriptions {
+ overflow: auto;
+ max-height: 400px;
+ margin-top: 0.5em;
+}
+
+#introtext {
+ position: absolute;
+ top: 16px;
+ left: 5px;
+ padding-left: 10px;
+ max-width: 40em;
+}
+
+#introtext .pointer-left {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 12px;
+ height: 80%;
+ background: url(pointer-left.png) right center no-repeat;
+}
+
+#introtext .inner {
+ color: #eee;
+ padding: 12px;
+ border: 1px solid #333;
+ border-radius: 4px;
+ text-shadow: 0px 1px 1px #333;
+ background: #5b5b5b;
+ background: -moz-linear-gradient(top, #5b5b5b 0%, #3a3a3a 100%);
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#5b5b5b), color-stop(100%,#3a3a3a));
+ background: -o-linear-gradient(top, #5b5b5b 0%, #3a3a3a 100%);
+ background: -ms-linear-gradient(top, #5b5b5b 0%, #3a3a3a 100%);
+ background: linear-gradient(top, #5b5b5b 0%, #3a3a3a 100%);
+ -webkit-box-shadow: inset 0px 0px 0px 1px #7e7e7e, 0 2px 6px 0 #333;
+ -moz-box-shadow: inset 0px 0px 0px 1px #7e7e7e, 0 2px 6px 0 #333;
+ box-shadow: inset 0px 0px 0px 1px #7e7e7e, 0 2px 6px 0 #333;
+}
+
+#introtext a {
+ color: #b0ccd7;
+}
diff --git a/plugins/kolab_zpush/skins/larry/foldertypes.png b/plugins/kolab_zpush/skins/larry/foldertypes.png
new file mode 100644
index 00000000..4950296a
Binary files /dev/null and b/plugins/kolab_zpush/skins/larry/foldertypes.png differ
diff --git a/plugins/kolab_zpush/skins/larry/pointer-left.png b/plugins/kolab_zpush/skins/larry/pointer-left.png
new file mode 100644
index 00000000..bfa7e86c
Binary files /dev/null and b/plugins/kolab_zpush/skins/larry/pointer-left.png differ
diff --git a/plugins/kolab_zpush/skins/larry/synchronize.png b/plugins/kolab_zpush/skins/larry/synchronize.png
new file mode 100755
index 00000000..ba5ebd1f
Binary files /dev/null and b/plugins/kolab_zpush/skins/larry/synchronize.png differ
diff --git a/plugins/kolab_zpush/skins/larry/templates/config.html b/plugins/kolab_zpush/skins/larry/templates/config.html
new file mode 100644
index 00000000..9671bc6a
--- /dev/null
+++ b/plugins/kolab_zpush/skins/larry/templates/config.html
@@ -0,0 +1,71 @@
+<roundcube:object name="doctype" value="html5" />
+<html>
+<head>
+<title><roundcube:object name="pagetitle" /></title>
+<roundcube:include file="/includes/links.html" />
+</head>
+<body>
+
+<roundcube:include file="/includes/header.html" />
+
+<div id="mainscreen" class="offset">
+
+<roundcube:include file="/includes/settingstabs.html" />
+
+<div id="settings-right">
+
+<div id="sectionslist" class="uibox listbox">
+<h2 id="directorylist-title" class="boxtitle"><roundcube:label name="kolab_zpush.devices" /></h2>
+<div class="boxlistcontent">
+ <roundcube:object name="plugin.devicelist" id="devices-table" class="listing" cellspacing="0" />
+</div>
+<div class="boxfooter">
+ <roundcube:button type="link" command="plugin.delete-device" title="kolab_zpush.deletedevice" class="listbutton delete disabled" classAct="listbutton delete" innerClass="inner" content="x" />
+</div>
+</div>
+
+<div id="prefs-box" class="uibox contentbox">
+ <h1 class="boxtitle" style="display:none"><roundcube:label name="kolab_zpush.syncsettings"></h1>
+ <form action="#" method="post" id="configform" class="boxcontent tabbed" style="display:none">
+ <fieldset>
+ <legend><roundcube:label name="kolab_zpush.folderstosync" /></legend>
+ <roundcube:object name="plugin.foldersubscriptions" form="configform" id="foldersubscriptions" syncicon="synchronize.png" alarmicon="alarm-clock.png" />
+ </fieldset>
+ <fieldset>
+ <legend><roundcube:label name="kolab_zpush.deviceconfig" /></legend>
+ <roundcube:object name="plugin.deviceconfigform" form="configform" class="propform" />
+ </fieldset>
+
+ <p class="formbuttons">
+ <roundcube:button type="input" class="button mainaction" command="plugin.save-config" label="save" />
+ <roundcube:button type="input" class="button" command="plugin.delete-device" label="kolab_zpush.deletedevice" />
+ </p>
+ </form>
+
+ <div id="introtext">
+ <div class="inner">
+ <roundcube:if condition="env:devicecount" />
+ <roundcube:label name="kolab_zpush.introtext" />
+ <roundcube:else />
+ <roundcube:label name="kolab_zpush.nodevices" html="true" />
+ <roundcube:endif />
+ </div>
+ <div class="pointer-left"></div>
+ </div>
+
+ <roundcube:object name="message" id="message" class="statusbar" />
+</div>
+
+</div>
+
+<roundcube:include file="/includes/footer.html" />
+
+<script type="text/javascript">
+
+ var viewsplit = new rcube_splitter({ id:'devicelistsplitter', p1:'#sectionslist', p2:'#prefs-box', orientation:'v', relative:true, start:226, min:150, size:12 });
+ rcmail.add_onload('viewsplit.init()');
+
+</script>
+
+</body>
+</html>
diff --git a/plugins/owncloud/LICENSE b/plugins/owncloud/LICENSE
new file mode 100644
index 00000000..dba13ed2
--- /dev/null
+++ b/plugins/owncloud/LICENSE
@@ -0,0 +1,661 @@
+ GNU AFFERO GENERAL PUBLIC LICENSE
+ Version 3, 19 November 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
+ Everyone is permitted to copy and distribute verbatim copies
+ of this license document, but changing it is not allowed.
+
+ Preamble
+
+ The GNU Affero General Public License is a free, copyleft license for
+software and other kinds of works, specifically designed to ensure
+cooperation with the community in the case of network server software.
+
+ The licenses for most software and other practical works are designed
+to take away your freedom to share and change the works. By contrast,
+our General Public Licenses are intended to guarantee your freedom to
+share and change all versions of a program--to make sure it remains free
+software for all its users.
+
+ When we speak of free software, we are referring to freedom, not
+price. Our General Public Licenses are designed to make sure that you
+have the freedom to distribute copies of free software (and charge for
+them if you wish), that you receive source code or can get it if you
+want it, that you can change the software or use pieces of it in new
+free programs, and that you know you can do these things.
+
+ Developers that use our General Public Licenses protect your rights
+with two steps: (1) assert copyright on the software, and (2) offer
+you this License which gives you legal permission to copy, distribute
+and/or modify the software.
+
+ A secondary benefit of defending all users' freedom is that
+improvements made in alternate versions of the program, if they
+receive widespread use, become available for other developers to
+incorporate. Many developers of free software are heartened and
+encouraged by the resulting cooperation. However, in the case of
+software used on network servers, this result may fail to come about.
+The GNU General Public License permits making a modified version and
+letting the public access it on a server without ever releasing its
+source code to the public.
+
+ The GNU Affero General Public License is designed specifically to
+ensure that, in such cases, the modified source code becomes available
+to the community. It requires the operator of a network server to
+provide the source code of the modified version running there to the
+users of that server. Therefore, public use of a modified version, on
+a publicly accessible server, gives the public access to the source
+code of the modified version.
+
+ An older license, called the Affero General Public License and
+published by Affero, was designed to accomplish similar goals. This is
+a different license, not a version of the Affero GPL, but Affero has
+released a new version of the Affero GPL which permits relicensing under
+this license.
+
+ The precise terms and conditions for copying, distribution and
+modification follow.
+
+ TERMS AND CONDITIONS
+
+ 0. Definitions.
+
+ "This License" refers to version 3 of the GNU Affero General Public License.
+
+ "Copyright" also means copyright-like laws that apply to other kinds of
+works, such as semiconductor masks.
+
+ "The Program" refers to any copyrightable work licensed under this
+License. Each licensee is addressed as "you". "Licensees" and
+"recipients" may be individuals or organizations.
+
+ To "modify" a work means to copy from or adapt all or part of the work
+in a fashion requiring copyright permission, other than the making of an
+exact copy. The resulting work is called a "modified version" of the
+earlier work or a work "based on" the earlier work.
+
+ A "covered work" means either the unmodified Program or a work based
+on the Program.
+
+ To "propagate" a work means to do anything with it that, without
+permission, would make you directly or secondarily liable for
+infringement under applicable copyright law, except executing it on a
+computer or modifying a private copy. Propagation includes copying,
+distribution (with or without modification), making available to the
+public, and in some countries other activities as well.
+
+ To "convey" a work means any kind of propagation that enables other
+parties to make or receive copies. Mere interaction with a user through
+a computer network, with no transfer of a copy, is not conveying.
+
+ An interactive user interface displays "Appropriate Legal Notices"
+to the extent that it includes a convenient and prominently visible
+feature that (1) displays an appropriate copyright notice, and (2)
+tells the user that there is no warranty for the work (except to the
+extent that warranties are provided), that licensees may convey the
+work under this License, and how to view a copy of this License. If
+the interface presents a list of user commands or options, such as a
+menu, a prominent item in the list meets this criterion.
+
+ 1. Source Code.
+
+ The "source code" for a work means the preferred form of the work
+for making modifications to it. "Object code" means any non-source
+form of a work.
+
+ A "Standard Interface" means an interface that either is an official
+standard defined by a recognized standards body, or, in the case of
+interfaces specified for a particular programming language, one that
+is widely used among developers working in that language.
+
+ The "System Libraries" of an executable work include anything, other
+than the work as a whole, that (a) is included in the normal form of
+packaging a Major Component, but which is not part of that Major
+Component, and (b) serves only to enable use of the work with that
+Major Component, or to implement a Standard Interface for which an
+implementation is available to the public in source code form. A
+"Major Component", in this context, means a major essential component
+(kernel, window system, and so on) of the specific operating system
+(if any) on which the executable work runs, or a compiler used to
+produce the work, or an object code interpreter used to run it.
+
+ The "Corresponding Source" for a work in object code form means all
+the source code needed to generate, install, and (for an executable
+work) run the object code and to modify the work, including scripts to
+control those activities. However, it does not include the work's
+System Libraries, or general-purpose tools or generally available free
+programs which are used unmodified in performing those activities but
+which are not part of the work. For example, Corresponding Source
+includes interface definition files associated with source files for
+the work, and the source code for shared libraries and dynamically
+linked subprograms that the work is specifically designed to require,
+such as by intimate data communication or control flow between those
+subprograms and other parts of the work.
+
+ The Corresponding Source need not include anything that users
+can regenerate automatically from other parts of the Corresponding
+Source.
+
+ The Corresponding Source for a work in source code form is that
+same work.
+
+ 2. Basic Permissions.
+
+ All rights granted under this License are granted for the term of
+copyright on the Program, and are irrevocable provided the stated
+conditions are met. This License explicitly affirms your unlimited
+permission to run the unmodified Program. The output from running a
+covered work is covered by this License only if the output, given its
+content, constitutes a covered work. This License acknowledges your
+rights of fair use or other equivalent, as provided by copyright law.
+
+ You may make, run and propagate covered works that you do not
+convey, without conditions so long as your license otherwise remains
+in force. You may convey covered works to others for the sole purpose
+of having them make modifications exclusively for you, or provide you
+with facilities for running those works, provided that you comply with
+the terms of this License in conveying all material for which you do
+not control copyright. Those thus making or running the covered works
+for you must do so exclusively on your behalf, under your direction
+and control, on terms that prohibit them from making any copies of
+your copyrighted material outside their relationship with you.
+
+ Conveying under any other circumstances is permitted solely under
+the conditions stated below. Sublicensing is not allowed; section 10
+makes it unnecessary.
+
+ 3. Protecting Users' Legal Rights From Anti-Circumvention Law.
+
+ No covered work shall be deemed part of an effective technological
+measure under any applicable law fulfilling obligations under article
+11 of the WIPO copyright treaty adopted on 20 December 1996, or
+similar laws prohibiting or restricting circumvention of such
+measures.
+
+ When you convey a covered work, you waive any legal power to forbid
+circumvention of technological measures to the extent such circumvention
+is effected by exercising rights under this License with respect to
+the covered work, and you disclaim any intention to limit operation or
+modification of the work as a means of enforcing, against the work's
+users, your or third parties' legal rights to forbid circumvention of
+technological measures.
+
+ 4. Conveying Verbatim Copies.
+
+ You may convey verbatim copies of the Program's source code as you
+receive it, in any medium, provided that you conspicuously and
+appropriately publish on each copy an appropriate copyright notice;
+keep intact all notices stating that this License and any
+non-permissive terms added in accord with section 7 apply to the code;
+keep intact all notices of the absence of any warranty; and give all
+recipients a copy of this License along with the Program.
+
+ You may charge any price or no price for each copy that you convey,
+and you may offer support or warranty protection for a fee.
+
+ 5. Conveying Modified Source Versions.
+
+ You may convey a work based on the Program, or the modifications to
+produce it from the Program, in the form of source code under the
+terms of section 4, provided that you also meet all of these conditions:
+
+ a) The work must carry prominent notices stating that you modified
+ it, and giving a relevant date.
+
+ b) The work must carry prominent notices stating that it is
+ released under this License and any conditions added under section
+ 7. This requirement modifies the requirement in section 4 to
+ "keep intact all notices".
+
+ c) You must license the entire work, as a whole, under this
+ License to anyone who comes into possession of a copy. This
+ License will therefore apply, along with any applicable section 7
+ additional terms, to the whole of the work, and all its parts,
+ regardless of how they are packaged. This License gives no
+ permission to license the work in any other way, but it does not
+ invalidate such permission if you have separately received it.
+
+ d) If the work has interactive user interfaces, each must display
+ Appropriate Legal Notices; however, if the Program has interactive
+ interfaces that do not display Appropriate Legal Notices, your
+ work need not make them do so.
+
+ A compilation of a covered work with other separate and independent
+works, which are not by their nature extensions of the covered work,
+and which are not combined with it such as to form a larger program,
+in or on a volume of a storage or distribution medium, is called an
+"aggregate" if the compilation and its resulting copyright are not
+used to limit the access or legal rights of the compilation's users
+beyond what the individual works permit. Inclusion of a covered work
+in an aggregate does not cause this License to apply to the other
+parts of the aggregate.
+
+ 6. Conveying Non-Source Forms.
+
+ You may convey a covered work in object code form under the terms
+of sections 4 and 5, provided that you also convey the
+machine-readable Corresponding Source under the terms of this License,
+in one of these ways:
+
+ a) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by the
+ Corresponding Source fixed on a durable physical medium
+ customarily used for software interchange.
+
+ b) Convey the object code in, or embodied in, a physical product
+ (including a physical distribution medium), accompanied by a
+ written offer, valid for at least three years and valid for as
+ long as you offer spare parts or customer support for that product
+ model, to give anyone who possesses the object code either (1) a
+ copy of the Corresponding Source for all the software in the
+ product that is covered by this License, on a durable physical
+ medium customarily used for software interchange, for a price no
+ more than your reasonable cost of physically performing this
+ conveying of source, or (2) access to copy the
+ Corresponding Source from a network server at no charge.
+
+ c) Convey individual copies of the object code with a copy of the
+ written offer to provide the Corresponding Source. This
+ alternative is allowed only occasionally and noncommercially, and
+ only if you received the object code with such an offer, in accord
+ with subsection 6b.
+
+ d) Convey the object code by offering access from a designated
+ place (gratis or for a charge), and offer equivalent access to the
+ Corresponding Source in the same way through the same place at no
+ further charge. You need not require recipients to copy the
+ Corresponding Source along with the object code. If the place to
+ copy the object code is a network server, the Corresponding Source
+ may be on a different server (operated by you or a third party)
+ that supports equivalent copying facilities, provided you maintain
+ clear directions next to the object code saying where to find the
+ Corresponding Source. Regardless of what server hosts the
+ Corresponding Source, you remain obligated to ensure that it is
+ available for as long as needed to satisfy these requirements.
+
+ e) Convey the object code using peer-to-peer transmission, provided
+ you inform other peers where the object code and Corresponding
+ Source of the work are being offered to the general public at no
+ charge under subsection 6d.
+
+ A separable portion of the object code, whose source code is excluded
+from the Corresponding Source as a System Library, need not be
+included in conveying the object code work.
+
+ A "User Product" is either (1) a "consumer product", which means any
+tangible personal property which is normally used for personal, family,
+or household purposes, or (2) anything designed or sold for incorporation
+into a dwelling. In determining whether a product is a consumer product,
+doubtful cases shall be resolved in favor of coverage. For a particular
+product received by a particular user, "normally used" refers to a
+typical or common use of that class of product, regardless of the status
+of the particular user or of the way in which the particular user
+actually uses, or expects or is expected to use, the product. A product
+is a consumer product regardless of whether the product has substantial
+commercial, industrial or non-consumer uses, unless such uses represent
+the only significant mode of use of the product.
+
+ "Installation Information" for a User Product means any methods,
+procedures, authorization keys, or other information required to install
+and execute modified versions of a covered work in that User Product from
+a modified version of its Corresponding Source. The information must
+suffice to ensure that the continued functioning of the modified object
+code is in no case prevented or interfered with solely because
+modification has been made.
+
+ If you convey an object code work under this section in, or with, or
+specifically for use in, a User Product, and the conveying occurs as
+part of a transaction in which the right of possession and use of the
+User Product is transferred to the recipient in perpetuity or for a
+fixed term (regardless of how the transaction is characterized), the
+Corresponding Source conveyed under this section must be accompanied
+by the Installation Information. But this requirement does not apply
+if neither you nor any third party retains the ability to install
+modified object code on the User Product (for example, the work has
+been installed in ROM).
+
+ The requirement to provide Installation Information does not include a
+requirement to continue to provide support service, warranty, or updates
+for a work that has been modified or installed by the recipient, or for
+the User Product in which it has been modified or installed. Access to a
+network may be denied when the modification itself materially and
+adversely affects the operation of the network or violates the rules and
+protocols for communication across the network.
+
+ Corresponding Source conveyed, and Installation Information provided,
+in accord with this section must be in a format that is publicly
+documented (and with an implementation available to the public in
+source code form), and must require no special password or key for
+unpacking, reading or copying.
+
+ 7. Additional Terms.
+
+ "Additional permissions" are terms that supplement the terms of this
+License by making exceptions from one or more of its conditions.
+Additional permissions that are applicable to the entire Program shall
+be treated as though they were included in this License, to the extent
+that they are valid under applicable law. If additional permissions
+apply only to part of the Program, that part may be used separately
+under those permissions, but the entire Program remains governed by
+this License without regard to the additional permissions.
+
+ When you convey a copy of a covered work, you may at your option
+remove any additional permissions from that copy, or from any part of
+it. (Additional permissions may be written to require their own
+removal in certain cases when you modify the work.) You may place
+additional permissions on material, added by you to a covered work,
+for which you have or can give appropriate copyright permission.
+
+ Notwithstanding any other provision of this License, for material you
+add to a covered work, you may (if authorized by the copyright holders of
+that material) supplement the terms of this License with terms:
+
+ a) Disclaiming warranty or limiting liability differently from the
+ terms of sections 15 and 16 of this License; or
+
+ b) Requiring preservation of specified reasonable legal notices or
+ author attributions in that material or in the Appropriate Legal
+ Notices displayed by works containing it; or
+
+ c) Prohibiting misrepresentation of the origin of that material, or
+ requiring that modified versions of such material be marked in
+ reasonable ways as different from the original version; or
+
+ d) Limiting the use for publicity purposes of names of licensors or
+ authors of the material; or
+
+ e) Declining to grant rights under trademark law for use of some
+ trade names, trademarks, or service marks; or
+
+ f) Requiring indemnification of licensors and authors of that
+ material by anyone who conveys the material (or modified versions of
+ it) with contractual assumptions of liability to the recipient, for
+ any liability that these contractual assumptions directly impose on
+ those licensors and authors.
+
+ All other non-permissive additional terms are considered "further
+restrictions" within the meaning of section 10. If the Program as you
+received it, or any part of it, contains a notice stating that it is
+governed by this License along with a term that is a further
+restriction, you may remove that term. If a license document contains
+a further restriction but permits relicensing or conveying under this
+License, you may add to a covered work material governed by the terms
+of that license document, provided that the further restriction does
+not survive such relicensing or conveying.
+
+ If you add terms to a covered work in accord with this section, you
+must place, in the relevant source files, a statement of the
+additional terms that apply to those files, or a notice indicating
+where to find the applicable terms.
+
+ Additional terms, permissive or non-permissive, may be stated in the
+form of a separately written license, or stated as exceptions;
+the above requirements apply either way.
+
+ 8. Termination.
+
+ You may not propagate or modify a covered work except as expressly
+provided under this License. Any attempt otherwise to propagate or
+modify it is void, and will automatically terminate your rights under
+this License (including any patent licenses granted under the third
+paragraph of section 11).
+
+ However, if you cease all violation of this License, then your
+license from a particular copyright holder is reinstated (a)
+provisionally, unless and until the copyright holder explicitly and
+finally terminates your license, and (b) permanently, if the copyright
+holder fails to notify you of the violation by some reasonable means
+prior to 60 days after the cessation.
+
+ Moreover, your license from a particular copyright holder is
+reinstated permanently if the copyright holder notifies you of the
+violation by some reasonable means, this is the first time you have
+received notice of violation of this License (for any work) from that
+copyright holder, and you cure the violation prior to 30 days after
+your receipt of the notice.
+
+ Termination of your rights under this section does not terminate the
+licenses of parties who have received copies or rights from you under
+this License. If your rights have been terminated and not permanently
+reinstated, you do not qualify to receive new licenses for the same
+material under section 10.
+
+ 9. Acceptance Not Required for Having Copies.
+
+ You are not required to accept this License in order to receive or
+run a copy of the Program. Ancillary propagation of a covered work
+occurring solely as a consequence of using peer-to-peer transmission
+to receive a copy likewise does not require acceptance. However,
+nothing other than this License grants you permission to propagate or
+modify any covered work. These actions infringe copyright if you do
+not accept this License. Therefore, by modifying or propagating a
+covered work, you indicate your acceptance of this License to do so.
+
+ 10. Automatic Licensing of Downstream Recipients.
+
+ Each time you convey a covered work, the recipient automatically
+receives a license from the original licensors, to run, modify and
+propagate that work, subject to this License. You are not responsible
+for enforcing compliance by third parties with this License.
+
+ An "entity transaction" is a transaction transferring control of an
+organization, or substantially all assets of one, or subdividing an
+organization, or merging organizations. If propagation of a covered
+work results from an entity transaction, each party to that
+transaction who receives a copy of the work also receives whatever
+licenses to the work the party's predecessor in interest had or could
+give under the previous paragraph, plus a right to possession of the
+Corresponding Source of the work from the predecessor in interest, if
+the predecessor has it or can get it with reasonable efforts.
+
+ You may not impose any further restrictions on the exercise of the
+rights granted or affirmed under this License. For example, you may
+not impose a license fee, royalty, or other charge for exercise of
+rights granted under this License, and you may not initiate litigation
+(including a cross-claim or counterclaim in a lawsuit) alleging that
+any patent claim is infringed by making, using, selling, offering for
+sale, or importing the Program or any portion of it.
+
+ 11. Patents.
+
+ A "contributor" is a copyright holder who authorizes use under this
+License of the Program or a work on which the Program is based. The
+work thus licensed is called the contributor's "contributor version".
+
+ A contributor's "essential patent claims" are all patent claims
+owned or controlled by the contributor, whether already acquired or
+hereafter acquired, that would be infringed by some manner, permitted
+by this License, of making, using, or selling its contributor version,
+but do not include claims that would be infringed only as a
+consequence of further modification of the contributor version. For
+purposes of this definition, "control" includes the right to grant
+patent sublicenses in a manner consistent with the requirements of
+this License.
+
+ Each contributor grants you a non-exclusive, worldwide, royalty-free
+patent license under the contributor's essential patent claims, to
+make, use, sell, offer for sale, import and otherwise run, modify and
+propagate the contents of its contributor version.
+
+ In the following three paragraphs, a "patent license" is any express
+agreement or commitment, however denominated, not to enforce a patent
+(such as an express permission to practice a patent or covenant not to
+sue for patent infringement). To "grant" such a patent license to a
+party means to make such an agreement or commitment not to enforce a
+patent against the party.
+
+ If you convey a covered work, knowingly relying on a patent license,
+and the Corresponding Source of the work is not available for anyone
+to copy, free of charge and under the terms of this License, through a
+publicly available network server or other readily accessible means,
+then you must either (1) cause the Corresponding Source to be so
+available, or (2) arrange to deprive yourself of the benefit of the
+patent license for this particular work, or (3) arrange, in a manner
+consistent with the requirements of this License, to extend the patent
+license to downstream recipients. "Knowingly relying" means you have
+actual knowledge that, but for the patent license, your conveying the
+covered work in a country, or your recipient's use of the covered work
+in a country, would infringe one or more identifiable patents in that
+country that you have reason to believe are valid.
+
+ If, pursuant to or in connection with a single transaction or
+arrangement, you convey, or propagate by procuring conveyance of, a
+covered work, and grant a patent license to some of the parties
+receiving the covered work authorizing them to use, propagate, modify
+or convey a specific copy of the covered work, then the patent license
+you grant is automatically extended to all recipients of the covered
+work and works based on it.
+
+ A patent license is "discriminatory" if it does not include within
+the scope of its coverage, prohibits the exercise of, or is
+conditioned on the non-exercise of one or more of the rights that are
+specifically granted under this License. You may not convey a covered
+work if you are a party to an arrangement with a third party that is
+in the business of distributing software, under which you make payment
+to the third party based on the extent of your activity of conveying
+the work, and under which the third party grants, to any of the
+parties who would receive the covered work from you, a discriminatory
+patent license (a) in connection with copies of the covered work
+conveyed by you (or copies made from those copies), or (b) primarily
+for and in connection with specific products or compilations that
+contain the covered work, unless you entered into that arrangement,
+or that patent license was granted, prior to 28 March 2007.
+
+ Nothing in this License shall be construed as excluding or limiting
+any implied license or other defenses to infringement that may
+otherwise be available to you under applicable patent law.
+
+ 12. No Surrender of Others' Freedom.
+
+ If conditions are imposed on you (whether by court order, agreement or
+otherwise) that contradict the conditions of this License, they do not
+excuse you from the conditions of this License. If you cannot convey a
+covered work so as to satisfy simultaneously your obligations under this
+License and any other pertinent obligations, then as a consequence you may
+not convey it at all. For example, if you agree to terms that obligate you
+to collect a royalty for further conveying from those to whom you convey
+the Program, the only way you could satisfy both those terms and this
+License would be to refrain entirely from conveying the Program.
+
+ 13. Remote Network Interaction; Use with the GNU General Public License.
+
+ Notwithstanding any other provision of this License, if you modify the
+Program, your modified version must prominently offer all users
+interacting with it remotely through a computer network (if your version
+supports such interaction) an opportunity to receive the Corresponding
+Source of your version by providing access to the Corresponding Source
+from a network server at no charge, through some standard or customary
+means of facilitating copying of software. This Corresponding Source
+shall include the Corresponding Source for any work covered by version 3
+of the GNU General Public License that is incorporated pursuant to the
+following paragraph.
+
+ Notwithstanding any other provision of this License, you have
+permission to link or combine any covered work with a work licensed
+under version 3 of the GNU General Public License into a single
+combined work, and to convey the resulting work. The terms of this
+License will continue to apply to the part which is the covered work,
+but the work with which it is combined will remain governed by version
+3 of the GNU General Public License.
+
+ 14. Revised Versions of this License.
+
+ The Free Software Foundation may publish revised and/or new versions of
+the GNU Affero General Public License from time to time. Such new versions
+will be similar in spirit to the present version, but may differ in detail to
+address new problems or concerns.
+
+ Each version is given a distinguishing version number. If the
+Program specifies that a certain numbered version of the GNU Affero General
+Public License "or any later version" applies to it, you have the
+option of following the terms and conditions either of that numbered
+version or of any later version published by the Free Software
+Foundation. If the Program does not specify a version number of the
+GNU Affero General Public License, you may choose any version ever published
+by the Free Software Foundation.
+
+ If the Program specifies that a proxy can decide which future
+versions of the GNU Affero General Public License can be used, that proxy's
+public statement of acceptance of a version permanently authorizes you
+to choose that version for the Program.
+
+ Later license versions may give you additional or different
+permissions. However, no additional obligations are imposed on any
+author or copyright holder as a result of your choosing to follow a
+later version.
+
+ 15. Disclaimer of Warranty.
+
+ THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
+APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
+HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
+OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
+IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
+ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
+
+ 16. Limitation of Liability.
+
+ IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
+WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
+THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
+GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
+USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
+DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
+PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
+EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
+SUCH DAMAGES.
+
+ 17. Interpretation of Sections 15 and 16.
+
+ If the disclaimer of warranty and limitation of liability provided
+above cannot be given local legal effect according to their terms,
+reviewing courts shall apply local law that most closely approximates
+an absolute waiver of all civil liability in connection with the
+Program, unless a warranty or assumption of liability accompanies a
+copy of the Program in return for a fee.
+
+ END OF TERMS AND CONDITIONS
+
+ How to Apply These Terms to Your New Programs
+
+ If you develop a new program, and you want it to be of the greatest
+possible use to the public, the best way to achieve this is to make it
+free software which everyone can redistribute and change under these terms.
+
+ To do so, attach the following notices to the program. It is safest
+to attach them to the start of each source file to most effectively
+state the exclusion of warranty; and each file should have at least
+the "copyright" line and a pointer to where the full notice is found.
+
+ <one line to give the program's name and a brief idea of what it does.>
+ Copyright (C) <year> <name of author>
+
+ 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/>.
+
+Also add information on how to contact you by electronic and paper mail.
+
+ If your software can interact with users remotely through a computer
+network, you should also make sure that it provides a way for users to
+get its source. For example, if your program is a web application, its
+interface could display a "Source" link that leads users to an archive
+of the code. There are many ways you could offer source, and different
+solutions will be better for different programs; see section 13 for the
+specific requirements.
+
+ You should also get your employer (if you work as a programmer) or school,
+if any, to sign a "copyright disclaimer" for the program, if necessary.
+For more information on this, and how to apply and follow the GNU AGPL, see
+<http://www.gnu.org/licenses/>.
diff --git a/plugins/owncloud/config.inc.php.dist b/plugins/owncloud/config.inc.php.dist
new file mode 100644
index 00000000..c9e9d231
--- /dev/null
+++ b/plugins/owncloud/config.inc.php.dist
@@ -0,0 +1,4 @@
+<?php
+
+// ownCloud URL
+$rcmail_config['owncloud_url'] = 'https://owncloud.webmail.tld';
diff --git a/plugins/owncloud/localization/en_US.inc b/plugins/owncloud/localization/en_US.inc
new file mode 100644
index 00000000..f5cb8fb8
--- /dev/null
+++ b/plugins/owncloud/localization/en_US.inc
@@ -0,0 +1,6 @@
+<?php
+
+$labels = array();
+$labels['owncloud'] = 'Files';
+
+?>
diff --git a/plugins/owncloud/localization/pl_PL.inc b/plugins/owncloud/localization/pl_PL.inc
new file mode 100644
index 00000000..4b84bd46
--- /dev/null
+++ b/plugins/owncloud/localization/pl_PL.inc
@@ -0,0 +1,6 @@
+<?php
+
+$labels = array();
+$labels['owncloud'] = 'Pliki';
+
+?>
diff --git a/plugins/owncloud/owncloud.php b/plugins/owncloud/owncloud.php
new file mode 100644
index 00000000..d847cc3d
--- /dev/null
+++ b/plugins/owncloud/owncloud.php
@@ -0,0 +1,84 @@
+<?php
+
+/**
+ * OwnCloud Plugin
+ *
+ * @author Aleksander 'A.L.E.C' Machniak <machniak@kolabsys.com>
+ * @licence GNU AGPL
+ *
+ * Configuration (see config.inc.php.dist)
+ *
+ */
+
+class owncloud extends rcube_plugin
+{
+ // all task excluding 'login' and 'logout'
+ public $task = '?(?!login|logout).*';
+ // we've got no ajax handlers
+ public $noajax = true;
+ // skip frames
+ public $noframe = true;
+
+ function init()
+ {
+ $rcmail = rcmail::get_instance();
+
+ // requires kolab_auth plugin
+ if (empty($_SESSION['kolab_uid'])) {
+ return;
+ }
+
+ $this->add_texts('localization/', false);
+
+ // register task
+ $this->register_task('owncloud');
+
+ // register actions
+ $this->register_action('index', array($this, 'action'));
+ $this->register_action('redirect', array($this, 'redirect'));
+
+ // add taskbar button
+ $this->add_button(array(
+ 'command' => 'owncloud',
+ 'class' => 'button-owncloud',
+ 'classsel' => 'button-owncloud button-selected',
+ 'innerclass' => 'button-inner',
+ 'label' => 'owncloud.owncloud',
+ ), 'taskbar');
+
+ $skin = $rcmail->config->get('skin');
+ if (!file_exists($this->home."/skins/$skin/owncloud.css")) {
+ $skin = 'default';
+ }
+
+ // add style for taskbar button (must be here) and Help UI
+ $this->include_stylesheet("skins/$skin/owncloud.css");
+ }
+
+ function action()
+ {
+ $rcmail = rcmail::get_instance();
+
+ $rcmail->output->add_handlers(array('owncloudframe' => array($this, 'frame')));
+ $rcmail->output->set_pagetitle($this->gettext('owncloud'));
+ $rcmail->output->send('owncloud.owncloud');
+ }
+
+ function frame()
+ {
+ $rcmail = rcmail::get_instance();
+
+ $this->load_config();
+
+ $src = $rcmail->config->get('owncloud_url');
+ $user = $_SESSION['kolab_uid']; // requires kolab_auth plugin
+ $pass = $rcmail->decrypt($_SESSION['password']);
+
+ $src = preg_replace('/^(https?:\/\/)/',
+ '\\1' . urlencode($user) . ':' . urlencode($pass) . '@', $src);
+
+ return '<iframe id="owncloudframe" width="100%" height="100%" frameborder="0"'
+ .' src="' . $src. '"></iframe>';
+ }
+
+}
diff --git a/plugins/owncloud/skins/default/cloud.png b/plugins/owncloud/skins/default/cloud.png
new file mode 100644
index 00000000..15a288f7
Binary files /dev/null and b/plugins/owncloud/skins/default/cloud.png differ
diff --git a/plugins/owncloud/skins/default/owncloud.css b/plugins/owncloud/skins/default/owncloud.css
new file mode 100644
index 00000000..920b730e
--- /dev/null
+++ b/plugins/owncloud/skins/default/owncloud.css
@@ -0,0 +1,12 @@
+/***** ownCloud plugin styles *****/
+
+#taskbar a.button-owncloud
+{
+ background-image: url(cloud.png);
+}
+
+.owncloud-box
+{
+ overflow: auto;
+ background-color: #F2F2F2;
+}
diff --git a/plugins/owncloud/skins/default/templates/owncloud.html b/plugins/owncloud/skins/default/templates/owncloud.html
new file mode 100644
index 00000000..efbd3628
--- /dev/null
+++ b/plugins/owncloud/skins/default/templates/owncloud.html
@@ -0,0 +1,19 @@
+<!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" />
+<link rel="stylesheet" type="text/css" href="/this/owncloud.css" />
+<link rel="stylesheet" type="text/css" href="/settings.css" />
+</head>
+<body>
+
+<roundcube:include file="/includes/taskbar.html" />
+<roundcube:include file="/includes/header.html" />
+
+<div id="mainscreen" class="box owncloud-box">
+ <roundcube:object name="owncloudframe" />
+</div>
+
+</body>
+</html>
diff --git a/plugins/owncloud/skins/larry/cloud.png b/plugins/owncloud/skins/larry/cloud.png
new file mode 100644
index 00000000..7ad3cd96
Binary files /dev/null and b/plugins/owncloud/skins/larry/cloud.png differ
diff --git a/plugins/owncloud/skins/larry/owncloud.css b/plugins/owncloud/skins/larry/owncloud.css
new file mode 100644
index 00000000..1e9aa891
--- /dev/null
+++ b/plugins/owncloud/skins/larry/owncloud.css
@@ -0,0 +1,14 @@
+/***** ownCloud plugin styles *****/
+
+#taskbar a.button-owncloud span.button-inner
+{
+ background: url(cloud.png) 5px 5px no-repeat;
+ height: 14px;
+}
+
+#taskbar a.button-owncloud:hover span.button-inner,
+#taskbar a.button-owncloud.button-selected span.button-inner
+{
+ background: url(cloud.png) 5px -16px no-repeat;
+ height: 14px;
+}
diff --git a/plugins/owncloud/skins/larry/templates/owncloud.html b/plugins/owncloud/skins/larry/templates/owncloud.html
new file mode 100644
index 00000000..8b3f470a
--- /dev/null
+++ b/plugins/owncloud/skins/larry/templates/owncloud.html
@@ -0,0 +1,20 @@
+<roundcube:object name="doctype" value="html5" />
+<html>
+<head>
+<title><roundcube:object name="pagetitle" /></title>
+<roundcube:include file="/includes/links.html" />
+<link rel="stylesheet" type="text/css" href="/this/owncloud.css" />
+<link rel="stylesheet" type="text/css" href="/settings.css" />
+</head>
+<body class="noscroll">
+
+<roundcube:include file="/includes/header.html" />
+
+<div id="mainscreen" class="offset uibox" style="overflow: hidden">
+ <roundcube:object name="owncloudframe" />
+</div>
+
+<roundcube:include file="/includes/footer.html" />
+
+</body>
+</html>

File Metadata

Mime Type
text/x-diff
Expires
Mon, Aug 17, 11:11 PM (1 d, 16 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1257511
Default Alt Text
(882 KB)

Event Timeline