Page MenuHomePhorge

No OneTemporary

Size
1 MB
Referenced Files
None
Subscribers
None
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/plugins/calendar/.gitignore b/plugins/calendar/.gitignore
index 93262bcd..7c2f14c7 100644
--- a/plugins/calendar/.gitignore
+++ b/plugins/calendar/.gitignore
@@ -1,7 +1,8 @@
*.swp
*.bak
*.old
*~
config.inc.php
skins/*
-!skins/default
\ No newline at end of file
+!skins/default
+!skins/larry
\ No newline at end of file
diff --git a/plugins/calendar/README b/plugins/calendar/README
new file mode 100644
index 00000000..e5a38ee1
--- /dev/null
+++ b/plugins/calendar/README
@@ -0,0 +1,18 @@
+A calendar module for Roundcube
+-------------------------------
+
+This plugin currently supports a local database as well as a Kolab groupware
+server as backends for calendar and event storage. For both drivers, some
+initialization of the local database is necessary. To do so, execute the
+SQL commands in drivers/<yourchoice>/SQL/<yourdatabase>.sql
+
+The client-side calendar UI relies on the 'fullcalenda'r project by Adam Arshaw
+with extensions made for the use in Roundcube. All changes are published in
+an official fork at https://github.com/roundcube/fullcalendar
+
+For recurring event computation, some utility classes from the Horde project
+are used. They are packaged in a slightly modified version with this plugin.
+
+iCalendar parsing is done with the help of the Horde_iCalendar class. A copy
+of that class with all its dependencies is part of this package. In order
+to update it, execute lib/get_horde_icalendar.sh > lib/Horde_iCalendar.php
diff --git a/plugins/calendar/TODO b/plugins/calendar/TODO
index aefb61d3..b1a08d7e 100644
--- a/plugins/calendar/TODO
+++ b/plugins/calendar/TODO
@@ -1,47 +1,48 @@
+ Edit: 3.12: Subject
+ Edit: 3.13: Location
+ Edit: 3.14: Start / End / All Day
+ Edit: 3.15: Show time as: Busy, Free, Out of office
+ Edit: 3.16: Reminder set
+ Edit: 3.17: Priority: High/Low
+ Edit: 3.18: Recurrence (in line with Kontact)
+ Edit: 3.19: Attachment Upload
+ Edit: 3.20: Print
+ Add/Manage Attendees
+ Edit: 3.21: Required / Optional / Resource specification
+ Edit: 3.22: Conflict Handling (Free/Busy Check for attendees)
+ View: 3.3: Display modes (agenda / day / week / month)
+ Day / Week / Month
+ List (Agenda) view
- - Add selection for date range
+ + Add selection for date range
- Individual days selection
+ Show list of calendars in a (hideable) drawer
+ View: 3.1: Folder list
+ View: 3.2: Add / Remove / Rename / Share Folders
+ View: 3.6: Combined calendar view (Turn calendars on/off)
+ View: 3.7: Small month overview calendar
+ View: 3.5: Search
- Filter by categories (similar to mail)
+ View: 3.9: Alter event with drag/drop
+ Option: 4.12: Set default reminder time
+ Option: 3.23: Specify folder for new event (prefs)
-- Option: Set date/time format in prefs
++ Option: Set date/time format in prefs
+ Receive: 1.20: Invitation handling
- Jump to calendar view from mail ("Show event")
- Allow to re-send invitations
- Implement iTIP delegation
-- View: 3.4: Fish-Eye View For Busy Days
++ View: 3.4: Fish-Eye View For Busy Days
+ View: 3.8: Color according to calendar and category (similar to Kontact)
+ Support for multiple calendars (replace categories)
+ Allow user to create/edit/delete calendars
+ Colors for calendars should be user-configurable
+ ICS parser/generator (http://code.google.com/p/qcal/)
+- Script to send event alarms by email (in cronjob)
- Export *with* attachments
-- Importing ICS files (upload, drag & drop)
- Remember last visited view
- Create/manage invdividual views
-- Support for tasks/todos with task list view (ordered by date/time)
++ Importing ICS files (upload, drag & drop)
+
diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php
index 312a51ed..e7ecd2ec 100644
--- a/plugins/calendar/calendar.php
+++ b/plugins/calendar/calendar.php
@@ -1,2424 +1,2477 @@
<?php
/**
* Calendar plugin for Roundcube webmail
*
* @version @package_version@
* @author Lazlo Westerhof <hello@lazlo.me>
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
* Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class calendar extends rcube_plugin
{
const FREEBUSY_UNKNOWN = 0;
const FREEBUSY_FREE = 1;
const FREEBUSY_BUSY = 2;
const FREEBUSY_TENTATIVE = 3;
const FREEBUSY_OOF = 4;
public $task = '?(?!logout).*';
public $rc;
public $driver;
public $home; // declare public to be used in other classes
public $urlbase;
public $timezone;
public $timezone_offset;
public $gmt_offset;
public $ical;
public $ui;
public $defaults = array(
'calendar_default_view' => "agendaWeek",
'calendar_date_format' => "yyyy-MM-dd",
'calendar_date_short' => "M-d",
'calendar_date_long' => "MMM d yyyy",
'calendar_date_agenda' => "ddd MM-dd",
'calendar_time_format' => "HH:mm",
'calendar_timeslots' => 2,
'calendar_first_day' => 1,
'calendar_first_hour' => 6,
'calendar_work_start' => 6,
'calendar_work_end' => 18,
'calendar_agenda_range' => 60,
'calendar_agenda_sections' => 'smart',
'calendar_event_coloring' => 0,
'calendar_time_indicator' => true,
'calendar_date_format_sets' => array(
'yyyy-MM-dd' => array('MMM d yyyy', 'M-d', 'ddd MM-dd'),
'dd-MM-yyyy' => array('d MMM yyyy', 'd-M', 'ddd dd-MM'),
'yyyy/MM/dd' => array('MMM d yyyy', 'M/d', 'ddd MM/dd'),
'MM/dd/yyyy' => array('MMM d yyyy', 'M/d', 'ddd MM/dd'),
'dd/MM/yyyy' => array('d MMM yyyy', 'd/M', 'ddd dd/MM'),
'dd.MM.yyyy' => array('dd. MMM yyyy', 'd.M', 'ddd dd.MM.'),
'd.M.yyyy' => array('d. MMM yyyy', 'd.M', 'ddd d.MM.'),
),
);
private $default_categories = array(
'Personal' => 'c0c0c0',
'Work' => 'ff0000',
'Family' => '00ff00',
'Holiday' => 'ff6600',
);
private $ics_parts = array();
/**
* Plugin initialization.
*/
function init()
{
$this->rc = rcmail::get_instance();
$this->register_task('calendar', 'calendar');
// load calendar configuration
$this->load_config();
// load localizations
$this->add_texts('localization/', $this->rc->task == 'calendar' && (!$this->rc->action || $this->rc->action == 'print'));
// set user's timezone
$this->timezone = new DateTimeZone($this->rc->config->get('timezone', 'GMT'));
$now = new DateTime('now', $this->timezone);
- $this->timezone_offset = $now->format('Z') / 3600;
- $this->dst_active = $now->format('I');
$this->gmt_offset = $now->getOffset();
+ $this->dst_active = $now->format('I');
+ $this->timezone_offset = $this->gmt_offset / 3600 - $this->dst_active;
require($this->home . '/lib/calendar_ui.php');
$this->ui = new calendar_ui($this);
// load Calendar user interface which includes jquery-ui
if (!$this->rc->output->ajax_call && !$this->rc->output->env['framed']) {
$this->require_plugin('jqueryui');
$this->ui->init();
// settings are required in (almost) every GUI step
if ($this->rc->action != 'attend')
$this->rc->output->set_env('calendar_settings', $this->load_settings());
}
// catch iTIP confirmation requests that don're require a valid session
if ($this->rc->action == 'attend' && !empty($_REQUEST['_t'])) {
$this->add_hook('startup', array($this, 'itip_attend_response'));
}
else if ($this->rc->action == 'feed' && !empty($_REQUEST['_cal'])) {
$this->add_hook('startup', array($this, 'ical_feed_export'));
}
else if ($this->rc->task == 'calendar' && $this->rc->action != 'save-pref') {
if ($this->rc->action != 'upload') {
$this->load_driver();
}
// register calendar actions
$this->register_action('index', array($this, 'calendar_view'));
$this->register_action('event', array($this, 'event_action'));
$this->register_action('calendar', array($this, 'calendar_action'));
$this->register_action('load_events', array($this, 'load_events'));
$this->register_action('export_events', array($this, 'export_events'));
$this->register_action('import_events', array($this, 'import_events'));
$this->register_action('upload', array($this, 'attachment_upload'));
$this->register_action('get-attachment', array($this, 'attachment_get'));
$this->register_action('freebusy-status', array($this, 'freebusy_status'));
$this->register_action('freebusy-times', array($this, 'freebusy_times'));
$this->register_action('randomdata', array($this, 'generate_randomdata'));
$this->register_action('print', array($this,'print_view'));
$this->register_action('mailimportevent', array($this, 'mail_import_event'));
$this->register_action('mailtoevent', array($this, 'mail_message2event'));
$this->register_action('inlineui', array($this, 'get_inline_ui'));
$this->register_action('check-recent', array($this, 'check_recent'));
// remove undo information...
if ($undo = $_SESSION['calendar_event_undo']) {
// ...after timeout
$undo_time = $this->rc->config->get('undo_timeout', 0);
if ($undo['ts'] < time() - $undo_time) {
$this->rc->session->remove('calendar_event_undo');
// @TODO: do EXPUNGE on kolab objects?
}
}
}
else if ($this->rc->task == 'settings') {
// add hooks for Calendar settings
$this->add_hook('preferences_sections_list', array($this, 'preferences_sections_list'));
$this->add_hook('preferences_list', array($this, 'preferences_list'));
$this->add_hook('preferences_save', array($this, 'preferences_save'));
}
else if ($this->rc->task == 'mail') {
// hooks to catch event invitations on incoming mails
if ($this->rc->action == 'show' || $this->rc->action == 'preview') {
$this->add_hook('message_load', array($this, 'mail_message_load'));
$this->add_hook('template_object_messagebody', array($this, 'mail_messagebody_html'));
}
// add 'Create event' item to message menu
if ($this->api->output->type == 'html') {
$this->api->add_content(html::tag('li', null,
$this->api->output->button(array(
'command' => 'calendar-create-from-mail',
'label' => 'calendar.createfrommail',
'type' => 'link',
'classact' => 'icon calendarlink active',
'class' => 'icon calendarlink',
'innerclass' => 'icon calendar',
))),
'messagemenu');
}
}
// add hook to display alarms
$this->add_hook('keep_alive', array($this, 'keep_alive'));
}
/**
* Helper method to load the backend driver according to local config
*/
private function load_driver()
{
if (is_object($this->driver))
return;
$driver_name = $this->rc->config->get('calendar_driver', 'database');
$driver_class = $driver_name . '_driver';
require_once($this->home . '/drivers/calendar_driver.php');
require_once($this->home . '/drivers/' . $driver_name . '/' . $driver_class . '.php');
switch ($driver_name) {
case "kolab":
- $this->require_plugin('kolab_core');
+ $this->require_plugin('libkolab');
default:
$this->driver = new $driver_class($this);
break;
}
}
/**
* Load iTIP functions
*/
private function load_itip()
{
if (!$this->itip) {
require_once($this->home . '/lib/calendar_itip.php');
$this->itip = new calendar_itip($this);
}
return $this->itip;
}
/**
* Load iCalendar functions
*/
public function get_ical()
{
if (!$this->ical) {
require_once($this->home . '/lib/calendar_ical.php');
$this->ical = new calendar_ical($this);
}
return $this->ical;
}
/**
*
*/
public function get_default_calendar($writeable = false)
{
$cal_id = $this->rc->config->get('calendar_default_calendar');
$calendars = $this->driver->list_calendars();
$calendar = $calendars[$cal_id] ? $calendars[$cal_id] : null;
if (!$calendar || ($writeable && $calendar['readonly'])) {
foreach ($calendars as $cal) {
if (!$writeable || !$cal['readonly']) {
$calendar = $cal;
break;
}
}
}
return $calendar;
}
/**
* Render the main calendar view from skin template
*/
function calendar_view()
{
$this->rc->output->set_pagetitle($this->gettext('calendar'));
// Add CSS stylesheets to the page header
$this->ui->addCSS();
// Add JS files to the page header
$this->ui->addJS();
$this->ui->init_templates();
$this->rc->output->add_label('lowest','low','normal','high','highest','delete','cancel','uploading','noemailwarning');
// initialize attendees autocompletion
rcube_autocomplete_init();
$this->rc->output->set_env('calendar_driver', $this->rc->config->get('calendar_driver'), false);
$this->rc->output->set_env('mscolors', $this->driver->get_color_values());
$view = get_input_value('view', RCUBE_INPUT_GPC);
if (in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table')))
$this->rc->output->set_env('view', $view);
if ($date = get_input_value('date', RCUBE_INPUT_GPC))
$this->rc->output->set_env('date', $date);
$this->rc->output->send("calendar.calendar");
}
/**
* Handler for preferences_sections_list hook.
* Adds Calendar settings sections into preferences sections list.
*
* @param array Original parameters
* @return array Modified parameters
*/
function preferences_sections_list($p)
{
$p['list']['calendar'] = array(
'id' => 'calendar', 'section' => $this->gettext('calendar'),
);
return $p;
}
/**
* Handler for preferences_list hook.
* Adds options blocks into Calendar settings sections in Preferences.
*
* @param array Original parameters
* @return array Modified parameters
*/
function preferences_list($p)
{
if ($p['section'] == 'calendar') {
$this->load_driver();
$p['blocks']['view']['name'] = $this->gettext('mainoptions');
$field_id = 'rcmfd_default_view';
$select = new html_select(array('name' => '_default_view', 'id' => $field_id));
$select->add($this->gettext('day'), "agendaDay");
$select->add($this->gettext('week'), "agendaWeek");
$select->add($this->gettext('month'), "month");
$select->add($this->gettext('agenda'), "table");
$p['blocks']['view']['options']['default_view'] = array(
'title' => html::label($field_id, Q($this->gettext('default_view'))),
'content' => $select->show($this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view'])),
);
$field_id = 'rcmfd_timeslot';
$choices = array('1', '2', '3', '4', '6');
$select = new html_select(array('name' => '_timeslots', 'id' => $field_id));
$select->add($choices);
$p['blocks']['view']['options']['timeslots'] = array(
'title' => html::label($field_id, Q($this->gettext('timeslots'))),
'content' => $select->show($this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots'])),
);
$field_id = 'rcmfd_firstday';
$select = new html_select(array('name' => '_first_day', 'id' => $field_id));
$select->add(rcube_label('sunday'), '0');
$select->add(rcube_label('monday'), '1');
$select->add(rcube_label('tuesday'), '2');
$select->add(rcube_label('wednesday'), '3');
$select->add(rcube_label('thursday'), '4');
$select->add(rcube_label('friday'), '5');
$select->add(rcube_label('saturday'), '6');
$p['blocks']['view']['options']['first_day'] = array(
'title' => html::label($field_id, Q($this->gettext('first_day'))),
'content' => $select->show($this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day'])),
);
$time_format = $this->rc->config->get('time_format', self::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format'])));
$select_hours = new html_select();
for ($h = 0; $h < 24; $h++)
$select_hours->add(date($time_format, mktime($h, 0, 0)), $h);
$field_id = 'rcmfd_firsthour';
$p['blocks']['view']['options']['first_hour'] = array(
'title' => html::label($field_id, Q($this->gettext('first_hour'))),
'content' => $select_hours->show($this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']), array('name' => '_first_hour', 'id' => $field_id)),
);
$field_id = 'rcmfd_workstart';
$p['blocks']['view']['options']['workinghours'] = array(
'title' => html::label($field_id, Q($this->gettext('workinghours'))),
'content' => $select_hours->show($this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']), array('name' => '_work_start', 'id' => $field_id)) .
' &mdash; ' . $select_hours->show($this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']), array('name' => '_work_end', 'id' => $field_id)),
);
$field_id = 'rcmfd_coloing';
$select_colors = new html_select(array('name' => '_event_coloring', 'id' => $field_id));
$select_colors->add($this->gettext('coloringmode0'), 0);
$select_colors->add($this->gettext('coloringmode1'), 1);
$select_colors->add($this->gettext('coloringmode2'), 2);
$select_colors->add($this->gettext('coloringmode3'), 3);
$p['blocks']['view']['options']['eventcolors'] = array(
'title' => html::label($field_id . 'value', Q($this->gettext('eventcoloring'))),
'content' => $select_colors->show($this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring'])),
);
$field_id = 'rcmfd_alarm';
$select_type = new html_select(array('name' => '_alarm_type', 'id' => $field_id));
$select_type->add($this->gettext('none'), '');
foreach ($this->driver->alarm_types as $type)
$select_type->add($this->gettext(strtolower("alarm{$type}option")), $type);
$input_value = new html_inputfield(array('name' => '_alarm_value', 'id' => $field_id . 'value', 'size' => 3));
$select_offset = new html_select(array('name' => '_alarm_offset', 'id' => $field_id . 'offset'));
foreach (array('-M','-H','-D','+M','+H','+D') as $trigger)
$select_offset->add($this->gettext('trigger' . $trigger), $trigger);
$p['blocks']['view']['options']['alarmtype'] = array(
'title' => html::label($field_id, Q($this->gettext('defaultalarmtype'))),
'content' => $select_type->show($this->rc->config->get('calendar_default_alarm_type', '')),
);
$preset = self::parse_alaram_value($this->rc->config->get('calendar_default_alarm_offset', '-15M'));
$p['blocks']['view']['options']['alarmoffset'] = array(
'title' => html::label($field_id . 'value', Q($this->gettext('defaultalarmoffset'))),
'content' => $input_value->show($preset[0]) . ' ' . $select_offset->show($preset[1]),
);
// default calendar selection
$field_id = 'rcmfd_default_calendar';
$select_cal = new html_select(array('name' => '_default_calendar', 'id' => $field_id));
foreach ((array)$this->driver->list_calendars() as $id => $prop) {
if (!$prop['readonly'])
$select_cal->add($prop['name'], strval($id));
}
$p['blocks']['view']['options']['defaultcalendar'] = array(
'title' => html::label($field_id . 'value', Q($this->gettext('defaultcalendar'))),
'content' => $select_cal->show($this->rc->config->get('calendar_default_calendar', '')),
);
// category definitions
if (!$this->driver->nocategories) {
$p['blocks']['categories']['name'] = $this->gettext('categories');
$categories = (array) $this->driver->list_categories();
$categories_list = '';
foreach ($categories as $name => $color) {
$key = md5($name);
$field_class = 'rcmfd_category_' . str_replace(' ', '_', $name);
$category_remove = new html_inputfield(array('type' => 'button', 'value' => 'X', 'class' => 'button', 'onclick' => '$(this).parent().remove()', 'title' => $this->gettext('remove_category')));
$category_name = new html_inputfield(array('name' => "_categories[$key]", 'class' => $field_class, 'size' => 30, 'disabled' => $this->driver->categoriesimmutable));
$category_color = new html_inputfield(array('name' => "_colors[$key]", 'class' => "$field_class colors", 'size' => 6));
$hidden = $this->driver->categoriesimmutable ? html::tag('input', array('type' => 'hidden', 'name' => "_categories[$key]", 'value' => $name)) : '';
$categories_list .= html::div(null, $hidden . $category_name->show($name) . '&nbsp;' . $category_color->show($color) . '&nbsp;' . $category_remove->show());
}
$p['blocks']['categories']['options']['category_' . $name] = array(
'content' => html::div(array('id' => 'calendarcategories'), $categories_list),
);
$field_id = 'rcmfd_new_category';
$new_category = new html_inputfield(array('name' => '_new_category', 'id' => $field_id, 'size' => 30));
$add_category = new html_inputfield(array('type' => 'button', 'class' => 'button', 'value' => $this->gettext('add_category'), 'onclick' => "rcube_calendar_add_category()"));
$p['blocks']['categories']['options']['categories'] = array(
'content' => $new_category->show('') . '&nbsp;' . $add_category->show(),
);
$this->rc->output->add_script('function rcube_calendar_add_category(){
var name = $("#rcmfd_new_category").val();
if (name.length) {
var input = $("<input>").attr("type", "text").attr("name", "_categories[]").attr("size", 30).val(name);
var color = $("<input>").attr("type", "text").attr("name", "_colors[]").attr("size", 6).addClass("colors").val("000000");
var button = $("<input>").attr("type", "button").attr("value", "X").addClass("button").click(function(){ $(this).parent().remove() });
$("<div>").append(input).append("&nbsp;").append(color).append("&nbsp;").append(button).appendTo("#calendarcategories");
color.miniColors({ colorValues:mscolors });
}
}');
// include color picker
$this->include_script('lib/js/jquery.miniColors.min.js');
$this->include_stylesheet('skins/' .$this->rc->config->get('skin') . '/jquery.miniColors.css');
$this->rc->output->set_env('mscolors', $this->driver->get_color_values());
$this->rc->output->add_script('$("input.colors").miniColors({ colorValues:rcmail.env.mscolors })', 'docready');
}
}
return $p;
}
/**
* Handler for preferences_save hook.
* Executed on Calendar settings form submit.
*
* @param array Original parameters
* @return array Modified parameters
*/
function preferences_save($p)
{
if ($p['section'] == 'calendar') {
$this->load_driver();
// compose default alarm preset value
$alarm_offset = get_input_value('_alarm_offset', RCUBE_INPUT_POST);
$default_alam = $alarm_offset[0] . intval(get_input_value('_alarm_value', RCUBE_INPUT_POST)) . $alarm_offset[1];
$p['prefs'] = array(
'calendar_default_view' => get_input_value('_default_view', RCUBE_INPUT_POST),
'calendar_timeslots' => get_input_value('_timeslots', RCUBE_INPUT_POST),
'calendar_first_day' => get_input_value('_first_day', RCUBE_INPUT_POST),
'calendar_first_hour' => intval(get_input_value('_first_hour', RCUBE_INPUT_POST)),
'calendar_work_start' => intval(get_input_value('_work_start', RCUBE_INPUT_POST)),
'calendar_work_end' => intval(get_input_value('_work_end', RCUBE_INPUT_POST)),
'calendar_event_coloring' => intval(get_input_value('_event_coloring', RCUBE_INPUT_POST)),
'calendar_default_alarm_type' => get_input_value('_alarm_type', RCUBE_INPUT_POST),
'calendar_default_alarm_offset' => $default_alam,
'calendar_default_calendar' => get_input_value('_default_calendar', RCUBE_INPUT_POST),
'calendar_date_format' => null, // clear previously saved values
'calendar_time_format' => null,
);
// categories
if (!$this->driver->nocategories) {
$old_categories = $new_categories = array();
foreach ($this->driver->list_categories() as $name => $color) {
$old_categories[md5($name)] = $name;
}
$categories = get_input_value('_categories', RCUBE_INPUT_POST);
$colors = get_input_value('_colors', RCUBE_INPUT_POST);
foreach ($categories as $key => $name) {
$color = preg_replace('/^#/', '', strval($colors[$key]));
// rename categories in existing events -> driver's job
if ($oldname = $old_categories[$key]) {
$this->driver->replace_category($oldname, $name, $color);
unset($old_categories[$key]);
}
else
$this->driver->add_category($name, $color);
$new_categories[$name] = $color;
}
// these old categories have been removed, alter events accordingly -> driver's job
foreach ((array)$old_categories[$key] as $key => $name) {
$this->driver->remove_category($name);
}
$p['prefs']['calendar_categories'] = $new_categories;
}
}
return $p;
}
/**
* Dispatcher for calendar actions initiated by the client
*/
function calendar_action()
{
$action = get_input_value('action', RCUBE_INPUT_GPC);
$cal = get_input_value('c', RCUBE_INPUT_GPC);
$success = $reload = false;
if (isset($cal['showalarms']))
$cal['showalarms'] = intval($cal['showalarms']);
switch ($action) {
case "form-new":
case "form-edit":
echo $this->ui->calendar_editform($action, $cal);
exit;
case "new":
$success = $this->driver->create_calendar($cal);
$reload = true;
break;
case "edit":
$success = $this->driver->edit_calendar($cal);
$reload = true;
break;
case "remove":
if ($success = $this->driver->remove_calendar($cal))
$this->rc->output->command('plugin.destroy_source', array('id' => $cal['id']));
break;
case "subscribe":
if (!$this->driver->subscribe_calendar($cal))
$this->rc->output->show_message($this->gettext('errorsaving'), 'error');
return;
}
if ($success)
$this->rc->output->show_message('successfullysaved', 'confirmation');
else {
$error_msg = $this->gettext('errorsaving') . ($this->driver->last_error ? ': ' . $this->driver->last_error :'');
$this->rc->output->show_message($error_msg, 'error');
}
$this->rc->output->command('plugin.unlock_saving');
// TODO: keep view and date selection
if ($success && $reload)
$this->rc->output->redirect('');
}
/**
* Dispatcher for event actions initiated by the client
*/
function event_action()
{
$action = get_input_value('action', RCUBE_INPUT_GPC);
$event = get_input_value('e', RCUBE_INPUT_POST, true);
$success = $reload = $got_msg = false;
// don't notify if modifying a recurring instance (really?)
if ($event['_savemode'] && $event['_savemode'] != 'all' && $event['_notify'])
unset($event['_notify']);
// read old event data in order to find changes
if (($event['_notify'] || $event['decline']) && $action != 'new')
$old = $this->driver->get_event($event);
switch ($action) {
case "new":
// create UID for new event
$event['uid'] = $this->generate_uid();
$this->prepare_event($event, $action);
if ($success = $this->driver->new_event($event)) {
$event['id'] = $event['uid'];
$this->cleanup_event($event);
}
$reload = $success && $event['recurrence'] ? 2 : 1;
break;
case "edit":
$this->prepare_event($event, $action);
if ($success = $this->driver->edit_event($event))
$this->cleanup_event($event);
$reload = $success && ($event['recurrence'] || $event['_savemode'] || $event['_fromcalendar']) ? 2 : 1;
break;
case "resize":
$this->prepare_event($event, $action);
$success = $this->driver->resize_event($event);
$reload = $event['_savemode'] ? 2 : 1;
break;
case "move":
$this->prepare_event($event, $action);
$success = $this->driver->move_event($event);
$reload = $success && $event['_savemode'] ? 2 : 1;
break;
case "remove":
// remove previous deletes
$undo_time = $this->driver->undelete ? $this->rc->config->get('undo_timeout', 0) : 0;
$this->rc->session->remove('calendar_event_undo');
// search for event if only UID is given
if (!isset($event['calendar']) && $event['uid']) {
if (!($event = $this->driver->get_event($event, true))) {
break;
}
$undo_time = 0;
}
$success = $this->driver->remove_event($event, $undo_time < 1);
$reload = (!$success || $event['_savemode']) ? 2 : 1;
if ($undo_time > 0 && $success) {
$_SESSION['calendar_event_undo'] = array('ts' => time(), 'data' => $event);
// display message with Undo link.
$msg = html::span(null, $this->gettext('successremoval'))
. ' ' . html::a(array('onclick' => sprintf("%s.http_request('event', 'action=undo', %s.display_message('', 'loading'))",
JS_OBJECT_NAME, JS_OBJECT_NAME)), rcube_label('undo'));
$this->rc->output->show_message($msg, 'confirmation', null, true, $undo_time);
$got_msg = true;
}
else if ($success) {
$this->rc->output->show_message('calendar.successremoval', 'confirmation');
$got_msg = true;
}
// send iTIP reply that participant has declined the event
if ($success && $event['decline']) {
$emails = $this->get_user_emails();
foreach ($old['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER')
$organizer = $attendee;
else if ($attendee['email'] && in_array($attendee['email'], $emails)) {
$old['attendees'][$i]['status'] = 'DECLINED';
}
}
$itip = $this->load_itip();
if ($organizer && $itip->send_itip_message($old, 'REPLY', $organizer, 'itipsubjectdeclined', 'itipmailbodydeclined'))
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
break;
case "undo":
// Restore deleted event
$event = $_SESSION['calendar_event_undo']['data'];
if ($event)
$success = $this->driver->restore_event($event);
if ($success) {
$this->rc->session->remove('calendar_event_undo');
$this->rc->output->show_message('calendar.successrestore', 'confirmation');
$got_msg = true;
$reload = 2;
}
break;
case "rsvp-status":
$action = 'rsvp';
$status = $event['fallback'];
$html = html::div('rsvp-status', $status != 'CANCELLED' ? $this->gettext('acceptinvitation') : '');
$this->load_driver();
if ($existing = $this->driver->get_event($event, true)) {
$emails = $this->get_user_emails();
foreach ($existing['attendees'] as $i => $attendee) {
if ($attendee['email'] && in_array($attendee['email'], $emails)) {
$status = $attendee['status'];
break;
}
}
}
else {
// get a list of writeable calendars
$calendars = $this->driver->list_calendars();
$calendar_select = new html_select(array('name' => 'calendar', 'id' => 'calendar-saveto'));
$numcals = 0;
foreach ($calendars as $calendar) {
if (!$calendar['readonly']) {
$calendar_select->add($calendar['name'], $calendar['id']);
$numcals++;
}
}
if ($numcals <= 1)
$calendar_select = null;
}
if ($status == 'unknown') {
$html = html::div('rsvp-status', $this->gettext('notanattendee'));
$action = 'import';
}
else if (in_array($status, array('ACCEPTED','TENTATIVE','DECLINED'))) {
$html = html::div('rsvp-status ' . strtolower($status), $this->gettext('youhave'.strtolower($status)));
if ($existing['changed'] && $event['changed'] < $existing['changed']) {
$action = '';
}
}
$this->rc->output->command('plugin.update_event_rsvp_status', array(
'uid' => $event['uid'],
'id' => asciiwords($event['uid'], true),
'status' => $status,
'action' => $action,
'html' => $html,
'select' => $calendar_select ? html::span('calendar-select', $this->gettext('saveincalendar') . '&nbsp;' . $calendar_select->show($this->rc->config->get('calendar_default_calendar'))) : '',
));
return;
case "rsvp":
$ev = $this->driver->get_event($event);
$ev['attendees'] = $event['attendees'];
$event = $ev;
if ($success = $this->driver->edit_event($event)) {
$status = get_input_value('status', RCUBE_INPUT_GPC);
$organizer = null;
foreach ($event['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
$organizer = $attendee;
break;
}
}
$itip = $this->load_itip();
if ($organizer && $itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status))
$this->rc->output->command('display_message', $this->gettext(array('name' => 'sentresponseto', 'vars' => array('mailto' => $organizer['name'] ? $organizer['name'] : $organizer['email']))), 'confirmation');
else
$this->rc->output->command('display_message', $this->gettext('itipresponseerror'), 'error');
}
break;
case "dismiss":
foreach (explode(',', $event['id']) as $id)
$success |= $this->driver->dismiss_alarm($id, $event['snooze']);
break;
}
// send out notifications
if ($success && $event['_notify'] && ($event['attendees'] || $old['attendees'])) {
// make sure we have the complete record
$event = $action == 'remove' ? $old : $this->driver->get_event($event);
// only notify if data really changed (TODO: do diff check on client already)
if (!$old || $action == 'remove' || self::event_diff($event, $old)) {
if ($this->notify_attendees($event, $old, $action) < 0)
$this->rc->output->show_message('calendar.errornotifying', 'error');
}
}
// show confirmation/error message
if (!$got_msg) {
if ($success)
$this->rc->output->show_message('successfullysaved', 'confirmation');
else
$this->rc->output->show_message('calendar.errorsaving', 'error');
}
// unlock client
$this->rc->output->command('plugin.unlock_saving');
// update event object on the client or trigger a complete refretch if too complicated
if ($reload) {
$args = array('source' => $event['calendar']);
if ($reload > 1)
$args['refetch'] = true;
else if ($success && $action != 'remove')
$args['update'] = $this->_client_event($this->driver->get_event($event));
$this->rc->output->command('plugin.refresh_calendar', $args);
}
}
/**
* Handler for load-requests from fullcalendar
* This will return pure JSON formatted output
*/
function load_events()
{
$events = $this->driver->load_events(
get_input_value('start', RCUBE_INPUT_GET),
get_input_value('end', RCUBE_INPUT_GET),
($query = get_input_value('q', RCUBE_INPUT_GET)),
get_input_value('source', RCUBE_INPUT_GET)
);
echo $this->encode($events, !empty($query));
exit;
}
/**
* Handler for keep-alive requests
* This will check for pending notifications and pass them to the client
*/
function keep_alive($attr)
{
$this->load_driver();
$alarms = $this->driver->pending_alarms(time());
if ($alarms) {
// make sure texts and env vars are available on client
if ($this->rc->task != 'calendar') {
$this->add_texts('localization/', true);
$this->rc->output->set_env('snooze_select', $this->ui->snooze_select());
}
$this->rc->output->command('plugin.display_alarms', $this->_alarms_output($alarms));
}
}
/**
* Handler for check-recent requests which are accidentally sent to calendar taks
*/
function check_recent()
{
// NOP
$this->rc->output->send();
}
/**
*
*/
function import_events()
{
// Upload progress update
if (!empty($_GET['_progress'])) {
rcube_upload_progress();
}
$calendar = get_input_value('calendar', RCUBE_INPUT_GPC);
$uploadid = get_input_value('_uploadid', RCUBE_INPUT_GPC);
// process uploaded file if there is no error
$err = $_FILES['_data']['error'];
if (!$err && $_FILES['_data']['tmp_name']) {
$calendar = get_input_value('calendar', RCUBE_INPUT_GPC);
$events = $this->get_ical()->import_from_file($_FILES['_data']['tmp_name']);
$count = $errors = 0;
$rangestart = $_REQUEST['_range'] ? strtotime("now -" . intval($_REQUEST['_range']) . " months") : 0;
foreach ($events as $event) {
// TODO: correctly handle recurring events which start before $rangestart
if ($event['end'] < $rangestart && (!$event['recurrence'] || ($event['recurrence']['until'] && $event['recurrence']['until'] < $rangestart)))
continue;
$event['calendar'] = $calendar;
if ($success = $this->driver->new_event($event)) {
$count++;
}
else
$errors++;
}
if ($count) {
$this->rc->output->command('display_message', $this->gettext(array('name' => 'importsuccess', 'vars' => array('nr' => $count))), 'confirmation');
$this->rc->output->command('plugin.import_success', array('source' => $calendar, 'refetch' => true));
}
else if (!$errors) {
$this->rc->output->command('display_message', $this->gettext('importnone'), 'notice');
$this->rc->output->command('plugin.import_success', array('source' => $calendar));
}
else
$this->rc->output->command('display_message', $this->gettext('importerror'), 'error');
}
else {
if ($err == UPLOAD_ERR_INI_SIZE || $err == UPLOAD_ERR_FORM_SIZE) {
$msg = rcube_label(array('name' => 'filesizeerror', 'vars' => array(
'size' => show_bytes(parse_bytes(ini_get('upload_max_filesize'))))));
}
else {
$msg = rcube_label('fileuploaderror');
}
$this->rc->output->command('display_message', $msg, 'error');
$this->rc->output->command('plugin.unlock_saving', false);
}
$this->rc->output->send('iframe');
}
/**
* Construct the ics file for exporting events to iCalendar format;
*/
function export_events($terminate = true)
{
$start = get_input_value('start', RCUBE_INPUT_GET);
$end = get_input_value('end', RCUBE_INPUT_GET);
if (!$start) $start = mktime(0, 0, 0, 1, date('n'), date('Y')-1);
if (!$end) $end = mktime(0, 0, 0, 31, 12, date('Y')+10);
$calid = $calname = get_input_value('source', RCUBE_INPUT_GET);
$calendars = $this->driver->list_calendars();
if ($calendars[$calid]) {
$calname = $calendars[$calid]['name'] ? $calendars[$calid]['name'] : $calid;
$events = $this->driver->load_events($start, $end, null, $calid, 0);
}
else
$events = array();
header("Content-Type: text/calendar");
header("Content-Disposition: inline; filename=".$calname.'.ics');
$this->get_ical()->export($events, '', true);
if ($terminate)
exit;
}
/**
* Handler for iCal feed requests
*/
function ical_feed_export()
{
// process HTTP auth info
if (!empty($_SERVER['PHP_AUTH_USER']) && isset($_SERVER['PHP_AUTH_PW'])) {
$_POST['_user'] = $_SERVER['PHP_AUTH_USER']; // used for rcmail::autoselect_host()
$auth = $this->rc->plugins->exec_hook('authenticate', array(
'host' => $this->rc->autoselect_host(),
'user' => trim($_SERVER['PHP_AUTH_USER']),
'pass' => $_SERVER['PHP_AUTH_PW'],
'cookiecheck' => true,
'valid' => true,
));
if ($auth['valid'] && !$auth['abort'])
$this->rc->login($auth['user'], $auth['pass'], $auth['host']);
}
// require HTTP auth
if (empty($_SESSION['user_id'])) {
header('WWW-Authenticate: Basic realm="Roundcube Calendar"');
header('HTTP/1.0 401 Unauthorized');
exit;
}
// decode calendar feed hash
$format = 'ics';
$calhash = get_input_value('_cal', RCUBE_INPUT_GET);
if (preg_match(($suff_regex = '/\.([a-z0-9]{3,5})$/i'), $calhash, $m)) {
$format = strtolower($m[1]);
$calhash = preg_replace($suff_regex, '', $calhash);
}
if (!strpos($calhash, ':'))
$calhash = base64_decode($calhash);
list($user, $_GET['source']) = explode(':', $calhash, 2);
// sanity check user
if ($this->rc->user->get_username() == $user) {
$this->load_driver();
$this->export_events(false);
}
else {
header('HTTP/1.0 404 Not Found');
}
// don't save session data
session_destroy();
exit;
}
/**
*
*/
function load_settings()
{
$this->date_format_defaults();
$settings = array();
// configuration
$settings['default_calendar'] = $this->rc->config->get('calendar_default_calendar');
$settings['default_view'] = (string)$this->rc->config->get('calendar_default_view', $this->defaults['calendar_default_view']);
$settings['date_format'] = (string)$this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']);
$settings['time_format'] = (string)$this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format']);
$settings['date_short'] = (string)$this->rc->config->get('calendar_date_short', $this->defaults['calendar_date_short']);
$settings['date_long'] = (string)$this->rc->config->get('calendar_date_long', $this->defaults['calendar_date_long']);
$settings['dates_long'] = str_replace(' yyyy', '[ yyyy]', $settings['date_long']) . "{ '&mdash;' " . $settings['date_long'] . '}';
$settings['date_agenda'] = (string)$this->rc->config->get('calendar_date_agenda', $this->defaults['calendar_date_agenda']);
$settings['timeslots'] = (int)$this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']);
$settings['first_day'] = (int)$this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']);
$settings['first_hour'] = (int)$this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']);
$settings['work_start'] = (int)$this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']);
$settings['work_end'] = (int)$this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']);
$settings['agenda_range'] = (int)$this->rc->config->get('calendar_agenda_range', $this->defaults['calendar_agenda_range']);
$settings['agenda_sections'] = $this->rc->config->get('calendar_agenda_sections', $this->defaults['calendar_agenda_sections']);
$settings['event_coloring'] = (int)$this->rc->config->get('calendar_event_coloring', $this->defaults['calendar_event_coloring']);
$settings['time_indicator'] = (int)$this->rc->config->get('calendar_time_indicator', $this->defaults['calendar_time_indicator']);
$settings['timezone'] = $this->timezone_offset;
$settings['dst'] = $this->dst_active;
// localization
$settings['days'] = array(
rcube_label('sunday'), rcube_label('monday'),
rcube_label('tuesday'), rcube_label('wednesday'),
rcube_label('thursday'), rcube_label('friday'),
rcube_label('saturday')
);
$settings['days_short'] = array(
rcube_label('sun'), rcube_label('mon'),
rcube_label('tue'), rcube_label('wed'),
rcube_label('thu'), rcube_label('fri'),
rcube_label('sat')
);
$settings['months'] = array(
$this->rc->gettext('longjan'), $this->rc->gettext('longfeb'),
$this->rc->gettext('longmar'), $this->rc->gettext('longapr'),
$this->rc->gettext('longmay'), $this->rc->gettext('longjun'),
$this->rc->gettext('longjul'), $this->rc->gettext('longaug'),
$this->rc->gettext('longsep'), $this->rc->gettext('longoct'),
$this->rc->gettext('longnov'), $this->rc->gettext('longdec')
);
$settings['months_short'] = array(
$this->rc->gettext('jan'), $this->rc->gettext('feb'),
$this->rc->gettext('mar'), $this->rc->gettext('apr'),
$this->rc->gettext('may'), $this->rc->gettext('jun'),
$this->rc->gettext('jul'), $this->rc->gettext('aug'),
$this->rc->gettext('sep'), $this->rc->gettext('oct'),
$this->rc->gettext('nov'), $this->rc->gettext('dec')
);
$settings['today'] = $this->rc->gettext('today');
// get user identity to create default attendee
if ($this->ui->screen == 'calendar') {
foreach ($this->rc->user->list_identities() as $rec) {
if (!$identity)
$identity = $rec;
$identity['emails'][] = $rec['email'];
}
$identity['emails'][] = $this->rc->user->get_username();
$settings['identity'] = array('name' => $identity['name'], 'email' => $identity['email'], 'emails' => ';' . join(';', $identity['emails']));
}
+ // define list of file types which can be displayed inline
+ // same as in program/steps/mail/show.inc
+ $mimetypes = $this->rc->config->get('client_mimetypes', 'text/plain,text/html,text/xml,image/jpeg,image/gif,image/png,application/x-javascript,application/pdf,application/x-shockwave-flash');
+ $settings['mimetypes'] = is_string($mimetypes) ? explode(',', $mimetypes) : (array)$mimetypes;
+
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;
}
-
+
+ /**
+ * Get the next alarm (time & action) for the given event
+ *
+ * @param array Event data
+ * @return array Hash array with alarm time/type or null if no alarms are configured
+ */
+ public static function get_next_alarm($event)
+ {
+ if (!$event['alarms'])
+ return null;
+
+ // TODO: handle multiple alarms (currently not supported)
+ list($trigger, $action) = explode(':', $event['alarms'], 2);
+
+ $notify = self::parse_alaram_value($trigger);
+ if (!empty($notify[1])){ // offset
+ $mult = 1;
+ switch ($notify[1]) {
+ case '-S': $mult = -1; break;
+ case '+S': $mult = 1; break;
+ 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;
+ case '-W': $mult = -604800; break;
+ case '+W': $mult = 604800; break;
+ }
+ $offset = $notify[0] * $mult;
+ $refdate = $mult > 0 ? $event['end'] : $event['start'];
+ $notify_at = $refdate + $offset;
+ }
+ else { // absolute timestamp
+ $notify_at = $notify[0];
+ }
+
+ return array('time' => $notify_at, 'action' => $action ? strtoupper($action) : 'DISPLAY');
+ }
+
/**
* 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);
+ $val = join(',', (array)$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);
+ $attachment = $GLOBALS['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']);
+ // allow post-processing of the attachment body
+ $part = new rcube_message_part;
+ $part->filename = $attachment['name'];
+ $part->size = $attachment['size'];
+ $part->mimetype = $attachment['mimetype'];
+
+ $plugin = $this->rc->plugins->exec_hook('message_part_get', array(
+ 'body' => $this->driver->get_attachment_body($id, $event),
+ 'mimetype' => strtolower($attachment['mimetype']),
+ 'download' => !empty($_GET['_download']),
+ 'part' => $part,
+ ));
+
+ if ($plugin['abort'])
+ exit;
+
+ $mimetype = $plugin['mimetype'];
list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
$browser = $this->rc->output->browser;
// send download headers
- if ($_GET['_download']) {
+ if ($plugin['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);
+ $OUTPUT->write($plugin['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;
+ echo $plugin['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'];
+ $attachment = $GLOBALS['calendar_attachment'];
$mimetype = strtolower($attachment['mimetype']);
list($ctype_primary, $ctype_secondary) = explode('/', $mimetype);
$attrib['src'] = './?' . str_replace('_frame=', ($ctype_primary == 'text' ? '_show=' : '_preload='), $_SERVER['QUERY_STRING']);
return html::iframe($attrib);
}
/**
* Prepares new/edited event properties before save
*/
private function prepare_event(&$event, $action)
{
$attachments = array();
$eventid = 'cal:'.$event['id'];
if (is_array($_SESSION['event_session']) && $_SESSION['event_session']['id'] == $eventid) {
if (!empty($_SESSION['event_session']['attachments'])) {
foreach ($_SESSION['event_session']['attachments'] as $id => $attachment) {
if (is_array($event['attachments']) && in_array($id, $event['attachments'])) {
$attachments[$id] = $this->rc->plugins->exec_hook('attachment_get', $attachment);
}
}
}
}
$event['attachments'] = $attachments;
// check for organizer in attendees
if ($event['attendees'] && ($action == 'new' || $action == 'edit')) {
$emails = $this->get_user_emails();
$organizer = $owner = false;
foreach ($event['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER')
$organizer = true;
if ($attendee['email'] == in_array($attendee['email'], $emails))
$owner = $i;
else if (!isset($attendee['rsvp']))
$event['attendees'][$i]['rsvp'] = true;
}
// set owner as organizer if yet missing
if (!$organizer && $owner !== false) {
$event['attendees'][$owner]['role'] = 'ORGANIZER';
unset($event['attendees'][$owner]['rsvp']);
}
else if (!$organizer && $action == 'new' && ($identity = $this->rc->user->get_identity()) && $identity['email']) {
array_unshift($event['attendees'], array('role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email'], 'status' => 'ACCEPTED'));
}
}
}
/**
* Releases some resources after successful event save
*/
private function cleanup_event(&$event)
{
// remove temp. attachment files
$eventid = 'cal:'.$event['id'];
if (!empty($_SESSION['event_session']) && ($eventid = $_SESSION['event_session']['id'])) {
$this->rc->plugins->exec_hook('attachments_cleanup', array('group' => $eventid));
unset($_SESSION['event_session']);
}
}
/**
* Send out an invitation/notification to all event attendees
*/
private function notify_attendees($event, $old, $action = 'edit')
{
if ($action == 'remove') {
$event['cancelled'] = true;
$is_cancelled = true;
}
$itip = $this->load_itip();
$emails = $this->get_user_emails();
// compose multipart message using PEAR:Mail_Mime
$method = $action == 'remove' ? 'CANCEL' : 'REQUEST';
$message = $itip->compose_itip_message($event, $method);
// list existing attendees from $old event
$old_attendees = array();
foreach ((array)$old['attendees'] as $attendee) {
$old_attendees[] = $attendee['email'];
}
// send to every attendee
$sent = 0;
foreach ((array)$event['attendees'] as $attendee) {
// skip myself for obvious reasons
if (!$attendee['email'] || in_array($attendee['email'], $emails))
continue;
// which template to use for mail text
$is_new = !in_array($attendee['email'], $old_attendees);
$bodytext = $is_cancelled ? 'eventcancelmailbody' : ($is_new ? 'invitationmailbody' : 'eventupdatemailbody');
$subject = $is_cancelled ? 'eventcancelsubject' : ($is_new ? 'invitationsubject' : ($event['title'] ? 'eventupdatesubject':'eventupdatesubjectempty'));
// finally send the message
if ($itip->send_itip_message($event, $method, $attendee, $subject, $bodytext, $message))
$sent++;
else
$sent = -100;
}
return $sent;
}
/**
* Compose a date string for the given event
*/
public function event_date_text($event, $tzinfo = false)
{
$fromto = '';
$duration = $event['end'] - $event['start'];
$this->date_format_defaults();
$date_format = self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']));
$time_format = self::to_php_date_format($this->rc->config->get('calendar_time_format', $this->defaults['calendar_time_format']));
if ($event['allday']) {
$fromto = format_date($event['start'], $date_format);
if (($todate = format_date($event['end'], $date_format)) != $fromto)
$fromto .= ' - ' . $todate;
}
else if ($duration < 86400 && gmdate('d', $event['start']) == gmdate('d', $event['end'])) {
$fromto = format_date($event['start'], $date_format) . ' ' . format_date($event['start'], $time_format) .
' - ' . format_date($event['end'], $time_format);
}
else {
$fromto = format_date($event['start'], $date_format) . ' ' . format_date($event['start'], $time_format) .
' - ' . format_date($event['end'], $date_format) . ' ' . format_date($event['end'], $time_format);
}
// add timezone information
if ($tzinfo && ($tzname = $this->timezone->getName())) {
$fromto .= ' (' . strtr($tzname, '_', ' ') . ')';
}
return $fromto;
}
/**
* Echo simple free/busy status text for the given user and time range
*/
public function freebusy_status()
{
$email = get_input_value('email', RCUBE_INPUT_GPC);
$start = get_input_value('start', RCUBE_INPUT_GPC);
$end = get_input_value('end', RCUBE_INPUT_GPC);
if (!$start) $start = time();
if (!$end) $end = $start + 3600;
$fbtypemap = array(calendar::FREEBUSY_UNKNOWN => 'UNKNOWN', calendar::FREEBUSY_FREE => 'FREE', calendar::FREEBUSY_BUSY => 'BUSY', calendar::FREEBUSY_TENTATIVE => 'TENTATIVE', calendar::FREEBUSY_OOF => 'OUT-OF-OFFICE');
$status = 'UNKNOWN';
// if the backend has free-busy information
$fblist = $this->driver->get_freebusy_list($email, $start, $end);
if (is_array($fblist)) {
$status = 'FREE';
foreach ($fblist as $slot) {
list($from, $to, $type) = $slot;
if ($from < $end && $to > $start) {
$status = isset($type) && $fbtypemap[$type] ? $fbtypemap[$type] : 'BUSY';
break;
}
}
}
// let this information be cached for 5min
send_future_expire_header(300);
echo $status;
exit;
}
/**
* Return a list of free/busy time slots within the given period
* Echo data in JSON encoding
*/
public function freebusy_times()
{
$email = get_input_value('email', RCUBE_INPUT_GPC);
$start = get_input_value('start', RCUBE_INPUT_GPC);
$end = get_input_value('end', RCUBE_INPUT_GPC);
$interval = intval(get_input_value('interval', RCUBE_INPUT_GPC));
if (!$start) $start = time();
if (!$end) $end = $start + 86400 * 30;
if (!$interval) $interval = 60; // 1 hour
$fblist = $this->driver->get_freebusy_list($email, $start, $end);
$slots = array();
// build a list from $start till $end with blocks representing the fb-status
for ($s = 0, $t = $start; $t <= $end; $s++) {
$status = self::FREEBUSY_UNKNOWN;
$t_end = $t + $interval * 60;
// determine attendee's status
if (is_array($fblist)) {
$status = self::FREEBUSY_FREE;
foreach ($fblist as $slot) {
list($from, $to, $type) = $slot;
if ($from < $t_end && $to > $t) {
$status = isset($type) ? $type : self::FREEBUSY_BUSY;
if ($status == self::FREEBUSY_BUSY) // can't get any worse :-)
break;
}
}
}
$slots[$s] = $status;
$t = $t_end;
}
// let this information be cached for 5min
send_future_expire_header(300);
echo json_encode(array('email' => $email, 'start' => intval($start), 'end' => intval($t_end), 'interval' => $interval, 'slots' => $slots));
exit;
}
/**
* Handler for printing calendars
*/
public function print_view()
{
$title = $this->gettext('print');
$view = get_input_value('view', RCUBE_INPUT_GPC);
if (!in_array($view, array('agendaWeek', 'agendaDay', 'month', 'table')))
$view = 'agendaDay';
$this->rc->output->set_env('view',$view);
if ($date = get_input_value('date', RCUBE_INPUT_GPC))
$this->rc->output->set_env('date', $date);
if ($range = get_input_value('range', RCUBE_INPUT_GPC))
$this->rc->output->set_env('listRange', intval($range));
if (isset($_REQUEST['sections']))
$this->rc->output->set_env('listSections', get_input_value('sections', RCUBE_INPUT_GPC));
if ($search = get_input_value('search', RCUBE_INPUT_GPC)) {
$this->rc->output->set_env('search', $search);
$title .= ' "' . $search . '"';
}
// Add CSS stylesheets to the page header
$skin = $this->rc->config->get('skin');
$this->include_stylesheet('skins/' . $skin . '/fullcalendar.css');
$this->include_stylesheet('skins/' . $skin . '/print.css');
// Add JS files to the page header
$this->include_script('print.js');
$this->register_handler('plugin.calendar_css', array($this->ui, 'calendar_css'));
$this->register_handler('plugin.calendar_list', array($this->ui, 'calendar_list'));
$this->rc->output->set_pagetitle($title);
$this->rc->output->send("calendar.print");
}
/**
*
*/
public function get_inline_ui()
{
foreach (array('save','cancel','savingdata') as $label)
$texts['calendar.'.$label] = $this->gettext($label);
$texts['calendar.new_event'] = $this->gettext('createfrommail');
$this->ui->init_templates();
$this->ui->calendar_list(); # set env['calendars']
echo $this->api->output->parse('calendar.eventedit', false, false);
echo html::tag('script', array('type' => 'text/javascript'),
"rcmail.set_env('calendars', " . json_encode($this->api->output->env['calendars']) . ");\n".
"rcmail.set_env('deleteicon', '" . $this->api->output->env['deleteicon'] . "');\n".
"rcmail.set_env('cancelicon', '" . $this->api->output->env['cancelicon'] . "');\n".
"rcmail.set_env('loadingicon', '" . $this->api->output->env['loadingicon'] . "');\n".
"rcmail.add_label(" . json_encode($texts) . ");\n"
);
exit;
}
/**
* Compare two event objects and return differing properties
*
* @param array Event A
* @param array Event B
* @return array List of differing event properties
*/
public static function event_diff($a, $b)
{
$diff = array();
$ignore = array('changed' => 1, 'attachments' => 1);
foreach (array_unique(array_merge(array_keys($a), array_keys($b))) as $key) {
if (!$ignore[$key] && $a[$key] != $b[$key])
$diff[] = $key;
}
// only compare number of attachments
if (count($a['attachments']) != count($b['attachments']))
$diff[] = 'attachments';
return $diff;
}
/**** Event invitation plugin hooks ****/
/**
* Handler for URLs that allow an invitee to respond on his invitation mail
*/
public function itip_attend_response($p)
{
if ($p['action'] == 'attend') {
$this->rc->output->set_env('task', 'calendar'); // override some env vars
$this->rc->output->set_env('keep_alive', 0);
$this->rc->output->set_pagetitle($this->gettext('calendar'));
$itip = $this->load_itip();
$token = get_input_value('_t', RCUBE_INPUT_GPC);
// read event info stored under the given token
if ($invitation = $itip->get_invitation($token)) {
$this->token = $token;
$this->event = $invitation['event'];
// show message about cancellation
if ($invitation['cancelled']) {
$this->invitestatus = html::div('rsvp-status declined', $this->gettext('eventcancelled'));
}
// save submitted RSVP status
else if (!empty($_POST['rsvp'])) {
$status = null;
foreach (array('accepted','tentative','declined') as $method) {
if ($_POST['rsvp'] == $this->gettext('itip' . $method)) {
$status = $method;
break;
}
}
// send itip reply to organizer
if ($status && $itip->update_invitation($invitation, $invitation['attendee'], strtoupper($status))) {
$this->invitestatus = html::div('rsvp-status ' . strtolower($status), $this->gettext('youhave'.strtolower($status)));
}
else
$this->rc->output->command('display_message', $this->gettext('errorsaving'), 'error', -1);
// if user is logged in...
if ($this->rc->user->ID) {
$this->load_driver();
$invitation = $itip->get_invitation($token);
// save the event to his/her default calendar if not yet present
if (!$this->driver->get_event($this->event) && ($calendar = $this->get_default_calendar(true))) {
$invitation['event']['calendar'] = $calendar['id'];
if ($this->driver->new_event($invitation['event']))
$this->rc->output->command('display_message', $this->gettext(array('name' => 'importedsuccessfully', 'vars' => array('calendar' => $calendar['name']))), 'confirmation');
}
}
}
$this->register_handler('plugin.event_inviteform', array($this, 'itip_event_inviteform'));
$this->register_handler('plugin.event_invitebox', array($this->ui, 'event_invitebox'));
if (!$this->invitestatus)
$this->register_handler('plugin.event_rsvp_buttons', array($this->ui, 'event_rsvp_buttons'));
$this->rc->output->set_pagetitle($this->gettext('itipinvitation') . ' ' . $this->event['title']);
}
else
$this->rc->output->command('display_message', $this->gettext('itipinvalidrequest'), 'error', -1);
$this->rc->output->send('calendar.itipattend');
}
}
/**
*
*/
public function itip_event_inviteform($attrib)
{
$hidden = new html_hiddenfield(array('name' => "_t", 'value' => $this->token));
return html::tag('form', array('action' => $this->rc->url(array('task' => 'calendar', 'action' => 'attend')), 'method' => 'post', 'noclose' => true) + $attrib) . $hidden->show();
}
/**
* Check mail message structure of there are .ics files attached
*/
public function mail_message_load($p)
{
$this->message = $p['object'];
$itip_part = null;
// check all message parts for .ics files
foreach ((array)$this->message->mime_parts as $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);
+ $imap = $this->rc->get_storage();
+ $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);
+ $part = $imap->get_message_part($uid, $mime_id);
if ($part->ctype_parameters['charset'])
$charset = $part->ctype_parameters['charset'];
- $headers = $this->rc->imap->get_headers($uid);
+ $headers = $imap->get_message_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);
+ $imap = $this->rc->get_storage();
+ $imap->set_mailbox($mbox);
$message = new rcube_message($uid);
if ($message->headers) {
$event['title'] = trim($message->subject);
$event['description'] = trim($message->first_text_part());
// copy mail attachments to event
if ($message->attachments) {
$eventid = 'cal:';
if (!is_array($_SESSION['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),
+ 'data' => $imap->get_message_part($uid, $part->mime_id, $part),
'size' => $part->size,
'name' => $part->filename,
'mimetype' => $part->mimetype,
'group' => $eventid,
);
$attachment = $this->rc->plugins->exec_hook('attachment_save', $attachment);
if ($attachment['status'] && !$attachment['abort']) {
$id = $attachment['id'];
// 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 = 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 a2aac7cf..b8888253 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -1,2733 +1,2734 @@
/**
* Client UI Javascript for the Calendar plugin
*
* @version @package_version@
* @author Lazlo Westerhof <hello@lazlo.me>
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
// Roundcube calendar UI client class
function rcube_calendar_ui(settings)
{
// extend base class
rcube_calendar.call(this, settings);
/*** member vars ***/
this.is_loading = false;
this.selected_event = null;
this.selected_calendar = null;
this.search_request = null;
this.saving_lock = null;
/*** private vars ***/
var DAY_MS = 86400000;
var HOUR_MS = 3600000;
var me = this;
var gmt_offset = (new Date().getTimezoneOffset() / -60) - (settings.timezone || 0) - (settings.dst || 0);
var client_timezone = new Date().getTimezoneOffset();
var day_clicked = day_clicked_ts = 0;
var ignore_click = false;
var event_defaults = { free_busy:'busy' };
var event_attendees = [];
var attendees_list;
var freebusy_ui = { workinhoursonly:false, needsupdate:false };
var freebusy_data = {};
var current_view = null;
var exec_deferred = bw.ie6 ? 5 : 1;
var sensitivitylabels = { 0:rcmail.gettext('public','calendar'), 1:rcmail.gettext('private','calendar'), 2:rcmail.gettext('confidential','calendar') };
var ui_loading = rcmail.set_busy(true, 'loading');
// general datepicker settings
var datepicker_settings = {
// translate from fullcalendar format to datepicker format
dateFormat: settings['date_format'].replace(/M/g, 'm').replace(/mmmmm/, 'MM').replace(/mmm/, 'M').replace(/dddd/, 'DD').replace(/ddd/, 'D').replace(/yy/g, 'y'),
firstDay : settings['first_day'],
dayNamesMin: settings['days_short'],
monthNames: settings['months'],
monthNamesShort: settings['months'],
changeMonth: false,
showOtherMonths: true,
selectOtherMonths: true
};
/*** private methods ***/
var Q = this.quote_html;
var text2html = function(str, maxlen, maxlines)
{
var html = Q(String(str));
// limit visible text length
if (maxlen) {
var morelink = ' <a href="#more" onclick="$(this).hide().next().show();return false" class="morelink">'+rcmail.gettext('showmore','calendar')+'</a><span style="display:none">',
lines = html.split(/\r?\n/),
words, out = '', len = 0;
for (var i=0; i < lines.length; i++) {
len += lines[i].length;
if (maxlines && i == maxlines - 1) {
out += lines[i] + '\n' + morelink;
maxlen = html.length * 2;
}
else if (len > maxlen) {
len = out.length;
words = lines[i].split(' ');
for (var j=0; j < words.length; j++) {
len += words[j].length + 1;
out += words[j] + ' ';
if (len > maxlen) {
out += morelink;
maxlen = html.length * 2;
}
}
out += '\n';
}
else
out += lines[i] + '\n';
}
if (maxlen > str.length)
out += '</span>';
html = out;
}
// simple link parser (similar to rcube_string_replacer class in PHP)
var utf_domain = '[^?&@"\'/\\(\\)\\s\\r\\t\\n]+\\.([^\x00-\x2f\x3b-\x40\x5b-\x60\x7b-\x7f]{2,}|xn--[a-z0-9]{2,})';
var url1 = '.:;,', url2 = 'a-z0-9%=#@+?&/_~\\[\\]-';
var link_pattern = new RegExp('([hf]t+ps?://)('+utf_domain+'(['+url1+']?['+url2+']+)*)?', 'ig');
var mailto_pattern = new RegExp('([^\\s\\n\\(\\);]+@'+utf_domain+')', 'ig');
return html
.replace(link_pattern, '<a href="$1$2" target="_blank">$1$2</a>')
.replace(mailto_pattern, '<a href="mailto:$1">$1</a>')
.replace(/(mailto:)([^"]+)"/g, '$1$2" onclick="rcmail.command(\'compose\', \'$2\');return false"')
.replace(/\n/g, "<br/>");
};
// same as str.split(delimiter) but it ignores delimiters within quoted strings
var explode_quoted_string = function(str, delimiter)
{
var result = [],
strlen = str.length,
q, p, i, char, last;
for (q = p = i = 0; i < strlen; i++) {
char = str.charAt(i);
if (char == '"' && last != '\\') {
q = !q;
}
else if (!q && char == delimiter) {
result.push(str.substring(p, i));
p = i + 1;
}
last = char;
}
result.push(str.substr(p));
return result;
};
// from time and date strings to a real date object
var parse_datetime = function(time, date)
{
// we use the utility function from datepicker to parse dates
var date = date ? $.datepicker.parseDate(datepicker_settings.dateFormat, date, datepicker_settings) : new Date();
var time_arr = time.replace(/\s*[ap][.m]*/i, '').replace(/0([0-9])/g, '$1').split(/[:.]/);
if (!isNaN(time_arr[0])) {
date.setHours(time_arr[0]);
if (time.match(/p[.m]*/i) && date.getHours() < 12)
date.setHours(parseInt(time_arr[0]) + 12);
else if (time.match(/a[.m]*/i) && date.getHours() == 12)
date.setHours(0);
}
if (!isNaN(time_arr[1]))
date.setMinutes(time_arr[1]);
return date;
};
// clone the given date object and optionally adjust time
var clone_date = function(date, adjust)
{
var d = new Date(date.getTime());
// set time to 00:00
if (adjust == 1) {
d.setHours(0);
d.setMinutes(0);
}
// set time to 23:59
else if (adjust == 2) {
d.setHours(23);
d.setMinutes(59);
}
return d;
};
// convert the given Date object into a unix timestamp respecting browser's and user's timezone settings
var date2unixtime = function(date)
{
var dst_offset = (client_timezone - date.getTimezoneOffset()) * 60; // adjust DST offset
return Math.round(date.getTime()/1000 + gmt_offset * 3600 + dst_offset);
};
var fromunixtime = function(ts)
{
ts -= gmt_offset * 3600;
var date = new Date(ts * 1000),
dst_offset = (client_timezone - date.getTimezoneOffset()) * 60;
if (dst_offset) // adjust DST offset
date.setTime((ts + 3600) * 1000);
return date;
};
// determine whether the given date is on a weekend
var is_weekend = function(date)
{
return date.getDay() == 0 || date.getDay() == 6;
};
var is_workinghour = function(date)
{
if (settings['work_start'] > settings['work_end'])
return date.getHours() >= settings['work_start'] || date.getHours() < settings['work_end'];
else
return date.getHours() >= settings['work_start'] && date.getHours() < settings['work_end'];
};
// check if the event has 'real' attendees, excluding the current user
var has_attendees = function(event)
{
return (event.attendees && (event.attendees.length > 1 || event.attendees[0].email != settings.identity.email));
};
// check if the current user is an attendee of this event
var is_attendee = function(event, role)
{
for (var i=0; event.attendees && i < event.attendees.length; i++) {
if ((!role || event.attendees[i].role == role) && event.attendees[i].email && settings.identity.emails.indexOf(';'+event.attendees[i].email) >= 0)
return true;
}
return false;
};
// check if the current user is the organizer
var is_organizer = function(event)
{
return is_attendee(event, 'ORGANIZER') || !event.id;
};
// create a nice human-readable string for the date/time range
var event_date_text = function(event)
{
var fromto, duration = event.end.getTime() / 1000 - event.start.getTime() / 1000;
if (event.allDay)
fromto = $.fullCalendar.formatDate(event.start, settings['date_format']) + (duration > 86400 || event.start.getDay() != event.end.getDay() ? ' &mdash; ' + $.fullCalendar.formatDate(event.end, settings['date_format']) : '');
else if (duration < 86400 && event.start.getDay() == event.end.getDay())
fromto = $.fullCalendar.formatDate(event.start, settings['date_format']) + ' ' + $.fullCalendar.formatDate(event.start, settings['time_format']) + ' &mdash; '
+ $.fullCalendar.formatDate(event.end, settings['time_format']);
else
fromto = $.fullCalendar.formatDate(event.start, settings['date_format']) + ' ' + $.fullCalendar.formatDate(event.start, settings['time_format']) + ' &mdash; '
+ $.fullCalendar.formatDate(event.end, settings['date_format']) + ' ' + $.fullCalendar.formatDate(event.end, settings['time_format']);
return fromto;
};
var load_attachment = function(event, att)
{
var qstring = '_id='+urlencode(att.id)+'&_event='+urlencode(event.recurrence_id||event.id)+'&_cal='+urlencode(event.calendar);
// open attachment in frame if it's of a supported mimetype
- if (id && att.mimetype && $.inArray(att.mimetype, rcmail.mimetypes)>=0) {
+ if (id && att.mimetype && $.inArray(att.mimetype, settings.mimetypes)>=0) {
rcmail.attachment_win = window.open(rcmail.env.comm_path+'&_action=get-attachment&'+qstring+'&_frame=1', 'rcubeeventattachment');
if (rcmail.attachment_win) {
window.setTimeout(function() { rcmail.attachment_win.focus(); }, 10);
return;
}
}
rcmail.goto_url('get-attachment', qstring+'&_download=1', false);
};
// build event attachments list
var event_show_attachments = function(list, container, event, edit)
{
var i, id, len, img, content, li, elem,
ul = document.createElement('UL');
ul.className = 'attachmentslist';
for (i=0, len=list.length; i<len; i++) {
elem = list[i];
li = document.createElement('LI');
li.className = elem.classname;
if (edit) {
rcmail.env.attachments[elem.id] = elem;
// delete icon
content = document.createElement('A');
content.href = '#delete';
content.title = rcmail.gettext('delete');
content.className = 'delete';
$(content).click({id: elem.id}, function(e) { remove_attachment(this, e.data.id); return false; });
if (!rcmail.env.deleteicon)
content.innerHTML = rcmail.gettext('delete');
else {
img = document.createElement('IMG');
img.src = rcmail.env.deleteicon;
img.alt = rcmail.gettext('delete');
content.appendChild(img);
}
li.appendChild(content);
}
// name/link
content = document.createElement('A');
content.innerHTML = elem.name;
content.className = 'file';
content.href = '#load';
$(content).click({event: event, att: elem}, function(e) {
load_attachment(e.data.event, e.data.att); return false; });
li.appendChild(content);
ul.appendChild(li);
}
if (edit && rcmail.gui_objects.attachmentlist) {
ul.id = rcmail.gui_objects.attachmentlist.id;
rcmail.gui_objects.attachmentlist = ul;
}
container.empty().append(ul);
};
var remove_attachment = function(elem, id)
{
$(elem.parentNode).hide();
rcmail.env.deleted_attachments.push(id);
delete rcmail.env.attachments[id];
};
// event details dialog (show only)
var event_show_dialog = function(event)
{
var $dialog = $("#eventshow").dialog('close').removeClass().addClass('uidialog');
var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:false };
me.selected_event = event;
-
+
$dialog.find('div.event-section, div.event-line').hide();
$('#event-title').html(Q(event.title)).show();
if (event.location)
$('#event-location').html('@ ' + text2html(event.location)).show();
if (event.description)
$('#event-description').show().children('.event-text').html(text2html(event.description, 300, 6));
// render from-to in a nice human-readable way
// -> now shown in dialog title
// $('#event-date').html(Q(me.event_date_text(event))).show();
if (event.recurrence && event.recurrence_text)
$('#event-repeat').show().children('.event-text').html(Q(event.recurrence_text));
if (event.alarms && event.alarms_text)
$('#event-alarm').show().children('.event-text').html(Q(event.alarms_text));
if (calendar.name)
$('#event-calendar').show().children('.event-text').html(Q(calendar.name)).removeClass().addClass('event-text').addClass('cal-'+calendar.id);
if (event.categories)
$('#event-category').show().children('.event-text').html(Q(event.categories)).removeClass().addClass('event-text cat-'+String(event.categories).replace(rcmail.identifier_expr, ''));
if (event.free_busy)
$('#event-free-busy').show().children('.event-text').html(Q(rcmail.gettext(event.free_busy, 'calendar')));
if (event.priority > 0) {
- var priolabels = [ '', rcmail.gettext('high'), rcmail.gettext('highest'), '', '', rcmail.gettext('normal'), '', '', rcmail.gettext('low'), rcmail.gettext('lowest') ];
+ var priolabels = [ '', rcmail.gettext('highest'), rcmail.gettext('high'), '', '', rcmail.gettext('normal'), '', '', rcmail.gettext('low'), rcmail.gettext('lowest') ];
$('#event-priority').show().children('.event-text').html(Q(event.priority+' '+priolabels[event.priority]));
}
+
if (event.sensitivity != 0) {
var sensitivityclasses = { 0:'public', 1:'private', 2:'confidential' };
$('#event-sensitivity').show().children('.event-text').html(Q(sensitivitylabels[event.sensitivity]));
$dialog.addClass('sensitivity-'+sensitivityclasses[event.sensitivity]);
}
// create attachments list
if ($.isArray(event.attachments)) {
event_show_attachments(event.attachments, $('#event-attachments').children('.event-text'), event);
if (event.attachments.length > 0) {
$('#event-attachments').show();
}
}
else if (calendar.attachments) {
// fetch attachments, some drivers doesn't set 'attachments' prop of the event?
}
// list event attendees
if (calendar.attendees && event.attendees) {
var data, dispname, organizer = false, rsvp = false, html = '';
for (var j=0; j < event.attendees.length; j++) {
data = event.attendees[j];
dispname = Q(data.name || data.email);
if (data.email) {
dispname = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink">' + dispname + '</a>';
if (data.role == 'ORGANIZER')
organizer = true;
else if ((data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE') && settings.identity.emails.indexOf(';'+data.email) >= 0)
rsvp = data.status.toLowerCase();
}
html += '<span class="attendee ' + String(data.role == 'ORGANIZER' ? 'organizer' : data.status).toLowerCase() + '">' + dispname + '</span> ';
// stop listing attendees
if (j == 7 && event.attendees.length >= 7) {
html += ' <em>' + rcmail.gettext('andnmore', 'calendar').replace('$nr', event.attendees.length - j - 1) + '</em>';
break;
}
}
if (html && (event.attendees.length > 1 || !organizer)) {
$('#event-attendees').show()
.children('.event-text')
.html(html)
.find('a.mailtolink').click(function(e) { rcmail.redirect(rcmail.url('mail/compose', { _to:this.href.substr(7) })); return false; });
}
$('#event-rsvp')[(rsvp?'show':'hide')]();
$('#event-rsvp .rsvp-buttons input').prop('disabled', false).filter('input[rel='+rsvp+']').prop('disabled', true);
}
-
+
var buttons = {};
if (calendar.editable && event.editable !== false) {
buttons[rcmail.gettext('edit', 'calendar')] = function() {
event_edit_dialog('edit', event);
};
buttons[rcmail.gettext('remove', 'calendar')] = function() {
me.delete_event(event);
$dialog.dialog('close');
};
}
else {
buttons[rcmail.gettext('close', 'calendar')] = function(){
$dialog.dialog('close');
};
}
// open jquery UI dialog
$dialog.dialog({
modal: false,
resizable: !bw.ie6,
closeOnEscape: (!bw.ie6 && !bw.ie7), // disable for performance reasons
title: Q(me.event_date_text(event)),
close: function() {
$dialog.dialog('destroy').hide();
},
buttons: buttons,
minWidth: 320,
width: 420
}).show();
// set dialog size according to content
me.dialog_resize($dialog.get(0), $dialog.height(), 420);
/*
// add link for "more options" drop-down
$('<a>')
.attr('href', '#')
.html('More Options')
.addClass('dropdown-link')
.click(function(){ return false; })
.insertBefore($dialog.parent().find('.ui-dialog-buttonset').children().first());
*/
};
// bring up the event dialog (jquery-ui popup)
var event_edit_dialog = function(action, event)
{
// close show dialog first
$("#eventshow").dialog('close');
var $dialog = $('<div>');
var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:action=='new' };
me.selected_event = $.extend($.extend({}, event_defaults), event); // clone event object (with defaults)
event = me.selected_event; // change reference to clone
freebusy_ui.needsupdate = false;
// reset dialog first
$('#eventtabs').get(0).reset();
// event details
var title = $('#edit-title').val(event.title || '');
var location = $('#edit-location').val(event.location || '');
var description = $('#edit-description').html(event.description || '');
var categories = $('#edit-categories').val(event.categories);
var calendars = $('#edit-calendar').val(event.calendar);
var freebusy = $('#edit-free-busy').val(event.free_busy);
var priority = $('#edit-priority').val(event.priority);
var sensitivity = $('#edit-sensitivity').val(event.sensitivity);
var duration = Math.round((event.end.getTime() - event.start.getTime()) / 1000);
var startdate = $('#edit-startdate').val($.fullCalendar.formatDate(event.start, settings['date_format'])).data('duration', duration);
var starttime = $('#edit-starttime').val($.fullCalendar.formatDate(event.start, settings['time_format'])).show();
var enddate = $('#edit-enddate').val($.fullCalendar.formatDate(event.end, settings['date_format']));
var endtime = $('#edit-endtime').val($.fullCalendar.formatDate(event.end, settings['time_format'])).show();
var allday = $('#edit-allday').get(0);
var notify = $('#edit-attendees-donotify').get(0);
var invite = $('#edit-attendees-invite').get(0);
notify.checked = has_attendees(event), invite.checked = true;
if (event.allDay) {
starttime.val("12:00").hide();
endtime.val("13:00").hide();
allday.checked = true;
}
else {
allday.checked = false;
}
// set alarm(s)
// TODO: support multiple alarm entries
if (event.alarms) {
if (typeof event.alarms == 'string')
event.alarms = event.alarms.split(';');
for (var alarm, i=0; i < event.alarms.length; i++) {
alarm = String(event.alarms[i]).split(':');
if (!alarm[1] && alarm[0]) alarm[1] = 'DISPLAY';
$('select.edit-alarm-type').val(alarm[1]);
if (alarm[0].match(/@(\d+)/)) {
var ondate = fromunixtime(parseInt(RegExp.$1));
$('select.edit-alarm-offset').val('@');
$('input.edit-alarm-date').val($.fullCalendar.formatDate(ondate, settings['date_format']));
$('input.edit-alarm-time').val($.fullCalendar.formatDate(ondate, settings['time_format']));
}
else if (alarm[0].match(/([-+])(\d+)([MHD])/)) {
$('input.edit-alarm-value').val(RegExp.$2);
$('select.edit-alarm-offset').val(''+RegExp.$1+RegExp.$3);
}
}
}
// set correct visibility by triggering onchange handlers
$('select.edit-alarm-type, select.edit-alarm-offset').change();
// enable/disable alarm property according to backend support
$('#edit-alarms')[(calendar.alarms ? 'show' : 'hide')]();
// check categories drop-down: add value if not exists
if (event.categories && !categories.find("option[value='"+event.categories+"']").length) {
$('<option>').attr('value', event.categories).text(event.categories).appendTo(categories).prop('selected', true);
}
// set recurrence form
var recurrence, interval, rrtimes, rrenddate;
var load_recurrence_tab = function()
{
recurrence = $('#edit-recurrence-frequency').val(event.recurrence ? event.recurrence.FREQ : '').change();
interval = $('select.edit-recurrence-interval').val(event.recurrence ? event.recurrence.INTERVAL : 1);
rrtimes = $('#edit-recurrence-repeat-times').val(event.recurrence ? event.recurrence.COUNT : 1);
rrenddate = $('#edit-recurrence-enddate').val(event.recurrence && event.recurrence.UNTIL ? $.fullCalendar.formatDate(new Date(event.recurrence.UNTIL*1000), settings['date_format']) : '');
$('input.edit-recurrence-until:checked').prop('checked', false);
var weekdays = ['SU','MO','TU','WE','TH','FR','SA'];
var rrepeat_id = '#edit-recurrence-repeat-forever';
if (event.recurrence && event.recurrence.COUNT) rrepeat_id = '#edit-recurrence-repeat-count';
else if (event.recurrence && event.recurrence.UNTIL) rrepeat_id = '#edit-recurrence-repeat-until';
$(rrepeat_id).prop('checked', true);
if (event.recurrence && event.recurrence.BYDAY && event.recurrence.FREQ == 'WEEKLY') {
var wdays = event.recurrence.BYDAY.split(',');
$('input.edit-recurrence-weekly-byday').val(wdays);
}
if (event.recurrence && event.recurrence.BYMONTHDAY) {
$('input.edit-recurrence-monthly-bymonthday').val(String(event.recurrence.BYMONTHDAY).split(','));
$('input.edit-recurrence-monthly-mode').val(['BYMONTHDAY']);
}
if (event.recurrence && event.recurrence.BYDAY && (event.recurrence.FREQ == 'MONTHLY' || event.recurrence.FREQ == 'YEARLY')) {
var byday, section = event.recurrence.FREQ.toLowerCase();
if ((byday = String(event.recurrence.BYDAY).match(/(-?[1-4])([A-Z]+)/))) {
$('#edit-recurrence-'+section+'-prefix').val(byday[1]);
$('#edit-recurrence-'+section+'-byday').val(byday[2]);
}
$('input.edit-recurrence-'+section+'-mode').val(['BYDAY']);
}
else if (event.start) {
$('#edit-recurrence-monthly-byday').val(weekdays[event.start.getDay()]);
}
if (event.recurrence && event.recurrence.BYMONTH) {
$('input.edit-recurrence-yearly-bymonth').val(String(event.recurrence.BYMONTH).split(','));
}
else if (event.start) {
$('input.edit-recurrence-yearly-bymonth').val([String(event.start.getMonth()+1)]);
}
};
// show warning if editing a recurring event
if (event.id && event.recurrence) {
$('#edit-recurring-warning').show();
$('input.edit-recurring-savemode[value="all"]').prop('checked', true);
}
else
$('#edit-recurring-warning').hide();
// init attendees tab
var organizer = !event.attendees || is_organizer(event);
event_attendees = [];
attendees_list = $('#edit-attendees-table > tbody').html('');
$('#edit-attendees-notify')[(notify.checked && organizer ? 'show' : 'hide')]();
$('#edit-localchanges-warning')[(has_attendees(event) && !organizer ? 'show' : 'hide')]();
var load_attendees_tab = function()
{
if (event.attendees) {
for (var j=0; j < event.attendees.length; j++)
add_attendee(event.attendees[j], !organizer);
}
$('#edit-attendees-form')[(organizer?'show':'hide')]();
$('#edit-attendee-schedule')[(calendar.freebusy?'show':'hide')]();
};
// attachments
var load_attachments_tab = function()
{
rcmail.enable_command('remove-attachment', !calendar.readonly);
rcmail.env.deleted_attachments = [];
// we're sharing some code for uploads handling with app.js
rcmail.env.attachments = [];
rcmail.env.compose_id = event.id; // for rcmail.async_upload_form()
if ($.isArray(event.attachments)) {
event_show_attachments(event.attachments, $('#edit-attachments'), event, true);
}
else {
$('#edit-attachments > ul').empty();
// fetch attachments, some drivers doesn't set 'attachments' array for event?
}
};
// init dialog buttons
var buttons = {};
buttons[rcmail.gettext('save', 'calendar')] = function() {
var start = parse_datetime(allday.checked ? '12:00' : starttime.val(), startdate.val());
var end = parse_datetime(allday.checked ? '13:00' : endtime.val(), enddate.val());
// basic input validatetion
if (start.getTime() > end.getTime()) {
alert(rcmail.gettext('invalideventdates', 'calendar'));
return false;
}
// post data to server
var data = {
calendar: event.calendar,
start: date2unixtime(start),
end: date2unixtime(end),
allday: allday.checked?1:0,
title: title.val(),
description: description.val(),
location: location.val(),
categories: categories.val(),
free_busy: freebusy.val(),
priority: priority.val(),
sensitivity: sensitivity.val(),
recurrence: '',
alarms: '',
attendees: event_attendees,
deleted_attachments: rcmail.env.deleted_attachments,
attachments: []
};
// serialize alarm settings
// TODO: support multiple alarm entries
var alarm = $('select.edit-alarm-type').val();
if (alarm) {
var val, offset = $('select.edit-alarm-offset').val();
if (offset == '@')
data.alarms = '@' + date2unixtime(parse_datetime($('input.edit-alarm-time').val(), $('input.edit-alarm-date').val())) + ':' + alarm;
else if ((val = parseInt($('input.edit-alarm-value').val())) && !isNaN(val) && val >= 0)
data.alarms = offset[0] + val + offset[1] + ':' + alarm;
}
// uploaded attachments list
for (var i in rcmail.env.attachments)
if (i.match(/^rcmfile(.+)/))
data.attachments.push(RegExp.$1);
// read attendee roles
$('select.edit-attendee-role').each(function(i, elem){
if (data.attendees[i])
data.attendees[i].role = $(elem).val();
});
// don't submit attendees if only myself is added as organizer
if (data.attendees.length == 1 && data.attendees[0].role == 'ORGANIZER' && data.attendees[0].email == settings.identity.email)
data.attendees = [];
// tell server to send notifications
if (data.attendees.length && organizer && ((event.id && notify.checked) || (!event.id && invite.checked))) {
data._notify = 1;
}
// gather recurrence settings
var freq;
if ((freq = recurrence.val()) != '') {
data.recurrence = {
FREQ: freq,
INTERVAL: $('#edit-recurrence-interval-'+freq.toLowerCase()).val()
};
var until = $('input.edit-recurrence-until:checked').val();
if (until == 'count')
data.recurrence.COUNT = rrtimes.val();
else if (until == 'until')
data.recurrence.UNTIL = date2unixtime(parse_datetime(endtime.val(), rrenddate.val()));
if (freq == 'WEEKLY') {
var byday = [];
$('input.edit-recurrence-weekly-byday:checked').each(function(){ byday.push(this.value); });
if (byday.length)
data.recurrence.BYDAY = byday.join(',');
}
else if (freq == 'MONTHLY') {
var mode = $('input.edit-recurrence-monthly-mode:checked').val(), bymonday = [];
if (mode == 'BYMONTHDAY') {
$('input.edit-recurrence-monthly-bymonthday:checked').each(function(){ bymonday.push(this.value); });
if (bymonday.length)
data.recurrence.BYMONTHDAY = bymonday.join(',');
}
else
data.recurrence.BYDAY = $('#edit-recurrence-monthly-prefix').val() + $('#edit-recurrence-monthly-byday').val();
}
else if (freq == 'YEARLY') {
var byday, bymonth = [];
$('input.edit-recurrence-yearly-bymonth:checked').each(function(){ bymonth.push(this.value); });
if (bymonth.length)
data.recurrence.BYMONTH = bymonth.join(',');
if ((byday = $('#edit-recurrence-yearly-byday').val()))
data.recurrence.BYDAY = $('#edit-recurrence-yearly-prefix').val() + byday;
}
}
data.calendar = calendars.val();
if (event.id) {
data.id = event.id;
if (event.recurrence)
data._savemode = $('input.edit-recurring-savemode:checked').val();
if (data.calendar && data.calendar != event.calendar)
data._fromcalendar = event.calendar;
}
update_event(action, data);
$dialog.dialog("close");
};
if (event.id) {
buttons[rcmail.gettext('remove', 'calendar')] = function() {
me.delete_event(event);
$dialog.dialog('close');
};
}
buttons[rcmail.gettext('cancel', 'calendar')] = function() {
$dialog.dialog("close");
};
// show/hide tabs according to calendar's feature support
$('#edit-tab-attendees')[(calendar.attendees?'show':'hide')]();
$('#edit-tab-attachments')[(calendar.attachments?'show':'hide')]();
// activate the first tab
$('#eventtabs').tabs('select', 0);
// hack: set task to 'calendar' to make all dialog actions work correctly
var comm_path_before = rcmail.env.comm_path;
rcmail.env.comm_path = comm_path_before.replace(/_task=[a-z]+/, '_task=calendar');
var editform = $("#eventedit");
// open jquery UI dialog
$dialog.dialog({
modal: true,
resizable: (!bw.ie6 && !bw.ie7), // disable for performance reasons
closeOnEscape: false,
title: rcmail.gettext((action == 'edit' ? 'edit_event' : 'new_event'), 'calendar'),
close: function() {
editform.hide().appendTo(document.body);
$dialog.dialog("destroy").remove();
rcmail.ksearch_blur();
rcmail.ksearch_destroy();
freebusy_data = {};
rcmail.env.comm_path = comm_path_before; // restore comm_path
},
buttons: buttons,
minWidth: 500,
width: 580
}).append(editform.show()); // adding form content AFTERWARDS massively speeds up opening on IE6
// set dialog size according to form content
me.dialog_resize($dialog.get(0), editform.height() + (bw.ie ? 20 : 0), 530);
title.select();
// init other tabs asynchronously
window.setTimeout(load_recurrence_tab, exec_deferred);
if (calendar.attendees)
window.setTimeout(load_attendees_tab, exec_deferred);
if (calendar.attachments)
window.setTimeout(load_attachments_tab, exec_deferred);
};
// open a dialog to display detailed free-busy information and to find free slots
var event_freebusy_dialog = function()
{
var $dialog = $('#eventfreebusy').dialog('close');
var event = me.selected_event;
if (!event_attendees.length)
return false;
// set form elements
var allday = $('#edit-allday').get(0);
var duration = Math.round((event.end.getTime() - event.start.getTime()) / 1000);
freebusy_ui.startdate = $('#schedule-startdate').val($.fullCalendar.formatDate(event.start, settings['date_format'])).data('duration', duration);
freebusy_ui.starttime = $('#schedule-starttime').val($.fullCalendar.formatDate(event.start, settings['time_format'])).show();
freebusy_ui.enddate = $('#schedule-enddate').val($.fullCalendar.formatDate(event.end, settings['date_format']));
freebusy_ui.endtime = $('#schedule-endtime').val($.fullCalendar.formatDate(event.end, settings['time_format'])).show();
if (allday.checked) {
freebusy_ui.starttime.val("12:00").hide();
freebusy_ui.endtime.val("13:00").hide();
event.allDay = true;
}
// read attendee roles from drop-downs
$('select.edit-attendee-role').each(function(i, elem){
if (event_attendees[i])
event_attendees[i].role = $(elem).val();
});
// render time slots
var now = new Date(), fb_start = new Date(), fb_end = new Date();
fb_start.setTime(event.start);
fb_start.setHours(0); fb_start.setMinutes(0); fb_start.setSeconds(0); fb_start.setMilliseconds(0);
fb_end.setTime(fb_start.getTime() + DAY_MS);
freebusy_data = { required:{}, all:{} };
freebusy_ui.loading = 1; // prevent render_freebusy_grid() to load data yet
freebusy_ui.numdays = Math.max(allday.checked ? 14 : 1, Math.ceil(duration * 2 / 86400));
freebusy_ui.interval = allday.checked ? 1440 : 60;
freebusy_ui.start = fb_start;
freebusy_ui.end = new Date(freebusy_ui.start.getTime() + DAY_MS * freebusy_ui.numdays);
render_freebusy_grid(0);
// render list of attendees
freebusy_ui.attendees = {};
var domid, dispname, data, role_html, list_html = '';
for (var i=0; i < event_attendees.length; i++) {
data = event_attendees[i];
dispname = Q(data.name || data.email);
domid = String(data.email).replace(rcmail.identifier_expr, '');
role_html = '<a class="attendee-role-toggle" id="rcmlia' + domid + '" title="' + Q(rcmail.gettext('togglerole', 'calendar')) + '">&nbsp;</a>';
list_html += '<div class="attendee ' + String(data.role).toLowerCase() + '" id="rcmli' + domid + '">' + role_html + dispname + '</div>';
// clone attendees data for local modifications
freebusy_ui.attendees[i] = freebusy_ui.attendees[domid] = $.extend({}, data);
}
// add total row
list_html += '<div class="attendee spacer">&nbsp;</div>';
list_html += '<div class="attendee total">' + rcmail.gettext('reqallattendees','calendar') + '</div>';
$('#schedule-attendees-list').html(list_html)
.unbind('click.roleicons')
.bind('click.roleicons', function(e){
// toggle attendee status upon click on icon
if (e.target.id && e.target.id.match(/rcmlia(.+)/)) {
var attendee, domid = RegExp.$1, roles = [ 'REQ-PARTICIPANT', 'OPT-PARTICIPANT', 'CHAIR' ];
if ((attendee = freebusy_ui.attendees[domid]) && attendee.role != 'ORGANIZER') {
var req = attendee.role != 'OPT-PARTICIPANT';
var j = $.inArray(attendee.role, roles);
j = (j+1) % roles.length;
attendee.role = roles[j];
$(e.target).parent().removeClass().addClass('attendee '+String(attendee.role).toLowerCase());
// update total display if required-status changed
if (req != (roles[j] != 'OPT-PARTICIPANT')) {
compute_freebusy_totals();
update_freebusy_display(attendee.email);
}
}
}
return false;
});
// enable/disable buttons
$('#shedule-find-prev').button('option', 'disabled', (fb_start.getTime() < now.getTime()));
// dialog buttons
var buttons = {};
buttons[rcmail.gettext('select', 'calendar')] = function() {
$('#edit-startdate').val(freebusy_ui.startdate.val());
$('#edit-starttime').val(freebusy_ui.starttime.val());
$('#edit-enddate').val(freebusy_ui.enddate.val());
$('#edit-endtime').val(freebusy_ui.endtime.val());
// write role changes back to main dialog
$('select.edit-attendee-role').each(function(i, elem){
if (event_attendees[i] && freebusy_ui.attendees[i]) {
event_attendees[i].role = freebusy_ui.attendees[i].role;
$(elem).val(event_attendees[i].role);
}
});
if (freebusy_ui.needsupdate)
update_freebusy_status(me.selected_event);
freebusy_ui.needsupdate = false;
$dialog.dialog("close");
};
buttons[rcmail.gettext('cancel', 'calendar')] = function() {
$dialog.dialog("close");
};
$dialog.dialog({
modal: true,
resizable: true,
closeOnEscape: (!bw.ie6 && !bw.ie7),
title: rcmail.gettext('scheduletime', 'calendar'),
close: function() {
if (bw.ie6)
$("#edit-attendees-table").css('visibility','visible');
$dialog.dialog("destroy").hide();
},
resizeStop: function() {
render_freebusy_overlay();
},
buttons: buttons,
minWidth: 640,
width: 850
}).show();
// hide edit dialog on IE6 because of drop-down elements
if (bw.ie6)
$("#edit-attendees-table").css('visibility','hidden');
// adjust dialog size to fit grid without scrolling
var gridw = $('#schedule-freebusy-times').width();
var overflow = gridw - $('#attendees-freebusy-table td.times').width() + 1;
me.dialog_resize($dialog.get(0), $dialog.height() + (bw.ie ? 20 : 0), 800 + Math.max(0, overflow));
// fetch data from server
freebusy_ui.loading = 0;
load_freebusy_data(freebusy_ui.start, freebusy_ui.interval);
};
// render an HTML table showing free-busy status for all the event attendees
var render_freebusy_grid = function(delta)
{
if (delta) {
freebusy_ui.start.setTime(freebusy_ui.start.getTime() + DAY_MS * delta);
// skip weekends if in workinhoursonly-mode
if (Math.abs(delta) == 1 && freebusy_ui.workinhoursonly) {
while (is_weekend(freebusy_ui.start))
freebusy_ui.start.setTime(freebusy_ui.start.getTime() + DAY_MS * delta);
}
freebusy_ui.end = new Date(freebusy_ui.start.getTime() + DAY_MS * freebusy_ui.numdays);
}
var dayslots = Math.floor(1440 / freebusy_ui.interval);
var date_format = 'ddd '+ (dayslots <= 2 ? settings.date_short : settings.date_format);
var lastdate, datestr, css,
curdate = new Date(),
allday = (freebusy_ui.interval == 1440),
times_css = (allday ? 'allday ' : ''),
dates_row = '<tr class="dates">',
times_row = '<tr class="times">',
slots_row = '';
for (var s = 0, t = freebusy_ui.start.getTime(); t < freebusy_ui.end.getTime(); s++) {
curdate.setTime(t);
datestr = fc.fullCalendar('formatDate', curdate, date_format);
if (datestr != lastdate) {
dates_row += '<th colspan="' + dayslots + '" class="boxtitle date' + $.fullCalendar.formatDate(curdate, 'ddMMyyyy') + '">' + Q(datestr) + '</th>';
lastdate = datestr;
}
// set css class according to working hours
css = is_weekend(curdate) || (freebusy_ui.interval <= 60 && !is_workinghour(curdate)) ? 'offhours' : 'workinghours';
times_row += '<td class="' + times_css + css + '" id="t-' + Math.floor(t/1000) + '">' + Q(allday ? rcmail.gettext('all-day','calendar') : $.fullCalendar.formatDate(curdate, settings['time_format'])) + '</td>';
slots_row += '<td class="' + css + ' unknown">&nbsp;</td>';
t += freebusy_ui.interval * 60000;
}
dates_row += '</tr>';
times_row += '</tr>';
// render list of attendees
var domid, data, list_html = '', times_html = '';
for (var i=0; i < event_attendees.length; i++) {
data = event_attendees[i];
domid = String(data.email).replace(rcmail.identifier_expr, '');
times_html += '<tr id="fbrow' + domid + '">' + slots_row + '</tr>';
}
// add line for all/required attendees
times_html += '<tr class="spacer"><td colspan="' + (dayslots * freebusy_ui.numdays) + '">&nbsp;</td>';
times_html += '<tr id="fbrowall">' + slots_row + '</tr>';
var table = $('#schedule-freebusy-times');
table.children('thead').html(dates_row + times_row);
table.children('tbody').html(times_html);
// initialize event handlers on grid
if (!freebusy_ui.grid_events) {
freebusy_ui.grid_events = true;
table.children('thead').click(function(e){
// move event to the clicked date/time
if (e.target.id && e.target.id.match(/t-(\d+)/)) {
var newstart = new Date(RegExp.$1 * 1000);
// set time to 00:00
if (me.selected_event.allDay) {
newstart.setMinutes(0);
newstart.setHours(0);
}
update_freebusy_dates(newstart, new Date(newstart.getTime() + freebusy_ui.startdate.data('duration') * 1000));
render_freebusy_overlay();
}
})
}
// if we have loaded free-busy data, show it
if (!freebusy_ui.loading) {
if (date2unixtime(freebusy_ui.start) < freebusy_data.start || date2unixtime(freebusy_ui.end) > freebusy_data.end || freebusy_ui.interval != freebusy_data.interval) {
load_freebusy_data(freebusy_ui.start, freebusy_ui.interval);
}
else {
for (var email, i=0; i < event_attendees.length; i++) {
if ((email = event_attendees[i].email))
update_freebusy_display(email);
}
}
}
// render current event date/time selection over grid table
// use timeout to let the dom attributes (width/height/offset) be set first
window.setTimeout(function(){ render_freebusy_overlay(); }, 10);
};
// render overlay element over the grid to visiualize the current event date/time
var render_freebusy_overlay = function()
{
var overlay = $('#schedule-event-time');
if (me.selected_event.end.getTime() <= freebusy_ui.start.getTime() || me.selected_event.start.getTime() >= freebusy_ui.end.getTime()) {
overlay.draggable('disable').hide();
}
else {
var table = $('#schedule-freebusy-times'),
width = 0,
pos = { top:table.children('thead').height(), left:0 },
eventstart = date2unixtime(clone_date(me.selected_event.start, me.selected_event.allDay?1:0)),
eventend = date2unixtime(clone_date(me.selected_event.end, me.selected_event.allDay?2:0)) - 60,
slotstart = date2unixtime(freebusy_ui.start),
slotsize = freebusy_ui.interval * 60,
slotend, fraction, $cell;
// iterate through slots to determine position and size of the overlay
table.children('thead').find('td').each(function(i, cell){
slotend = slotstart + slotsize - 1;
// event starts in this slot: compute left
if (eventstart >= slotstart && eventstart <= slotend) {
fraction = 1 - (slotend - eventstart) / slotsize;
pos.left = Math.round(cell.offsetLeft + cell.offsetWidth * fraction);
}
// event ends in this slot: compute width
if (eventend >= slotstart && eventend <= slotend) {
fraction = 1 - (slotend - eventend) / slotsize;
width = Math.round(cell.offsetLeft + cell.offsetWidth * fraction) - pos.left;
}
slotstart = slotstart + slotsize;
});
if (!width)
width = table.width() - pos.left;
// overlay is visible
if (width > 0) {
overlay.css({ width: (width-5)+'px', height:(table.children('tbody').height() - 4)+'px', left:pos.left+'px', top:pos.top+'px' }).draggable('enable').show();
// configure draggable
if (!overlay.data('isdraggable')) {
overlay.draggable({
axis: 'x',
scroll: true,
stop: function(e, ui){
// convert pixels to time
var px = ui.position.left;
var range_p = $('#schedule-freebusy-times').width();
var range_t = freebusy_ui.end.getTime() - freebusy_ui.start.getTime();
var newstart = new Date(freebusy_ui.start.getTime() + px * (range_t / range_p));
newstart.setSeconds(0); newstart.setMilliseconds(0);
// snap to day boundaries
if (me.selected_event.allDay) {
if (newstart.getHours() >= 12) // snap to next day
newstart.setTime(newstart.getTime() + DAY_MS);
newstart.setMinutes(0);
newstart.setHours(0);
}
else {
// round to 5 minutes
var round = newstart.getMinutes() % 5;
if (round > 2.5) newstart.setTime(newstart.getTime() + (5 - round) * 60000);
else if (round > 0) newstart.setTime(newstart.getTime() - round * 60000);
}
// update event times and display
update_freebusy_dates(newstart, new Date(newstart.getTime() + freebusy_ui.startdate.data('duration') * 1000));
if (me.selected_event.allDay)
render_freebusy_overlay();
}
}).data('isdraggable', true);
}
}
else
overlay.draggable('disable').hide();
}
};
// fetch free-busy information for each attendee from server
var load_freebusy_data = function(from, interval)
{
var start = new Date(from.getTime() - DAY_MS * 2); // start 1 days before event
var end = new Date(start.getTime() + DAY_MS * Math.max(14, freebusy_ui.numdays + 7)); // load min. 14 days
freebusy_ui.numrequired = 0;
freebusy_data.all = [];
freebusy_data.required = [];
// load free-busy information for every attendee
var domid, email;
for (var i=0; i < event_attendees.length; i++) {
if ((email = event_attendees[i].email)) {
domid = String(email).replace(rcmail.identifier_expr, '');
$('#rcmli' + domid).addClass('loading');
freebusy_ui.loading++;
$.ajax({
type: 'GET',
dataType: 'json',
url: rcmail.url('freebusy-times'),
data: { email:email, start:date2unixtime(clone_date(start, 1)), end:date2unixtime(clone_date(end, 2)), interval:interval, _remote:1 },
success: function(data) {
freebusy_ui.loading--;
// find attendee
var attendee = null;
for (var i=0; i < event_attendees.length; i++) {
if (freebusy_ui.attendees[i].email == data.email) {
attendee = freebusy_ui.attendees[i];
break;
}
}
// copy data to member var
var req = attendee.role != 'OPT-PARTICIPANT';
var ts = data.start - 0;
freebusy_data.start = ts;
freebusy_data[data.email] = {};
for (var i=0; i < data.slots.length; i++) {
freebusy_data[data.email][ts] = data.slots[i];
// set totals
if (!freebusy_data.required[ts])
freebusy_data.required[ts] = [0,0,0,0];
if (req)
freebusy_data.required[ts][data.slots[i]]++;
if (!freebusy_data.all[ts])
freebusy_data.all[ts] = [0,0,0,0];
freebusy_data.all[ts][data.slots[i]]++;
ts += data.interval * 60;
}
freebusy_data.end = ts;
freebusy_data.interval = data.interval;
// hide loading indicator
var domid = String(data.email).replace(rcmail.identifier_expr, '');
$('#rcmli' + domid).removeClass('loading');
// update display
update_freebusy_display(data.email);
}
});
// count required attendees
if (freebusy_ui.attendees[i].role != 'OPT-PARTICIPANT')
freebusy_ui.numrequired++;
}
}
};
// re-calculate total status after role change
var compute_freebusy_totals = function()
{
freebusy_ui.numrequired = 0;
freebusy_data.all = [];
freebusy_data.required = [];
var email, req, status;
for (var i=0; i < event_attendees.length; i++) {
if (!(email = event_attendees[i].email))
continue;
req = freebusy_ui.attendees[i].role != 'OPT-PARTICIPANT';
if (req)
freebusy_ui.numrequired++;
for (var ts in freebusy_data[email]) {
if (!freebusy_data.required[ts])
freebusy_data.required[ts] = [0,0,0,0];
if (!freebusy_data.all[ts])
freebusy_data.all[ts] = [0,0,0,0];
status = freebusy_data[email][ts];
freebusy_data.all[ts][status]++;
if (req)
freebusy_data.required[ts][status]++;
}
}
};
// update free-busy grid with status loaded from server
var update_freebusy_display = function(email)
{
var status_classes = ['unknown','free','busy','tentative','out-of-office'];
var domid = String(email).replace(rcmail.identifier_expr, '');
var row = $('#fbrow' + domid);
var rowall = $('#fbrowall').children();
var ts = date2unixtime(freebusy_ui.start);
var fbdata = freebusy_data[email];
if (fbdata && fbdata[ts] !== undefined && row.length) {
row.children().each(function(i, cell){
cell.className = cell.className.replace('unknown', fbdata[ts] ? status_classes[fbdata[ts]] : 'unknown');
// also update total row if all data was loaded
if (freebusy_ui.loading == 0 && freebusy_data.all[ts] && (cell = rowall.get(i))) {
var workinghours = cell.className.indexOf('workinghours') >= 0;
var all_status = freebusy_data.all[ts][2] ? 'busy' : 'unknown';
req_status = freebusy_data.required[ts][2] ? 'busy' : 'free';
for (var j=1; j < status_classes.length; j++) {
if (freebusy_ui.numrequired && freebusy_data.required[ts][j] >= freebusy_ui.numrequired)
req_status = status_classes[j];
if (freebusy_data.all[ts][j] == event_attendees.length)
all_status = status_classes[j];
}
cell.className = (workinghours ? 'workinghours ' : 'offhours ') + req_status + ' all-' + all_status;
}
ts += freebusy_ui.interval * 60;
});
}
};
// write changed event date/times back to form fields
var update_freebusy_dates = function(start, end)
{
if (me.selected_event.allDay) {
start.setHours(12);
start.setMinutes(0);
end.setHours(13);
end.setMinutes(0);
}
me.selected_event.start = start;
me.selected_event.end = end;
freebusy_ui.startdate.val($.fullCalendar.formatDate(start, settings['date_format']));
freebusy_ui.starttime.val($.fullCalendar.formatDate(start, settings['time_format']));
freebusy_ui.enddate.val($.fullCalendar.formatDate(end, settings['date_format']));
freebusy_ui.endtime.val($.fullCalendar.formatDate(end, settings['time_format']));
freebusy_ui.needsupdate = true;
};
// attempt to find a time slot where all attemdees are available
var freebusy_find_slot = function(dir)
{
var event = me.selected_event,
eventstart = date2unixtime(event.start), // calculate with unixtimes
eventend = date2unixtime(event.end),
duration = eventend - eventstart,
sinterval = freebusy_data.interval * 60,
intvlslots = 1,
numslots = Math.ceil(duration / sinterval),
checkdate, slotend, email, curdate;
// shift event times to next possible slot
eventstart += sinterval * intvlslots * dir;
eventend += sinterval * intvlslots * dir;
// iterate through free-busy slots and find candidates
var candidatecount = 0, candidatestart = candidateend = success = false;
for (var slot = dir > 0 ? freebusy_data.start : freebusy_data.end - sinterval; (dir > 0 && slot < freebusy_data.end) || (dir < 0 && slot >= freebusy_data.start); slot += sinterval * dir) {
slotend = slot + sinterval;
if ((dir > 0 && slotend <= eventstart) || (dir < 0 && slot >= eventend)) // skip
continue;
// respect workingours setting
if (freebusy_ui.workinhoursonly) {
curdate = fromunixtime(dir > 0 || !candidateend ? slot : (candidateend - duration));
if (is_weekend(curdate) || (freebusy_data.interval <= 60 && !is_workinghour(curdate))) { // skip off-hours
candidatestart = candidateend = false;
candidatecount = 0;
continue;
}
}
if (!candidatestart)
candidatestart = slot;
// check freebusy data for all attendees
for (var i=0; i < event_attendees.length; i++) {
if (freebusy_ui.attendees[i].role != 'OPT-PARTICIPANT' && (email = freebusy_ui.attendees[i].email) && freebusy_data[email] && freebusy_data[email][slot] > 1) {
candidatestart = candidateend = false;
break;
}
}
// occupied slot
if (!candidatestart) {
slot += Math.max(0, intvlslots - candidatecount - 1) * sinterval * dir;
candidatecount = 0;
continue;
}
// set candidate end to slot end time
candidatecount++;
if (dir < 0 && !candidateend)
candidateend = slotend;
// if candidate is big enough, this is it!
if (candidatecount == numslots) {
if (dir > 0) {
event.start = fromunixtime(candidatestart);
event.end = fromunixtime(candidatestart + duration);
}
else {
event.end = fromunixtime(candidateend);
event.start = fromunixtime(candidateend - duration);
}
success = true;
break;
}
}
// update event date/time display
if (success) {
update_freebusy_dates(event.start, event.end);
// move freebusy grid if necessary
var offset = Math.ceil((event.start.getTime() - freebusy_ui.end.getTime()) / DAY_MS);
if (event.start.getTime() >= freebusy_ui.end.getTime())
render_freebusy_grid(Math.max(1, offset));
else if (event.end.getTime() <= freebusy_ui.start.getTime())
render_freebusy_grid(Math.min(-1, offset));
else
render_freebusy_overlay();
var now = new Date();
$('#shedule-find-prev').button('option', 'disabled', (event.start.getTime() < now.getTime()));
}
else {
alert(rcmail.gettext('noslotfound','calendar'));
}
};
// update event properties and attendees availability if event times have changed
var event_times_changed = function()
{
if (me.selected_event) {
var allday = $('#edit-allday').get(0);
me.selected_event.allDay = allday.checked;
me.selected_event.start = parse_datetime(allday.checked ? '12:00' : $('#edit-starttime').val(), $('#edit-startdate').val());
me.selected_event.end = parse_datetime(allday.checked ? '13:00' : $('#edit-endtime').val(), $('#edit-enddate').val());
if (event_attendees)
freebusy_ui.needsupdate = true;
$('#edit-startdate').data('duration', Math.round((me.selected_event.end.getTime() - me.selected_event.start.getTime()) / 1000));
}
};
// add the given list of participants
var add_attendees = function(names)
{
names = explode_quoted_string(names.replace(/,\s*$/, ''), ',');
// parse name/email pairs
var item, email, name, success = false;
for (var i=0; i < names.length; i++) {
email = name = '';
item = $.trim(names[i]);
if (!item.length) {
continue;
} // address in brackets without name (do nothing)
else if (item.match(/^<[^@]+@[^>]+>$/)) {
email = item.replace(/[<>]/g, '');
} // address without brackets and without name (add brackets)
else if (rcube_check_email(item)) {
email = item;
} // address with name
else if (item.match(/([^\s<@]+@[^>]+)>*$/)) {
email = RegExp.$1;
name = item.replace(email, '').replace(/^["\s<>]+/, '').replace(/["\s<>]+$/, '');
}
if (email) {
add_attendee({ email:email, name:name, role:'REQ-PARTICIPANT', status:'NEEDS-ACTION' });
success = true;
}
else {
alert(rcmail.gettext('noemailwarning'));
}
}
return success;
};
// add the given attendee to the list
var add_attendee = function(data, readonly)
{
// check for dupes...
var exists = false;
$.each(event_attendees, function(i, v){ exists |= (v.email == data.email); });
if (exists)
return false;
var dispname = Q(data.name || data.email);
if (data.email)
dispname = '<a href="mailto:' + data.email + '" title="' + Q(data.email) + '" class="mailtolink">' + dispname + '</a>';
// role selection
var organizer = data.role == 'ORGANIZER';
var opts = {};
if (organizer)
opts.ORGANIZER = rcmail.gettext('calendar.roleorganizer');
opts['REQ-PARTICIPANT'] = rcmail.gettext('calendar.rolerequired');
opts['OPT-PARTICIPANT'] = rcmail.gettext('calendar.roleoptional');
opts['CHAIR'] = rcmail.gettext('calendar.roleresource');
var select = '<select class="edit-attendee-role"' + (organizer || readonly ? ' disabled="true"' : '') + '>';
for (var r in opts)
select += '<option value="'+ r +'" class="' + r.toLowerCase() + '"' + (data.role == r ? ' selected="selected"' : '') +'>' + Q(opts[r]) + '</option>';
select += '</select>';
// availability
var avail = data.email ? 'loading' : 'unknown';
// delete icon
var icon = rcmail.env.deleteicon ? '<img src="' + rcmail.env.deleteicon + '" alt="" />' : rcmail.gettext('delete');
var dellink = '<a href="#delete" class="deletelink" title="' + Q(rcmail.gettext('delete')) + '">' + icon + '</a>';
var html = '<td class="role">' + select + '</td>' +
'<td class="name">' + dispname + '</td>' +
'<td class="availability"><img src="./program/blank.gif" class="availabilityicon ' + avail + '" /></td>' +
'<td class="confirmstate"><span class="' + String(data.status).toLowerCase() + '">' + Q(data.status) + '</span></td>' +
'<td class="options">' + (organizer || readonly ? '' : dellink) + '</td>';
var tr = $('<tr>')
.addClass(String(data.role).toLowerCase())
.html(html)
.appendTo(attendees_list);
tr.find('a.deletelink').click({ id:(data.email || data.name) }, function(e) { remove_attendee(this, e.data.id); return false; });
tr.find('a.mailtolink').click(function(e) { rcmail.redirect(rcmail.url('mail/compose', { _to:this.href.substr(7) })); return false; });
// check free-busy status
if (avail == 'loading') {
check_freebusy_status(tr.find('img.availabilityicon'), data.email, me.selected_event);
}
event_attendees.push(data);
};
// iterate over all attendees and update their free-busy status display
var update_freebusy_status = function(event)
{
var icons = attendees_list.find('img.availabilityicon');
for (var i=0; i < event_attendees.length; i++) {
if (icons.get(i) && event_attendees[i].email)
check_freebusy_status(icons.get(i), event_attendees[i].email, event);
}
freebusy_ui.needsupdate = false;
};
// load free-busy status from server and update icon accordingly
var check_freebusy_status = function(icon, email, event)
{
var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { freebusy:false };
if (!calendar.freebusy) {
$(icon).removeClass().addClass('availabilityicon unknown');
return;
}
icon = $(icon).removeClass().addClass('availabilityicon loading');
$.ajax({
type: 'GET',
dataType: 'html',
url: rcmail.url('freebusy-status'),
data: { email:email, start:date2unixtime(clone_date(event.start, event.allDay?1:0)), end:date2unixtime(clone_date(event.end, event.allDay?2:0)), _remote: 1 },
success: function(status){
icon.removeClass('loading').addClass(String(status).toLowerCase());
},
error: function(){
icon.removeClass('loading').addClass('unknown');
}
});
};
// remove an attendee from the list
var remove_attendee = function(elem, id)
{
$(elem).closest('tr').remove();
event_attendees = $.grep(event_attendees, function(data){ return (data.name != id && data.email != id) });
};
// when the user accepts or declines an event invitation
var event_rsvp = function(response)
{
if (me.selected_event && me.selected_event.attendees && response) {
// update attendee status
for (var data, i=0; i < me.selected_event.attendees.length; i++) {
data = me.selected_event.attendees[i];
if (settings.identity.emails.indexOf(';'+data.email) >= 0)
data.status = response.toUpperCase();
}
event_show_dialog(me.selected_event);
// submit status change to server
me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
rcmail.http_post('event', { action:'rsvp', e:me.selected_event, status:response });
}
}
// post the given event data to server
var update_event = function(action, data)
{
me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
rcmail.http_post('calendar/event', { action:action, e:data });
// render event temporarily into the calendar
if ((data.start && data.end) || data.id) {
var event = data.id ? $.extend(fc.fullCalendar('clientEvents', data.id)[0], data) : data;
if (data.start)
event.start = fromunixtime(data.start);
if (data.end)
event.end = fromunixtime(data.end);
if (data.allday !== undefined)
event.allDay = data.allday;
event.editable = false;
event.temp = true;
event.className = 'fc-event-cal-'+data.calendar+' fc-event-temp';
fc.fullCalendar(data.id ? 'updateEvent' : 'renderEvent', event);
}
};
// mouse-click handler to check if the show dialog is still open and prevent default action
var dialog_check = function(e)
{
var showd = $("#eventshow");
if (showd.is(':visible') && !$(e.target).closest('.ui-dialog').length) {
showd.dialog('close');
e.stopImmediatePropagation();
ignore_click = true;
return false;
}
else if (ignore_click) {
window.setTimeout(function(){ ignore_click = false; }, 20);
return false;
}
return true;
};
// display confirm dialog when modifying/deleting an event
var update_event_confirm = function(action, event, data)
{
if (!data) data = event;
var decline = false, notify = false, html = '', cal = me.calendars[event.calendar];
// event has attendees, ask whether to notify them
if (has_attendees(event)) {
if (is_organizer(event)) {
notify = true;
html += '<div class="message">' +
'<label><input class="confirm-attendees-donotify" type="checkbox" checked="checked" value="1" name="notify" />&nbsp;' +
rcmail.gettext((action == 'remove' ? 'sendcancellation' : 'sendnotifications'), 'calendar') +
'</label></div>';
}
else if (action == 'remove' && is_attendee(event)) {
decline = true;
html += '<div class="message">' +
'<label><input class="confirm-attendees-decline" type="checkbox" checked="checked" value="1" name="decline" />&nbsp;' +
rcmail.gettext('itipdeclineevent', 'calendar') +
'</label></div>';
}
else {
html += '<div class="message">' + rcmail.gettext('localchangeswarning', 'calendar') + '</div>';
}
}
// recurring event: user needs to select the savemode
if (event.recurrence) {
html += '<div class="message"><span class="ui-icon ui-icon-alert"></span>' +
rcmail.gettext((action == 'remove' ? 'removerecurringeventwarning' : 'changerecurringeventwarning'), 'calendar') + '</div>' +
'<div class="savemode">' +
'<a href="#current" class="button">' + rcmail.gettext('currentevent', 'calendar') + '</a>' +
'<a href="#future" class="button">' + rcmail.gettext('futurevents', 'calendar') + '</a>' +
'<a href="#all" class="button">' + rcmail.gettext('allevents', 'calendar') + '</a>' +
(action != 'remove' ? '<a href="#new" class="button">' + rcmail.gettext('saveasnew', 'calendar') + '</a>' : '') +
'</div>';
}
// show dialog
if (html) {
var $dialog = $('<div>').html(html);
$dialog.find('a.button').button().click(function(e){
data._savemode = String(this.href).replace(/.+#/, '');
if ($dialog.find('input.confirm-attendees-donotify').get(0))
data._notify = notify && $dialog.find('input.confirm-attendees-donotify').get(0).checked ? 1 : 0;
if (decline && $dialog.find('input.confirm-attendees-decline:checked'))
data.decline = 1;
update_event(action, data);
$dialog.dialog("destroy").hide();
return false;
});
var buttons = [{
text: rcmail.gettext('cancel', 'calendar'),
click: function() {
$(this).dialog("close");
}
}];
if (!event.recurrence) {
buttons.push({
text: rcmail.gettext((action == 'remove' ? 'remove' : 'save'), 'calendar'),
click: function() {
data._notify = notify && $dialog.find('input.confirm-attendees-donotify').get(0).checked ? 1 : 0;
data.decline = decline && $dialog.find('input.confirm-attendees-decline:checked').length ? 1 : 0;
update_event(action, data);
$(this).dialog("close");
}
});
}
$dialog.dialog({
modal: true,
width: 460,
dialogClass: 'warning',
title: rcmail.gettext((action == 'remove' ? 'removeeventconfirm' : 'changeeventconfirm'), 'calendar'),
buttons: buttons,
close: function(){
$dialog.dialog("destroy").hide();
if (!rcmail.busy)
fc.fullCalendar('refetchEvents');
}
}).addClass('event-update-confirm').show();
return false;
}
// show regular confirm box when deleting
else if (action == 'remove' && !cal.undelete) {
if (!confirm(rcmail.gettext('deleteventconfirm', 'calendar')))
return false;
}
// do update
update_event(action, data);
return true;
};
var update_agenda_toolbar = function()
{
$('#agenda-listrange').val(fc.fullCalendar('option', 'listRange'));
$('#agenda-listsections').val(fc.fullCalendar('option', 'listSections'));
}
/*** fullcalendar event handlers ***/
var fc_event_render = function(event, element, view) {
if (view.name != 'list' && view.name != 'table') {
var prefix = event.sensitivity != 0 ? String(sensitivitylabels[event.sensitivity]).toUpperCase()+': ' : '';
element.attr('title', prefix + event.title);
}
if (view.name != 'month') {
if (event.location) {
element.find('div.fc-event-title').after('<div class="fc-event-location">@&nbsp;' + Q(event.location) + '</div>');
}
if (event.sensitivity != 0)
element.find('div.fc-event-time').append('<i class="fc-icon-sensitive"></i>');
if (event.recurrence)
element.find('div.fc-event-time').append('<i class="fc-icon-recurring"></i>');
if (event.alarms)
element.find('div.fc-event-time').append('<i class="fc-icon-alarms"></i>');
}
};
/*** public methods ***/
// opens calendar day-view in a popup
this.fisheye_view = function(date)
{
$('#fish-eye-view').dialog('close');
// create list of active event sources
var src, cals = {}, sources = [];
for (var id in this.calendars) {
src = $.extend({}, this.calendars[id]);
src.editable = false;
src.url = null;
src.events = [];
if (src.active) {
cals[id] = src;
sources.push(src);
}
}
// copy events already loaded
var events = fc.fullCalendar('clientEvents');
for (var event, i=0; i< events.length; i++) {
event = events[i];
if (event.source && (src = cals[event.source.id])) {
src.events.push(event);
}
}
var h = $(window).height() - 50;
var dialog = $('<div>')
.attr('id', 'fish-eye-view')
.dialog({
modal: true,
width: 680,
height: h,
title: $.fullCalendar.formatDate(date, 'dddd ' + settings['date_long']),
close: function(){
dialog.dialog("destroy");
me.fisheye_date = null;
}
})
.fullCalendar({
header: { left: '', center: '', right: '' },
height: h - 50,
defaultView: 'agendaDay',
date: date.getDate(),
month: date.getMonth(),
year: date.getFullYear(),
ignoreTimezone: true, // will treat the given date strings as in local (browser's) timezone
eventSources: sources,
monthNames : settings['months'],
monthNamesShort : settings['months_short'],
dayNames : settings['days'],
dayNamesShort : settings['days_short'],
firstDay : settings['first_day'],
firstHour : settings['first_hour'],
slotMinutes : 60/settings['timeslots'],
timeFormat: { '': settings['time_format'] },
axisFormat : settings['time_format'],
columnFormat: { day: 'dddd ' + settings['date_short'] },
titleFormat: { day: 'dddd ' + settings['date_long'] },
allDayText: rcmail.gettext('all-day', 'calendar'),
currentTimeIndicator: settings.time_indicator,
eventRender: fc_event_render,
eventClick: function(event) {
event_show_dialog(event);
}
});
this.fisheye_date = date;
};
//public method to show the print dialog.
this.print_calendars = function(view)
{
if (!view) view = fc.fullCalendar('getView').name;
var date = fc.fullCalendar('getDate') || new Date();
var range = fc.fullCalendar('option', 'listRange');
var sections = fc.fullCalendar('option', 'listSections');
var printwin = window.open(rcmail.url('print', { view: view, date: date2unixtime(date), range: range, sections: sections, search: this.search_query }), "rc_print_calendars", "toolbar=no,location=yes,menubar=yes,resizable=yes,scrollbars=yes,width=800");
window.setTimeout(function(){ printwin.focus() }, 50);
};
// public method to bring up the new event dialog
this.add_event = function(templ) {
if (this.selected_calendar) {
var now = new Date();
var date = fc.fullCalendar('getDate');
if (typeof date != 'Date')
date = now;
date.setHours(now.getHours()+1);
date.setMinutes(0);
var end = new Date(date.getTime());
end.setHours(date.getHours()+1);
event_edit_dialog('new', $.extend({ start:date, end:end, allDay:false, calendar:this.selected_calendar }, templ || {}));
}
};
// delete the given event after showing a confirmation dialog
this.delete_event = function(event) {
// show confirm dialog for recurring events, use jquery UI dialog
return update_event_confirm('remove', event, { id:event.id, calendar:event.calendar, attendees:event.attendees });
};
// opens a jquery UI dialog with event properties (or empty for creating a new calendar)
this.calendar_edit_dialog = function(calendar)
{
// close show dialog first
var $dialog = $("#calendarform").dialog('close');
if (!calendar)
calendar = { name:'', color:'cc0000', editable:true, showalarms:true };
var form, name, color, alarms;
$dialog.html(rcmail.get_label('loading'));
$.ajax({
type: 'GET',
dataType: 'html',
url: rcmail.url('calendar'),
data: { action:(calendar.id ? 'form-edit' : 'form-new'), c:{ id:calendar.id } },
success: function(data) {
$dialog.html(data);
// resize and reposition dialog window
form = $('#calendarpropform');
me.dialog_resize('#calendarform', form.height(), form.width());
name = $('#calendar-name').prop('disabled', !calendar.editable).val(calendar.editname || calendar.name);
color = $('#calendar-color').val(calendar.color).miniColors({ value: calendar.color, colorValues:rcmail.env.mscolors });
alarms = $('#calendar-showalarms').prop('checked', calendar.showalarms).get(0);
name.select();
}
});
// dialog buttons
var buttons = {};
buttons[rcmail.gettext('save', 'calendar')] = function() {
// form is not loaded
if (!form || !form.length)
return;
// TODO: do some input validation
if (!name.val() || name.val().length < 2) {
alert(rcmail.gettext('invalidcalendarproperties', 'calendar'));
name.select();
return;
}
// post data to server
var data = form.serializeJSON();
if (data.color)
data.color = data.color.replace(/^#/, '');
if (calendar.id)
data.id = calendar.id;
if (alarms)
data.showalarms = alarms.checked ? 1 : 0;
me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
rcmail.http_post('calendar', { action:(calendar.id ? 'edit' : 'new'), c:data });
$dialog.dialog("close");
};
buttons[rcmail.gettext('cancel', 'calendar')] = function() {
$dialog.dialog("close");
};
// open jquery UI dialog
$dialog.dialog({
modal: true,
resizable: true,
closeOnEscape: false,
title: rcmail.gettext((calendar.id ? 'editcalendar' : 'createcalendar'), 'calendar'),
close: function() {
$dialog.html('').dialog("destroy").hide();
},
buttons: buttons,
minWidth: 400,
width: 420
}).show();
};
this.calendar_remove = function(calendar)
{
if (confirm(rcmail.gettext('deletecalendarconfirm', 'calendar'))) {
rcmail.http_post('calendar', { action:'remove', c:{ id:calendar.id } });
return true;
}
return false;
};
this.calendar_destroy_source = function(id)
{
if (this.calendars[id]) {
fc.fullCalendar('removeEventSource', this.calendars[id]);
$(rcmail.get_folder_li(id, 'rcmlical')).remove();
$('#edit-calendar option[value="'+id+'"]').remove();
delete this.calendars[id];
}
};
// open a dialog to upload an .ics file with events to be imported
this.import_events = function(calendar)
{
// close show dialog first
var $dialog = $("#eventsimport").dialog('close');
var form = rcmail.gui_objects.importform;
$('#event-import-calendar').val(calendar.id);
var buttons = {};
buttons[rcmail.gettext('import', 'calendar')] = function() {
if (form && form.elements._data.value) {
rcmail.async_upload_form(form, 'import_events', function(e) {
rcmail.set_busy(false, null, me.saving_lock);
});
// display upload indicator
me.saving_lock = rcmail.set_busy(true, 'uploading');
}
};
buttons[rcmail.gettext('cancel', 'calendar')] = function() {
$dialog.dialog("close");
};
// open jquery UI dialog
$dialog.dialog({
modal: true,
resizable: false,
closeOnEscape: false,
title: rcmail.gettext('importevents', 'calendar'),
close: function() {
$dialog.dialog("destroy").hide();
},
buttons: buttons,
width: 520
}).show();
};
// callback from server if import succeeded
this.import_success = function(p)
{
$("#eventsimport").dialog('close');
rcmail.set_busy(false, null, me.saving_lock);
rcmail.gui_objects.importform.reset();
if (p.refetch)
this.refresh(p);
};
// show URL of the given calendar in a dialog box
this.showurl = function(calendar)
{
var $dialog = $('#calendarurlbox').dialog('close');
if (calendar.feedurl) {
$dialog.dialog({
resizable: true,
closeOnEscape: true,
title: rcmail.gettext('showurl', 'calendar'),
close: function() {
$dialog.dialog("destroy").hide();
},
width: 520
}).show();
$('#calfeedurl').val(calendar.feedurl).select();
}
};
// refresh the calendar view after saving event data
this.refresh = function(p)
{
var source = me.calendars[p.source];
if (source && (p.refetch || (p.update && !source.active))) {
// activate event source if new event was added to an invisible calendar
if (!source.active) {
source.active = true;
fc.fullCalendar('addEventSource', source);
$('#' + rcmail.get_folder_li(source.id, 'rcmlical').id + ' input').prop('checked', true);
}
else
fc.fullCalendar('refetchEvents', source);
}
// add/update single event object
else if (source && p.update) {
var event = p.update;
event.temp = false;
event.editable = source.editable;
var existing = fc.fullCalendar('clientEvents', event.id);
if (existing.length) {
$.extend(existing[0], event);
fc.fullCalendar('updateEvent', existing[0]);
}
else {
event.source = source; // link with source
fc.fullCalendar('renderEvent', event);
}
// refresh fish-eye view
if (me.fisheye_date)
me.fisheye_view(me.fisheye_date);
}
// remove temp events
fc.fullCalendar('removeEvents', function(e){ return e.temp; });
};
/*** event searching ***/
// execute search
this.quicksearch = function()
{
if (rcmail.gui_objects.qsearchbox) {
var q = rcmail.gui_objects.qsearchbox.value;
if (q != '') {
var id = 'search-'+q;
var sources = [];
if (this._search_message)
rcmail.hide_message(this._search_message);
for (var sid in this.calendars) {
if (this.calendars[sid]) {
this.calendars[sid].url = this.calendars[sid].url.replace(/&q=.+/, '') + '&q='+escape(q);
sources.push(sid);
}
}
id += '@'+sources.join(',');
// ignore if query didn't change
if (this.search_request == id) {
return;
}
// remember current view
else if (!this.search_request) {
this.default_view = fc.fullCalendar('getView').name;
}
this.search_request = id;
this.search_query = q;
// change to list view
fc.fullCalendar('option', 'listSections', 'month')
.fullCalendar('option', 'listRange', Math.max(60, settings['agenda_range']))
.fullCalendar('changeView', 'table');
update_agenda_toolbar();
// refetch events with new url (if not already triggered by changeView)
if (!this.is_loading)
fc.fullCalendar('refetchEvents');
}
else // empty search input equals reset
this.reset_quicksearch();
}
};
// reset search and get back to normal event listing
this.reset_quicksearch = function()
{
$(rcmail.gui_objects.qsearchbox).val('');
if (this._search_message)
rcmail.hide_message(this._search_message);
if (this.search_request) {
// hide bottom links of agenda view
fc.find('.fc-list-content > .fc-listappend').hide();
// restore original event sources and view mode from fullcalendar
fc.fullCalendar('option', 'listSections', settings['agenda_sections'])
.fullCalendar('option', 'listRange', settings['agenda_range']);
update_agenda_toolbar();
for (var sid in this.calendars) {
if (this.calendars[sid])
this.calendars[sid].url = this.calendars[sid].url.replace(/&q=.+/, '');
}
if (this.default_view)
fc.fullCalendar('changeView', this.default_view);
if (!this.is_loading)
fc.fullCalendar('refetchEvents');
this.search_request = this.search_query = null;
}
};
// callback if all sources have been fetched from server
this.events_loaded = function(count)
{
var addlinks, append = '';
// enhance list view when searching
if (this.search_request) {
if (!count) {
this._search_message = rcmail.display_message(rcmail.gettext('searchnoresults', 'calendar'), 'notice');
append = '<div class="message">' + rcmail.gettext('searchnoresults', 'calendar') + '</div>';
}
append += '<div class="fc-bottomlinks formlinks"></div>';
addlinks = true;
}
if (fc.fullCalendar('getView').name == 'table') {
var container = fc.find('.fc-list-content > .fc-listappend');
if (append) {
if (!container.length)
container = $('<div class="fc-listappend"></div>').appendTo(fc.find('.fc-list-content'));
container.html(append).show();
}
else if (container.length)
container.hide();
// add links to adjust search date range
if (addlinks) {
var lc = container.find('.fc-bottomlinks');
$('<a>').attr('href', '#').html(rcmail.gettext('searchearlierdates', 'calendar')).appendTo(lc).click(function(){
fc.fullCalendar('incrementDate', 0, -1, 0);
});
lc.append(" ");
$('<a>').attr('href', '#').html(rcmail.gettext('searchlaterdates', 'calendar')).appendTo(lc).click(function(){
var range = fc.fullCalendar('option', 'listRange');
if (range < 90) {
fc.fullCalendar('option', 'listRange', fc.fullCalendar('option', 'listRange') + 30).fullCalendar('render');
update_agenda_toolbar();
}
else
fc.fullCalendar('incrementDate', 0, 1, 0);
});
}
}
if (this.fisheye_date)
this.fisheye_view(this.fisheye_date);
};
// resize and reposition (center) the dialog window
this.dialog_resize = function(id, height, width)
{
var win = $(window), w = win.width(), h = win.height();
$(id).dialog('option', { height: Math.min(h-20, height+130), width: Math.min(w-20, width+50) })
.dialog('option', 'position', ['center', 'center']); // only works in a separate call (!?)
};
// adjust calendar view size
this.view_resize = function()
{
var footer = fc.fullCalendar('getView').name == 'table' ? $('#agendaoptions').outerHeight() : 0;
fc.fullCalendar('option', 'height', $('#calendar').height() - footer);
};
/*** startup code ***/
// create list of event sources AKA calendars
this.calendars = {};
var li, cal, active, event_sources = [];
for (var id in rcmail.env.calendars) {
cal = rcmail.env.calendars[id];
this.calendars[id] = $.extend({
url: "./?_task=calendar&_action=load_events&source="+escape(id),
editable: !cal.readonly,
className: 'fc-event-cal-'+id,
id: id
}, cal);
this.calendars[id].color = settings.event_coloring % 2 ? '' : '#' + cal.color;
if ((active = cal.active || false)) {
event_sources.push(this.calendars[id]);
}
// init event handler on calendar list checkbox
if ((li = rcmail.get_folder_li(id, 'rcmlical'))) {
$('#'+li.id+' input').click(function(e){
var id = $(this).data('id');
if (me.calendars[id]) { // add or remove event source on click
var action;
if (this.checked) {
action = 'addEventSource';
me.calendars[id].active = true;
}
else {
action = 'removeEventSource';
me.calendars[id].active = false;
}
// add/remove event source
fc.fullCalendar(action, me.calendars[id]);
rcmail.http_post('calendar', { action:'subscribe', c:{ id:id, active:me.calendars[id].active?1:0 } });
}
}).data('id', id).get(0).checked = active;
$(li).click(function(e){
var id = $(this).data('id');
rcmail.select_folder(id, 'rcmlical');
rcmail.enable_command('calendar-edit', true);
rcmail.enable_command('calendar-remove', 'events-import', 'calendar-showurl', true);
me.selected_calendar = id;
})
.dblclick(function(){ me.calendar_edit_dialog(me.calendars[me.selected_calendar]); })
.data('id', id);
}
if (!cal.readonly && !this.selected_calendar) {
this.selected_calendar = id;
rcmail.enable_command('addevent', true);
}
}
// select default calendar
if (settings.default_calendar && this.calendars[settings.default_calendar] && !this.calendars[settings.default_calendar].readonly)
this.selected_calendar = settings.default_calendar;
var viewdate = new Date();
if (rcmail.env.date)
viewdate.setTime(fromunixtime(rcmail.env.date));
// initalize the fullCalendar plugin
var fc = $('#calendar').fullCalendar({
header: {
left: 'prev,next today',
center: 'title',
right: 'agendaDay,agendaWeek,month,table'
},
aspectRatio: 1,
date: viewdate.getDate(),
month: viewdate.getMonth(),
year: viewdate.getFullYear(),
ignoreTimezone: true, // will treat the given date strings as in local (browser's) timezone
height: $('#calendar').height(),
eventSources: event_sources,
monthNames : settings['months'],
monthNamesShort : settings['months_short'],
dayNames : settings['days'],
dayNamesShort : settings['days_short'],
firstDay : settings['first_day'],
firstHour : settings['first_hour'],
slotMinutes : 60/settings['timeslots'],
timeFormat: {
'': settings['time_format'],
agenda: settings['time_format'] + '{ - ' + settings['time_format'] + '}',
list: settings['time_format'] + '{ - ' + settings['time_format'] + '}',
table: settings['time_format'] + '{ - ' + settings['time_format'] + '}'
},
axisFormat : settings['time_format'],
columnFormat: {
month: 'ddd', // Mon
week: 'ddd ' + settings['date_short'], // Mon 9/7
day: 'dddd ' + settings['date_short'], // Monday 9/7
table: settings['date_agenda']
},
titleFormat: {
month: 'MMMM yyyy',
week: settings['dates_long'],
day: 'dddd ' + settings['date_long'],
table: settings['dates_long']
},
listPage: 1, // advance one day in agenda view
listRange: settings['agenda_range'],
listSections: settings['agenda_sections'],
tableCols: ['handle', 'date', 'time', 'title', 'location'],
defaultView: rcmail.env.view || settings['default_view'],
allDayText: rcmail.gettext('all-day', 'calendar'),
buttonText: {
prev: (bw.ie6 ? '&nbsp;&lt;&lt;&nbsp;' : '&nbsp;&#9668;&nbsp;'),
next: (bw.ie6 ? '&nbsp;&gt;&gt;&nbsp;' : '&nbsp;&#9658;&nbsp;'),
today: settings['today'],
day: rcmail.gettext('day', 'calendar'),
week: rcmail.gettext('week', 'calendar'),
month: rcmail.gettext('month', 'calendar'),
table: rcmail.gettext('agenda', 'calendar')
},
listTexts: {
until: rcmail.gettext('until', 'calendar'),
past: rcmail.gettext('pastevents', 'calendar'),
today: rcmail.gettext('today', 'calendar'),
tomorrow: rcmail.gettext('tomorrow', 'calendar'),
thisWeek: rcmail.gettext('thisweek', 'calendar'),
nextWeek: rcmail.gettext('nextweek', 'calendar'),
thisMonth: rcmail.gettext('thismonth', 'calendar'),
nextMonth: rcmail.gettext('nextmonth', 'calendar'),
future: rcmail.gettext('futureevents', 'calendar'),
week: rcmail.gettext('weekofyear', 'calendar')
},
selectable: true,
selectHelper: false,
currentTimeIndicator: settings.time_indicator,
loading: function(isLoading) {
me.is_loading = isLoading;
this._rc_loading = rcmail.set_busy(isLoading, 'loading', this._rc_loading);
// trigger callback
if (!isLoading)
me.events_loaded($(this).fullCalendar('clientEvents').length);
},
// event rendering
eventRender: fc_event_render,
// render element indicating more (invisible) events
overflowRender: function(data, element) {
element.html(rcmail.gettext('andnmore', 'calendar').replace('$nr', data.count))
.click(function(e){ me.fisheye_view(data.date); });
},
// callback for date range selection
select: function(start, end, allDay, e, view) {
var range_select = (!allDay || start.getDate() != end.getDate())
if (dialog_check(e) && range_select)
event_edit_dialog('new', { start:start, end:end, allDay:allDay, calendar:me.selected_calendar });
if (range_select || ignore_click)
view.calendar.unselect();
},
// callback for clicks in all-day box
dayClick: function(date, allDay, e, view) {
var now = new Date().getTime();
if (now - day_clicked_ts < 400 && day_clicked == date.getTime()) { // emulate double-click on day
var enddate = new Date(); enddate.setTime(date.getTime() + DAY_MS - 60000);
return event_edit_dialog('new', { start:date, end:enddate, allDay:allDay, calendar:me.selected_calendar });
}
if (!ignore_click) {
view.calendar.gotoDate(date);
if (day_clicked && new Date(day_clicked).getMonth() != date.getMonth())
view.calendar.select(date, date, allDay);
}
day_clicked = date.getTime();
day_clicked_ts = now;
},
// callback when a specific event is clicked
eventClick: function(event) {
if (!event.temp)
event_show_dialog(event);
},
// callback when an event was dragged and finally dropped
eventDrop: function(event, dayDelta, minuteDelta, allDay, revertFunc) {
if (event.end == null || event.end.getTime() < event.start.getTime()) {
event.end = new Date(event.start.getTime() + (allDay ? DAY_MS : HOUR_MS));
}
// moved to all-day section: set times to 12:00 - 13:00
- if (allDay && !event.allday) {
+ 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) {
+ 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')
+ if (fc && 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
+ var st, start = (String(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 ba7f1e7c..b871d512 100644
--- a/plugins/calendar/drivers/database/database_driver.php
+++ b/plugins/calendar/drivers/database/database_driver.php
@@ -1,974 +1,956 @@
<?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['alarms'] && $event['start'] > time()) {
+ $alarm = calendar::get_next_alarm($event);
- if ($event['start'] > time())
- return date('Y-m-d H:i:s', $notify_at);
+ if ($alarm['time'] && $alarm['action'] == 'DISPLAY')
+ return date('Y-m-d H:i:s', $alarm['time']);
}
-
+
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' => $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/SQL/mysql.sql b/plugins/calendar/drivers/kolab/SQL/mysql.sql
index e64413a5..7a93d0a8 100644
--- a/plugins/calendar/drivers/kolab/SQL/mysql.sql
+++ b/plugins/calendar/drivers/kolab/SQL/mysql.sql
@@ -1,28 +1,31 @@
/**
* Roundcube Calendar Kolab backend
*
* @version @package_version@
* @author Thomas Bruederli
* @licence GNU AGPL
**/
CREATE TABLE IF NOT EXISTS `kolab_alarms` (
`event_id` VARCHAR(255) NOT NULL,
+ `user_id` int(10) UNSIGNED NOT NULL,
`notifyat` DATETIME DEFAULT NULL,
`dismissed` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0',
- PRIMARY KEY(`event_id`)
+ PRIMARY KEY(`event_id`),
+ CONSTRAINT `fk_kolab_alarms_user_id` FOREIGN KEY (`user_id`)
+ REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
) /*!40000 ENGINE=INNODB */;
CREATE TABLE IF NOT EXISTS `itipinvitations` (
`token` VARCHAR(64) NOT NULL,
`event_uid` VARCHAR(255) NOT NULL,
`user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
`event` TEXT NOT NULL,
`expires` DATETIME DEFAULT NULL,
`cancelled` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0',
PRIMARY KEY(`token`),
INDEX `uid_idx` (`event_uid`,`user_id`),
CONSTRAINT `fk_itipinvitations_user_id` FOREIGN KEY (`user_id`)
REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php
index ccd542ae..d0366580 100644
--- a/plugins/calendar/drivers/kolab/kolab_calendar.php
+++ b/plugins/calendar/drivers/kolab/kolab_calendar.php
@@ -1,779 +1,508 @@
<?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>
+ * 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_calendar
{
+ const COLOR_KEY_SHARED = '/shared/vendor/kolab/color';
+ const COLOR_KEY_PRIVATE = '/shared/vendor/kolab/color';
+
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 $events = array();
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);
+ $this->id = kolab_storage::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);
+ $this->storage = kolab_storage::get_folder($this->imap_folder);
+ $this->ready = $this->storage && !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)) {
+ $rights = $this->storage->get_myrights();
+ if ($rights && !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);
+ $folder = kolab_storage::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();
+ return $this->storage->get_owner();
}
/**
* 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;
+ return $this->storage->get_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 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)) {
+ $metadata = $this->storage->get_metadata(array(self::COLOR_KEY_PRIVATE, self::COLOR_KEY_SHARED));
+ if (($color = $metadata[self::COLOR_KEY_PRIVATE]) || ($color = $metadata[self::COLOR_KEY_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
+ * Return the corresponding kolab_storage_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);
+ return $this->storage;
}
/**
* Getter for a single event object
*/
public function get_event($id)
{
- $this->_fetch_events();
-
+ // directly access storage object
+ if (!$this->events[$id] && ($record = $this->storage->get_object($id)))
+ $this->events[$id] = $this->_to_rcube_event($record);
+
// 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];
+ if ($record = $this->storage->get_object($master_id))
+ $this->events[$master_id] = $this->_to_rcube_event($record);
+
+ if (($master = $this->events[$master_id]) && $master['recurrence']) {
$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)
+ * @param boolean Include virtual events (optional)
+ * @param array Additional parameters to query storage
* @return array A list of event records
*/
- public function list_events($start, $end, $search = null, $virtual = 1)
+ public function list_events($start, $end, $search = null, $virtual = 1, $query = array())
{
- $this->_fetch_events();
-
+ // query Kolab storage
+ $query[] = array('dtstart', '<=', $end);
+ $query[] = array('dtend', '>=', $start);
+
+ foreach ((array)$this->storage->select($query) as $record) {
+ $event = $this->_to_rcube_event($record);
+ $this->events[$event['id']] = $event;
+ }
+
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);
+ $saved = $this->storage->save($object, 'event');
- if (PEAR::isError($saved)) {
+ if (!$saved || 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))
+ $old = $this->storage->get_object($event['id']);
+ if (!$old || 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']);
+ $object = $this->_from_rcube_event($event, $old);
+ $saved = $this->storage->save($object, 'event', $event['id']);
- if (PEAR::isError($saved)) {
+ if (!$saved || 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);
+ $deleted = $this->storage->delete($event['id'], $force);
- if (PEAR::isError($deleteme)) {
+ if (!$deleted || PEAR::isError($deleted)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Error deleting event object from Kolab server:" . $deleteme->getMessage()),
+ 'message' => "Error deleting event object from Kolab server"),
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;
+ if ($this->storage->undelete($event['id'])) {
+ return true;
}
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()),
+ raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Error undeleting a contact object $uid from the Kolab server"),
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;
- }
- }
+ return false;
}
/**
* 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)
+ private function _to_rcube_event($record)
{
- $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
+ $record['id'] = $record['uid'];
+ $record['calendar'] = $this->id;
+
+ // convert from DateTime to unix timestamp
+ if (is_a($record['start'], 'DateTime'))
+ $record['start'] = $record['start']->format('U');
+ if (is_a($record['end'], 'DateTime'))
+ $record['end'] = $record['end']->format('U');
+
+ // all-day events go from 12:00 - 13:00
+ if ($record['end'] <= $record['start'] && $record['allday'])
+ $record['end'] = $record['start'] + 3600;
+
+ if (!empty($record['_attachments'])) {
+ foreach ($record['_attachments'] as $name => $attachment) {
+ if ($attachment !== false) {
+ $attachment['name'] = $name;
+ $attachments[] = $attachment;
+ }
}
+
+ $record['attachments'] = $attachments;
}
$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'] . ' ';
- }
-
+ $record['sensitivity'] = intval($sensitivity_map[$record['sensitivity']]);
+
// 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,
- );
+ if (is_array($record['categories']))
+ $record['categories'] = $record['categories'][0];
+
+ // remove internals
+ unset($record['_mailbox'], $record['_msguid'], $record['_formatobj'], $record['_attachments']);
+
+ return $record;
}
/**
* 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)
+ private function _from_rcube_event($event, $old = array())
{
- $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;
- }
+ $object = &$event;
// in Horde attachments are indexed by name
$object['_attachments'] = array();
- if (!empty($event['attachments'])) {
+ if (is_array($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;
+ // flagged for deletion => set to false
+ if ($attachment['_deleted']) {
+ $object['_attachments'][$attachment['name']] = false;
}
+ else {
+ // 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]);
+ $object['_attachments'][$attachment['name']] = $attachment;
+ }
}
+
+ unset($event['attachments']);
}
- // 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'],
- );
- }
+ // translate sensitivity property
+ $event['sensitivity'] = $this->sensitivity_map[$event['sensitivity']];
+
+ // set current user as ORGANIZER
+ $identity = $this->cal->rc->user->get_identity();
+ if (empty($event['attendees']) && $identity['email'])
+ $event['attendees'] = array(array('role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email']));
+
+ $event['_owner'] = $identity['email'];
+
+ // copy meta data (starting with _) from old object
+ foreach ((array)$old as $key => $val) {
+ if (!isset($event[$key]) && $key[0] == '_')
+ $event[$key] = $val;
}
- return $object;
+ return $event;
}
}
diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php
index c3375929..1a0eb9c4 100644
--- a/plugins/calendar/drivers/kolab/kolab_driver.php
+++ b/plugins/calendar/drivers/kolab/kolab_driver.php
@@ -1,1240 +1,1245 @@
<?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>
+ * 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/>.
*/
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 $alarm_types = array('DISPLAY','EMAIL');
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');
+ $folders = kolab_storage::get_folders('event');
$this->calendars = array();
if (PEAR::isError($folders)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed to list calendar folders from Kolab server:" . $folders->getMessage()),
true, false);
}
else {
// convert to UTF8 and sort
$names = array();
foreach ($folders as $folder)
$names[$folder->name] = rcube_charset::convert($folder->name, 'UTF7-IMAP');
asort($names, SORT_LOCALE_STRING);
foreach ($names as $utf7name => $name) {
$calendar = new kolab_calendar($utf7name, $this->cal);
$this->calendars[$calendar->id] = $calendar;
if (!$calendar->readonly)
$this->has_writeable = true;
}
}
return $this->calendars;
}
/**
* Get a list of available calendars from this source
*/
public function list_calendars()
{
// attempt to create a default calendar for this user
if (!$this->has_writeable) {
if ($this->create_calendar(array('name' => 'Calendar', 'color' => 'cc0000'))) {
unset($this->calendars);
$this->_read_calendars();
}
}
$calendars = $names = array();
foreach ($this->calendars as $id => $cal) {
if ($cal->ready) {
$name = $origname = $cal->get_name();
// find folder prefix to truncate (the same code as in kolab_addressbook plugin)
for ($i = count($names)-1; $i >= 0; $i--) {
if (strpos($name, $names[$i].' &raquo; ') === 0) {
$length = strlen($names[$i].' &raquo; ');
$prefix = substr($name, 0, $length);
$count = count(explode(' &raquo; ', $prefix));
$name = str_repeat('&nbsp;&nbsp;', $count-1) . '&raquo; ' . substr($name, $length);
break;
}
}
$names[] = $origname;
$calendars[$cal->id] = array(
'id' => $cal->id,
'name' => $name,
'editname' => $cal->get_foldername(),
'color' => $cal->get_color(),
'readonly' => $cal->readonly,
'showalarms' => $cal->alarms,
'class_name' => $cal->get_namespace(),
- 'active' => rcube_kolab::is_subscribed($cal->get_realname()),
+ 'active' => $cal->storage->is_subscribed(kolab_storage::SERVERSIDE_SUBSCRIPTION),
);
}
}
return $calendars;
}
/**
* Create a new calendar assigned to the current user
*
* @param array Hash array with calendar properties
* name: Calendar name
* color: The color of the calendar
* @return mixed ID of the calendar on success, False on error
*/
public function create_calendar($prop)
{
$folder = $this->folder_update($prop);
if ($folder === false) {
return false;
}
// subscribe to new calendar by default
- $storage = $this->rc->get_storage();
- $storage->subscribe($folder);
+ $storage = kolab_storage::get_folder($folder);
+ $storage->subscribe($prop['active'], kolab_storage::SERVERSIDE_SUBSCRIPTION);
// create ID
- $id = rcube_kolab::folder_id($folder);
+ $id = kolab_storage::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);
+ $id = kolab_storage::folder_id($newfolder);
// fallback to local prefs
$prefs['kolab_calendars'] = $this->rc->config->get('kolab_calendars', array());
unset($prefs['kolab_calendars'][$prop['id']]);
if (isset($prop['color']))
$prefs['kolab_calendars'][$id]['color'] = $prop['color'];
if (isset($prop['showalarms']))
$prefs['kolab_calendars'][$id]['showalarms'] = $prop['showalarms'] ? true : false;
if ($prefs['kolab_calendars'][$id])
$this->rc->user->save_prefs($prefs);
return true;
}
return false;
}
/**
* Set active/subscribed state of a calendar
*
* @see calendar_driver::subscribe_calendar()
*/
public function subscribe_calendar($prop)
{
if ($prop['id'] && ($cal = $this->calendars[$prop['id']])) {
- $storage = $this->rc->get_storage();
- if ($prop['active'])
- return $storage->subscribe($cal->get_realname());
- else
- return $storage->unsubscribe($cal->get_realname());
+ return $cal->storage->subscribe($prop['active'], kolab_storage::SERVERSIDE_SUBSCRIPTION);
}
-
+
return false;
}
/**
* Rename or Create a new IMAP folder
*
* @param array Hash array with calendar properties
*
* @return mixed New folder name or False on failure
*/
private function folder_update(&$prop)
{
$folder = rcube_charset::convert($prop['name'], RCMAIL_CHARSET, 'UTF7-IMAP');
$oldfolder = $prop['oldname']; // UTF7
$parent = $prop['parent']; // UTF7
$storage = $this->rc->get_storage();
$delimiter = $storage->get_hierarchy_delimiter();
if (strlen($oldfolder)) {
$options = $storage->folder_info($oldfolder);
}
if (!empty($options) && ($options['norename'] || $options['protected'])) {
}
// sanity checks (from steps/settings/save_folder.inc)
else if (!strlen($folder)) {
$this->last_error = 'Invalid folder name';
return false;
}
else if (strlen($folder) > 128) {
$this->last_error = 'Folder name too long';
return false;
}
else {
// these characters are problematic e.g. when used in LIST/LSUB
foreach (array($delimiter, '%', '*') as $char) {
if (strpos($folder, $delimiter) !== false) {
$this->last_error = 'Invalid folder name';
return false;
}
}
}
if (!empty($options) && ($options['protected'] || $options['norename'])) {
$folder = $oldfolder;
}
else if (strlen($parent)) {
$folder = $parent . $delimiter . $folder;
}
else {
// add namespace prefix (when needed)
$folder = $storage->mod_folder($folder, 'in');
}
// Check access rights to the parent folder
if (strlen($parent) && (!strlen($oldfolder) || $oldfolder != $folder)) {
$parent_opts = $storage->folder_info($parent);
if ($parent_opts['namespace'] != 'personal'
&& (empty($parent_opts['rights']) || !preg_match('/[ck]/', implode($parent_opts['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;
+ if (!($result = kolab_storage::folder_rename($oldfolder, $folder)))
+ $this->last_error = kolab_storage::$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;
+ if (!($result = kolab_storage::folder_create($folder, 'event')))
+ $this->last_error = kolab_storage::$last_error;
}
// save color in METADATA
// TODO: also save 'showalarams' and other properties here
if ($result && $prop['color']) {
- if (!($meta_saved = $storage->set_metadata($folder, array('/shared/vendor/kolab/color' => $prop['color'])))) // try in shared namespace
- $meta_saved = $storage->set_metadata($folder, array('/private/vendor/kolab/color' => $prop['color'])); // try in private namespace
+ if (!($meta_saved = $storage->set_metadata(array(kolab_calendar::COLOR_KEY_SHARED => $prop['color'])))) // try in shared namespace
+ $meta_saved = $storage->set_metadata(array(kolab_calendar::COLOR_KEY_PRIVATE => $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)) {
+ if (kolab_storage::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;
+ $this->last_error = kolab_storage::$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]);
+ $old['attachments'][$idx]['_deleted'] = true;
}
}
}
}
}
// 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']),
+ 'mimetype' => $attachment['mimetype'],
+ 'content' => $attachment['data'],
+ 'path' => $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']);
+ $attachment['data'] = $fromcalendar->get_attachment_body($attachment['id'], $event);
}
$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();
+ $query = array(array('tags', 'LIKE', '% x-has-alarms %'));
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) {
+ foreach ($calendar->list_events($time, $time + 86400 * 365, null, 1, $query) as $e) {
// add to list if alarm is set
- if ($e['_alarm'] && ($notifyat = $e['start'] - $e['_alarm'] * 60) <= $time) {
+ $alarm = calendar::get_next_alarm($e);
+ if ($alarm && $alarm['time'] && $alarm['time'] <= $time && $alarm['action'] == 'DISPLAY') {
$id = $e['id'];
$events[$id] = $e;
- $events[$id]['notifyat'] = $notifyat;
+ $events[$id]['notifyat'] = $alarm['time'];
}
}
}
// 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()
- ));
+ "SELECT * FROM kolab_alarms
+ WHERE event_id IN (%s) AND user_id=?",
+ join(',', $event_ids),
+ $this->rc->db->now()
+ ),
+ $this->rc->user->ID
+ );
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)
{
+ // delete old alarm entry
+ $this->rc->db->query(
+ "DELETE FROM kolab_alarms
+ WHERE event_id=? AND user_id=?",
+ $event_id,
+ $this->rc->user->ID
+ );
+
// 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(?, ?, ?)",
+ "INSERT INTO kolab_alarms
+ (event_id, user_id, dismissed, notifyat)
+ VALUES(?, ?, ?, ?)",
$event_id,
- $snooze > 0 ? 0 : 1,
+ $this->rc->user->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
+ * @see calendar_driver::get_attachment_body()
*/
public function get_attachment_body($id, $event)
{
- if (!($storage = $this->calendars[$event['calendar']]))
+ if (!($cal = $this->calendars[$event['calendar']]))
return false;
- return $storage->get_attachment_body($id);
+ return $cal->storage->get_attachment($event['id'], $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');
require_once('HTTP/Request.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
$request = new HTTP_Request($url = rcube_kolab::get_freebusy_url($email));
$result = $request->sendRequest(true);
// authentication required
if (!PEAR::isError($result) && $request->getResponseCode() == 401) {
$request->setBasicAuth($this->rc->user->get_username(), $this->rc->decrypt($_SESSION['password']));
$result = $request->sendRequest(true);
}
if (!PEAR::isError($result) && $request->getResponseCode() == 200)
$fbdata = $request->getResponseBody();
// 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]))
+ if (!($cal = $this->calendars[$cal]))
return false;
// trigger updates on folder
- $folder = $storage->get_folder();
- $trigger = $folder->trigger();
+ $trigger = $cal->storage->trigger();
if (is_object($trigger) && is_a($trigger, 'PEAR_Error')) {
raise_error(array(
'code' => 900, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed triggering folder. Error was " . $trigger->getMessage()),
true, false);
}
exit;
}
/**
* Callback function to produce driver-specific calendar create/edit form
*
* @param string Request action 'form-edit|form-new'
* @param array Calendar properties (e.g. id, color)
* @param array Edit form fields
*
* @return string HTML content of the form
*/
public function calendar_form($action, $calendar, $formfields)
{
if ($calendar['id'] && ($cal = $this->calendars[$calendar['id']])) {
$folder = $cal->get_realname(); // UTF7
$color = $cal->get_color();
}
else {
$folder = '';
$color = '';
}
$hidden_fields[] = array('name' => 'oldname', 'value' => $folder);
$storage = $this->rc->get_storage();
$delim = $storage->get_hierarchy_delimiter();
$form = array();
if (strlen($folder)) {
$path_imap = explode($delim, $folder);
array_pop($path_imap); // pop off name part
$path_imap = implode($path_imap, $delim);
$options = $storage->folder_info($folder);
}
else {
$path_imap = '';
}
// General tab
$form['props'] = array(
'name' => $this->rc->gettext('properties'),
);
// Disable folder name input
if (!empty($options) && ($options['norename'] || $options['protected'])) {
$input_name = new html_hiddenfield(array('name' => 'name', 'id' => 'calendar-name'));
- $formfields['name']['value'] = Q(str_replace($delimiter, ' &raquo; ', rcube_kolab::object_name($folder)))
+ $formfields['name']['value'] = Q(str_replace($delimiter, ' &raquo; ', kolab_storage::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);
+ $select = kolab_storage::folder_selector('event', array('name' => 'parent'), $folder);
$form['props']['fieldsets']['location']['content']['path'] = array(
'label' => $this->cal->gettext('parentcalendar'),
'value' => $select->show(strlen($folder) ? $path_imap : ''),
);
}
// calendar color (default field)
$form['props']['fieldsets']['settings'] = array(
'name' => $this->rc->gettext('settings'),
'content' => array(
'color' => $formfields['color'],
'showalarms' => $formfields['showalarms'],
),
);
if ($action != 'form-new') {
$form['sharing'] = array(
'name' => Q($this->cal->gettext('tabsharing')),
'content' => html::tag('iframe', array(
'src' => $this->cal->rc->url(array('_action' => 'calendar-acl', 'id' => $calendar['id'], 'framed' => 1)),
'width' => '100%',
'height' => 350,
'border' => 0,
'style' => 'border:0'),
''),
);
}
$this->form_html = '';
if (is_array($hidden_fields)) {
foreach ($hidden_fields as $field) {
$hiddenfield = new html_hiddenfield($field);
$this->form_html .= $hiddenfield->show() . "\n";
}
}
// Create form output
foreach ($form as $tab) {
if (!empty($tab['fieldsets']) && is_array($tab['fieldsets'])) {
$content = '';
foreach ($tab['fieldsets'] as $fieldset) {
$subcontent = $this->get_form_part($fieldset);
if ($subcontent) {
$content .= html::tag('fieldset', null, html::tag('legend', null, Q($fieldset['name'])) . $subcontent) ."\n";
}
}
}
else {
$content = $this->get_form_part($tab);
}
if ($content) {
$this->form_html .= html::tag('fieldset', null, html::tag('legend', null, Q($tab['name'])) . $content) ."\n";
}
}
// Parse form template for skin-dependent stuff
$this->rc->output->add_handler('calendarform', array($this, 'calendar_form_html'));
return $this->rc->output->parse('calendar.kolabform', false, false);
}
/**
* Handler for template object
*/
public function calendar_form_html()
{
return $this->form_html;
}
/**
* Helper function used in calendar_form_content(). Creates a part of the form.
*/
private function get_form_part($form)
{
$content = '';
if (is_array($form['content']) && !empty($form['content'])) {
$table = new html_table(array('cols' => 2));
foreach ($form['content'] as $col => $colprop) {
$colprop['id'] = '_'.$col;
$label = !empty($colprop['label']) ? $colprop['label'] : rcube_label($col);
$table->add('title', sprintf('<label for="%s">%s</label>', $colprop['id'], Q($label)));
$table->add(null, $colprop['value']);
}
$content = $table->show();
}
else {
$content = $form['content'];
}
return $content;
}
/**
* Handler to render ACL form for a calendar folder
*/
public function calendar_acl()
{
$this->rc->output->add_handler('folderacl', array($this, 'calendar_acl_form'));
$this->rc->output->send('calendar.kolabacl');
}
/**
* Handler for ACL form template object
*/
public function calendar_acl_form()
{
$calid = get_input_value('_id', RCUBE_INPUT_GPC);
if ($calid && ($cal = $this->calendars[$calid])) {
$folder = $cal->get_realname(); // UTF7
$color = $cal->get_color();
}
else {
$folder = '';
$color = '';
}
$hidden_fields[] = array('name' => 'oldname', 'value' => $folder);
$storage = $this->rc->get_storage();
$delim = $storage->get_hierarchy_delimiter();
$form = array();
if (strlen($folder)) {
$path_imap = explode($delim, $folder);
array_pop($path_imap); // pop off name part
$path_imap = implode($path_imap, $delim);
$options = $storage->folder_info($folder);
// Allow plugins to modify the form content (e.g. with ACL form)
$plugin = $this->rc->plugins->exec_hook('calendar_form_kolab',
array('form' => $form, 'options' => $options, 'name' => $folder));
}
if (!$plugin['form']['sharing']['content'])
$plugin['form']['sharing']['content'] = html::div('hint', $this->cal->gettext('aclnorights'));
return $plugin['form']['sharing']['content'];
}
/**
* Return a (limited) list of color values to be used for calendar and category coloring
*
* @return mixed List for colors as hex values or false if no presets should be shown
*/
public function get_color_values()
{
// selection from http://msdn.microsoft.com/en-us/library/aa358802%28v=VS.85%29.aspx
return array('000000','006400','2F4F4F','800000','808000','008000',
'008080','000080','800080','4B0082','191970','8B0000','008B8B',
'00008B','8B008B','556B2F','8B4513','228B22','6B8E23','2E8B57',
'B8860B','483D8B','A0522D','0000CD','A52A2A','00CED1','696969',
'20B2AA','9400D3','B22222','C71585','3CB371','D2691E','DC143C',
'DAA520','00FA9A','4682B4','7CFC00','9932CC','FF0000','FF4500',
'FF8C00','FFA500','FFD700','FFFF00','9ACD32','32CD32','00FF00',
'00FF7F','00FFFF','5F9EA0','00BFFF','0000FF','FF00FF','808080',
'708090','CD853F','8A2BE2','778899','FF1493','48D1CC','1E90FF',
'40E0D0','4169E1','6A5ACD','BDB76B','BA55D3','CD5C5C','ADFF2F',
'66CDAA','FF6347','8FBC8B','DA70D6','BC8F8F','9370DB','DB7093',
'FF7F50','6495ED','A9A9A9','F4A460','7B68EE','D2B48C','E9967A',
'DEB887','FF69B4','FA8072','F08080','EE82EE','87CEEB','FFA07A',
'F0E68C','DDA0DD','90EE90','7FFFD4','C0C0C0','87CEFA','B0C4DE',
'98FB98','ADD8E6','B0E0E6','D8BFD8','EEE8AA','AFEEEE','D3D3D3',
'FFDEAD');
}
}
diff --git a/plugins/calendar/lib/Horde_Date.php b/plugins/calendar/lib/Horde_Date.php
new file mode 100644
index 00000000..d710d722
--- /dev/null
+++ b/plugins/calendar/lib/Horde_Date.php
@@ -0,0 +1,774 @@
+<?php
+
+/**
+ * This is a copy of the Horde/Date.php class from the Horde framework
+ */
+
+define('HORDE_DATE_SUNDAY', 0);
+define('HORDE_DATE_MONDAY', 1);
+define('HORDE_DATE_TUESDAY', 2);
+define('HORDE_DATE_WEDNESDAY', 3);
+define('HORDE_DATE_THURSDAY', 4);
+define('HORDE_DATE_FRIDAY', 5);
+define('HORDE_DATE_SATURDAY', 6);
+
+define('HORDE_DATE_MASK_SUNDAY', 1);
+define('HORDE_DATE_MASK_MONDAY', 2);
+define('HORDE_DATE_MASK_TUESDAY', 4);
+define('HORDE_DATE_MASK_WEDNESDAY', 8);
+define('HORDE_DATE_MASK_THURSDAY', 16);
+define('HORDE_DATE_MASK_FRIDAY', 32);
+define('HORDE_DATE_MASK_SATURDAY', 64);
+define('HORDE_DATE_MASK_WEEKDAYS', 62);
+define('HORDE_DATE_MASK_WEEKEND', 65);
+define('HORDE_DATE_MASK_ALLDAYS', 127);
+
+define('HORDE_DATE_MASK_SECOND', 1);
+define('HORDE_DATE_MASK_MINUTE', 2);
+define('HORDE_DATE_MASK_HOUR', 4);
+define('HORDE_DATE_MASK_DAY', 8);
+define('HORDE_DATE_MASK_MONTH', 16);
+define('HORDE_DATE_MASK_YEAR', 32);
+define('HORDE_DATE_MASK_ALLPARTS', 63);
+
+/**
+ * Horde Date wrapper/logic class, including some calculation
+ * functions.
+ *
+ * $Horde: framework/Date/Date.php,v 1.8.10.18 2008/09/17 08:46:04 jan Exp $
+ *
+ * @package Horde_Date
+ */
+class Horde_Date {
+
+ /**
+ * Year
+ *
+ * @var integer
+ */
+ var $year;
+
+ /**
+ * Month
+ *
+ * @var integer
+ */
+ var $month;
+
+ /**
+ * Day
+ *
+ * @var integer
+ */
+ var $mday;
+
+ /**
+ * Hour
+ *
+ * @var integer
+ */
+ var $hour = 0;
+
+ /**
+ * Minute
+ *
+ * @var integer
+ */
+ var $min = 0;
+
+ /**
+ * Second
+ *
+ * @var integer
+ */
+ var $sec = 0;
+
+ /**
+ * Internally supported strftime() specifiers.
+ *
+ * @var string
+ */
+ var $_supportedSpecs = '%CdDeHImMnRStTyY';
+
+ /**
+ * Build a new date object. If $date contains date parts, use them to
+ * initialize the object.
+ *
+ * Recognized formats:
+ * - arrays with keys 'year', 'month', 'mday', 'day' (since Horde 3.2),
+ * 'hour', 'min', 'minute' (since Horde 3.2), 'sec'
+ * - objects with properties 'year', 'month', 'mday', 'hour', 'min', 'sec'
+ * - yyyy-mm-dd hh:mm:ss (since Horde 3.1)
+ * - yyyymmddhhmmss (since Horde 3.1)
+ * - yyyymmddThhmmssZ (since Horde 3.1.4)
+ * - unix timestamps
+ */
+ function Horde_Date($date = null)
+ {
+ if (function_exists('nl_langinfo')) {
+ $this->_supportedSpecs .= 'bBpxX';
+ }
+
+ if (is_array($date) || is_object($date)) {
+ foreach ($date as $key => $val) {
+ if (in_array($key, array('year', 'month', 'mday', 'hour', 'min', 'sec'))) {
+ $this->$key = (int)$val;
+ }
+ }
+
+ // If $date['day'] is present and numeric we may have been passed
+ // a Horde_Form_datetime array.
+ if (is_array($date) && isset($date['day']) &&
+ is_numeric($date['day'])) {
+ $this->mday = (int)$date['day'];
+ }
+ // 'minute' key also from Horde_Form_datetime
+ if (is_array($date) && isset($date['minute'])) {
+ $this->min = $date['minute'];
+ }
+ } elseif (!is_null($date)) {
+ // Match YYYY-MM-DD HH:MM:SS, YYYYMMDDHHMMSS and YYYYMMDD'T'HHMMSS'Z'.
+ if (preg_match('/(\d{4})-?(\d{2})-?(\d{2})T? ?(\d{2}):?(\d{2}):?(\d{2})Z?/', $date, $parts)) {
+ $this->year = (int)$parts[1];
+ $this->month = (int)$parts[2];
+ $this->mday = (int)$parts[3];
+ $this->hour = (int)$parts[4];
+ $this->min = (int)$parts[5];
+ $this->sec = (int)$parts[6];
+ } else {
+ // Try as a timestamp.
+ $parts = @getdate($date);
+ if ($parts) {
+ $this->year = $parts['year'];
+ $this->month = $parts['mon'];
+ $this->mday = $parts['mday'];
+ $this->hour = $parts['hours'];
+ $this->min = $parts['minutes'];
+ $this->sec = $parts['seconds'];
+ }
+ }
+ }
+ }
+
+ /**
+ * @static
+ */
+ function isLeapYear($year)
+ {
+ if (strlen($year) != 4 || preg_match('/\D/', $year)) {
+ return false;
+ }
+
+ return (($year % 4 == 0 && $year % 100 != 0) || $year % 400 == 0);
+ }
+
+ /**
+ * Returns the day of the year (1-366) that corresponds to the
+ * first day of the given week.
+ *
+ * TODO: with PHP 5.1+, see http://derickrethans.nl/calculating_start_and_end_dates_of_a_week.php
+ *
+ * @param integer $week The week of the year to find the first day of.
+ * @param integer $year The year to calculate for.
+ *
+ * @return integer The day of the year of the first day of the given week.
+ */
+ function firstDayOfWeek($week, $year)
+ {
+ $jan1 = new Horde_Date(array('year' => $year, 'month' => 1, 'mday' => 1));
+ $start = $jan1->dayOfWeek();
+ if ($start > HORDE_DATE_THURSDAY) {
+ $start -= 7;
+ }
+ return (($week * 7) - (7 + $start)) + 1;
+ }
+
+ /**
+ * @static
+ */
+ function daysInMonth($month, $year)
+ {
+ if ($month == 2) {
+ if (Horde_Date::isLeapYear($year)) {
+ return 29;
+ } else {
+ return 28;
+ }
+ } elseif ($month == 4 || $month == 6 || $month == 9 || $month == 11) {
+ return 30;
+ } else {
+ return 31;
+ }
+ }
+
+ /**
+ * Return the day of the week (0 = Sunday, 6 = Saturday) of this
+ * object's date.
+ *
+ * @return integer The day of the week.
+ */
+ function dayOfWeek()
+ {
+ if ($this->month > 2) {
+ $month = $this->month - 2;
+ $year = $this->year;
+ } else {
+ $month = $this->month + 10;
+ $year = $this->year - 1;
+ }
+
+ $day = (floor((13 * $month - 1) / 5) +
+ $this->mday + ($year % 100) +
+ floor(($year % 100) / 4) +
+ floor(($year / 100) / 4) - 2 *
+ floor($year / 100) + 77);
+
+ return (int)($day - 7 * floor($day / 7));
+ }
+
+ /**
+ * Returns the day number of the year (1 to 365/366).
+ *
+ * @return integer The day of the year.
+ */
+ function dayOfYear()
+ {
+ $monthTotals = array(0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334);
+ $dayOfYear = $this->mday + $monthTotals[$this->month - 1];
+ if (Horde_Date::isLeapYear($this->year) && $this->month > 2) {
+ ++$dayOfYear;
+ }
+
+ return $dayOfYear;
+ }
+
+ /**
+ * Returns the week of the month.
+ *
+ * @since Horde 3.2
+ *
+ * @return integer The week number.
+ */
+ function weekOfMonth()
+ {
+ return ceil($this->mday / 7);
+ }
+
+ /**
+ * Returns the week of the year, first Monday is first day of first week.
+ *
+ * @return integer The week number.
+ */
+ function weekOfYear()
+ {
+ return $this->format('W');
+ }
+
+ /**
+ * Return the number of weeks in the given year (52 or 53).
+ *
+ * @static
+ *
+ * @param integer $year The year to count the number of weeks in.
+ *
+ * @return integer $numWeeks The number of weeks in $year.
+ */
+ function weeksInYear($year)
+ {
+ // Find the last Thursday of the year.
+ $day = 31;
+ $date = new Horde_Date(array('year' => $year, 'month' => 12, 'mday' => $day, 'hour' => 0, 'min' => 0, 'sec' => 0));
+ while ($date->dayOfWeek() != HORDE_DATE_THURSDAY) {
+ --$date->mday;
+ }
+ return $date->weekOfYear();
+ }
+
+ /**
+ * Set the date of this object to the $nth weekday of $weekday.
+ *
+ * @param integer $weekday The day of the week (0 = Sunday, etc).
+ * @param integer $nth The $nth $weekday to set to (defaults to 1).
+ */
+ function setNthWeekday($weekday, $nth = 1)
+ {
+ if ($weekday < HORDE_DATE_SUNDAY || $weekday > HORDE_DATE_SATURDAY) {
+ return false;
+ }
+
+ $this->mday = 1;
+ $first = $this->dayOfWeek();
+ if ($weekday < $first) {
+ $this->mday = 8 + $weekday - $first;
+ } else {
+ $this->mday = $weekday - $first + 1;
+ }
+ $this->mday += 7 * $nth - 7;
+
+ $this->correct();
+
+ return true;
+ }
+
+ function dump($prefix = '')
+ {
+ echo ($prefix ? $prefix . ': ' : '') . $this->year . '-' . $this->month . '-' . $this->mday . "<br />\n";
+ }
+
+ /**
+ * Is the date currently represented by this object a valid date?
+ *
+ * @return boolean Validity, counting leap years, etc.
+ */
+ function isValid()
+ {
+ if ($this->year < 0 || $this->year > 9999) {
+ return false;
+ }
+ return checkdate($this->month, $this->mday, $this->year);
+ }
+
+ /**
+ * Correct any over- or underflows in any of the date's members.
+ *
+ * @param integer $mask We may not want to correct some overflows.
+ */
+ function correct($mask = HORDE_DATE_MASK_ALLPARTS)
+ {
+ if ($mask & HORDE_DATE_MASK_SECOND) {
+ while ($this->sec < 0) {
+ --$this->min;
+ $this->sec += 60;
+ }
+ while ($this->sec > 59) {
+ ++$this->min;
+ $this->sec -= 60;
+ }
+ }
+
+ if ($mask & HORDE_DATE_MASK_MINUTE) {
+ while ($this->min < 0) {
+ --$this->hour;
+ $this->min += 60;
+ }
+ while ($this->min > 59) {
+ ++$this->hour;
+ $this->min -= 60;
+ }
+ }
+
+ if ($mask & HORDE_DATE_MASK_HOUR) {
+ while ($this->hour < 0) {
+ --$this->mday;
+ $this->hour += 24;
+ }
+ while ($this->hour > 23) {
+ ++$this->mday;
+ $this->hour -= 24;
+ }
+ }
+
+ if ($mask & HORDE_DATE_MASK_MONTH) {
+ while ($this->month > 12) {
+ ++$this->year;
+ $this->month -= 12;
+ }
+ while ($this->month < 1) {
+ --$this->year;
+ $this->month += 12;
+ }
+ }
+
+ if ($mask & HORDE_DATE_MASK_DAY) {
+ while ($this->mday > Horde_Date::daysInMonth($this->month, $this->year)) {
+ $this->mday -= Horde_Date::daysInMonth($this->month, $this->year);
+ ++$this->month;
+ $this->correct(HORDE_DATE_MASK_MONTH);
+ }
+ while ($this->mday < 1) {
+ --$this->month;
+ $this->correct(HORDE_DATE_MASK_MONTH);
+ $this->mday += Horde_Date::daysInMonth($this->month, $this->year);
+ }
+ }
+ }
+
+ /**
+ * Compare this date to another date object to see which one is
+ * greater (later). Assumes that the dates are in the same
+ * timezone.
+ *
+ * @param mixed $date The date to compare to.
+ *
+ * @return integer == 0 if the dates are equal
+ * >= 1 if this date is greater (later)
+ * <= -1 if the other date is greater (later)
+ */
+ function compareDate($date)
+ {
+ if (!is_object($date) || !is_a($date, 'Horde_Date')) {
+ $date = new Horde_Date($date);
+ }
+
+ if ($this->year != $date->year) {
+ return $this->year - $date->year;
+ }
+ if ($this->month != $date->month) {
+ return $this->month - $date->month;
+ }
+
+ return $this->mday - $date->mday;
+ }
+
+ /**
+ * Compare this to another date object by time, to see which one
+ * is greater (later). Assumes that the dates are in the same
+ * timezone.
+ *
+ * @param mixed $date The date to compare to.
+ *
+ * @return integer == 0 if the dates are equal
+ * >= 1 if this date is greater (later)
+ * <= -1 if the other date is greater (later)
+ */
+ function compareTime($date)
+ {
+ if (!is_object($date) || !is_a($date, 'Horde_Date')) {
+ $date = new Horde_Date($date);
+ }
+
+ if ($this->hour != $date->hour) {
+ return $this->hour - $date->hour;
+ }
+ if ($this->min != $date->min) {
+ return $this->min - $date->min;
+ }
+
+ return $this->sec - $date->sec;
+ }
+
+ /**
+ * Compare this to another date object, including times, to see
+ * which one is greater (later). Assumes that the dates are in the
+ * same timezone.
+ *
+ * @param mixed $date The date to compare to.
+ *
+ * @return integer == 0 if the dates are equal
+ * >= 1 if this date is greater (later)
+ * <= -1 if the other date is greater (later)
+ */
+ function compareDateTime($date)
+ {
+ if (!is_object($date) || !is_a($date, 'Horde_Date')) {
+ $date = new Horde_Date($date);
+ }
+
+ if ($diff = $this->compareDate($date)) {
+ return $diff;
+ }
+
+ return $this->compareTime($date);
+ }
+
+ /**
+ * Get the time offset for local time zone.
+ *
+ * @param boolean $colon Place a colon between hours and minutes?
+ *
+ * @return string Timezone offset as a string in the format +HH:MM.
+ */
+ function tzOffset($colon = true)
+ {
+ $secs = $this->format('Z');
+
+ if ($secs < 0) {
+ $sign = '-';
+ $secs = -$secs;
+ } else {
+ $sign = '+';
+ }
+ $colon = $colon ? ':' : '';
+ $mins = intval(($secs + 30) / 60);
+ return sprintf('%s%02d%s%02d',
+ $sign, $mins / 60, $colon, $mins % 60);
+ }
+
+ /**
+ * Return the unix timestamp representation of this date.
+ *
+ * @return integer A unix timestamp.
+ */
+ function timestamp()
+ {
+ if (class_exists('DateTime')) {
+ return $this->format('U');
+ } else {
+ return Horde_Date::_mktime($this->hour, $this->min, $this->sec, $this->month, $this->mday, $this->year);
+ }
+ }
+
+ /**
+ * Return the unix timestamp representation of this date, 12:00am.
+ *
+ * @return integer A unix timestamp.
+ */
+ function datestamp()
+ {
+ if (class_exists('DateTime')) {
+ $dt = new DateTime();
+ $dt->setDate($this->year, $this->month, $this->mday);
+ $dt->setTime(0, 0, 0);
+ return $dt->format('U');
+ } else {
+ return Horde_Date::_mktime(0, 0, 0, $this->month, $this->mday, $this->year);
+ }
+ }
+
+ /**
+ * Format time using the specifiers available in date() or in the DateTime
+ * class' format() method.
+ *
+ * @since Horde 3.3
+ *
+ * @param string $format
+ *
+ * @return string Formatted time.
+ */
+ function format($format)
+ {
+ if (class_exists('DateTime')) {
+ $dt = new DateTime();
+ $dt->setDate($this->year, $this->month, $this->mday);
+ $dt->setTime($this->hour, $this->min, $this->sec);
+ return $dt->format($format);
+ } else {
+ return date($format, $this->timestamp());
+ }
+ }
+
+ /**
+ * Format time in ISO-8601 format. Works correctly since Horde 3.2.
+ *
+ * @return string Date and time in ISO-8601 format.
+ */
+ function iso8601DateTime()
+ {
+ return $this->rfc3339DateTime() . $this->tzOffset();
+ }
+
+ /**
+ * Format time in RFC 2822 format.
+ *
+ * @return string Date and time in RFC 2822 format.
+ */
+ function rfc2822DateTime()
+ {
+ return $this->format('D, j M Y H:i:s') . ' ' . $this->tzOffset(false);
+ }
+
+ /**
+ * Format time in RFC 3339 format.
+ *
+ * @since Horde 3.1
+ *
+ * @return string Date and time in RFC 3339 format. The seconds part has
+ * been added with Horde 3.2.
+ */
+ function rfc3339DateTime()
+ {
+ return $this->format('Y-m-d\TH:i:s');
+ }
+
+ /**
+ * Format time to standard 'ctime' format.
+ *
+ * @return string Date and time.
+ */
+ function cTime()
+ {
+ return $this->format('D M j H:i:s Y');
+ }
+
+ /**
+ * Format date and time using strftime() format.
+ *
+ * @since Horde 3.1
+ *
+ * @return string strftime() formatted date and time.
+ */
+ function strftime($format)
+ {
+ if (preg_match('/%[^' . $this->_supportedSpecs . ']/', $format)) {
+ return strftime($format, $this->timestamp());
+ } else {
+ return $this->_strftime($format);
+ }
+ }
+
+ /**
+ * Format date and time using a limited set of the strftime() format.
+ *
+ * @return string strftime() formatted date and time.
+ */
+ function _strftime($format)
+ {
+ if (preg_match('/%[bBpxX]/', $format)) {
+ require_once 'Horde/NLS.php';
+ }
+
+ return preg_replace(
+ array('/%b/e',
+ '/%B/e',
+ '/%C/e',
+ '/%d/e',
+ '/%D/e',
+ '/%e/e',
+ '/%H/e',
+ '/%I/e',
+ '/%m/e',
+ '/%M/e',
+ '/%n/',
+ '/%p/e',
+ '/%R/e',
+ '/%S/e',
+ '/%t/',
+ '/%T/e',
+ '/%x/e',
+ '/%X/e',
+ '/%y/e',
+ '/%Y/',
+ '/%%/'),
+ array('$this->_strftime(NLS::getLangInfo(constant(\'ABMON_\' . (int)$this->month)))',
+ '$this->_strftime(NLS::getLangInfo(constant(\'MON_\' . (int)$this->month)))',
+ '(int)($this->year / 100)',
+ 'sprintf(\'%02d\', $this->mday)',
+ '$this->_strftime(\'%m/%d/%y\')',
+ 'sprintf(\'%2d\', $this->mday)',
+ 'sprintf(\'%02d\', $this->hour)',
+ 'sprintf(\'%02d\', $this->hour == 0 ? 12 : ($this->hour > 12 ? $this->hour - 12 : $this->hour))',
+ 'sprintf(\'%02d\', $this->month)',
+ 'sprintf(\'%02d\', $this->min)',
+ "\n",
+ '$this->_strftime(NLS::getLangInfo($this->hour < 12 ? AM_STR : PM_STR))',
+ '$this->_strftime(\'%H:%M\')',
+ 'sprintf(\'%02d\', $this->sec)',
+ "\t",
+ '$this->_strftime(\'%H:%M:%S\')',
+ '$this->_strftime(NLS::getLangInfo(D_FMT))',
+ '$this->_strftime(NLS::getLangInfo(T_FMT))',
+ 'substr(sprintf(\'%04d\', $this->year), -2)',
+ (int)$this->year,
+ '%'),
+ $format);
+ }
+
+ /**
+ * mktime() implementation that supports dates outside of 1970-2038,
+ * from http://phplens.com/phpeverywhere/adodb_date_library.
+ *
+ * @TODO remove in Horde 4
+ *
+ * This does NOT work with pre-1970 daylight saving times.
+ *
+ * @static
+ */
+ function _mktime($hr, $min, $sec, $mon = false, $day = false,
+ $year = false, $is_dst = false, $is_gmt = false)
+ {
+ if ($mon === false) {
+ return $is_gmt
+ ? @gmmktime($hr, $min, $sec)
+ : @mktime($hr, $min, $sec);
+ }
+
+ if ($year > 1901 && $year < 2038 &&
+ ($year >= 1970 || version_compare(PHP_VERSION, '5.0.0', '>='))) {
+ return $is_gmt
+ ? @gmmktime($hr, $min, $sec, $mon, $day, $year)
+ : @mktime($hr, $min, $sec, $mon, $day, $year);
+ }
+
+ $gmt_different = $is_gmt
+ ? 0
+ : (mktime(0, 0, 0, 1, 2, 1970, 0) - gmmktime(0, 0, 0, 1, 2, 1970, 0));
+
+ $mon = intval($mon);
+ $day = intval($day);
+ $year = intval($year);
+
+ if ($mon > 12) {
+ $y = floor($mon / 12);
+ $year += $y;
+ $mon -= $y * 12;
+ } elseif ($mon < 1) {
+ $y = ceil((1 - $mon) / 12);
+ $year -= $y;
+ $mon += $y * 12;
+ }
+
+ $_day_power = 86400;
+ $_hour_power = 3600;
+ $_min_power = 60;
+
+ $_month_table_normal = array('', 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
+ $_month_table_leaf = array('', 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
+
+ $_total_date = 0;
+ if ($year >= 1970) {
+ for ($a = 1970; $a <= $year; $a++) {
+ $leaf = Horde_Date::isLeapYear($a);
+ if ($leaf == true) {
+ $loop_table = $_month_table_leaf;
+ $_add_date = 366;
+ } else {
+ $loop_table = $_month_table_normal;
+ $_add_date = 365;
+ }
+ if ($a < $year) {
+ $_total_date += $_add_date;
+ } else {
+ for ($b = 1; $b < $mon; $b++) {
+ $_total_date += $loop_table[$b];
+ }
+ }
+ }
+
+ return ($_total_date + $day - 1) * $_day_power + $hr * $_hour_power + $min * $_min_power + $sec + $gmt_different;
+ }
+
+ for ($a = 1969 ; $a >= $year; $a--) {
+ $leaf = Horde_Date::isLeapYear($a);
+ if ($leaf == true) {
+ $loop_table = $_month_table_leaf;
+ $_add_date = 366;
+ } else {
+ $loop_table = $_month_table_normal;
+ $_add_date = 365;
+ }
+ if ($a > $year) {
+ $_total_date += $_add_date;
+ } else {
+ for ($b = 12; $b > $mon; $b--) {
+ $_total_date += $loop_table[$b];
+ }
+ }
+ }
+
+ $_total_date += $loop_table[$mon] - $day;
+ $_day_time = $hr * $_hour_power + $min * $_min_power + $sec;
+ $_day_time = $_day_power - $_day_time;
+ $ret = -($_total_date * $_day_power + $_day_time - $gmt_different);
+ if ($ret < -12220185600) {
+ // If earlier than 5 Oct 1582 - gregorian correction.
+ return $ret + 10 * 86400;
+ } elseif ($ret < -12219321600) {
+ // If in limbo, reset to 15 Oct 1582.
+ return -12219321600;
+ } else {
+ return $ret;
+ }
+ }
+
+}
+
diff --git a/plugins/calendar/lib/Horde_Date_Recurrence.php b/plugins/calendar/lib/Horde_Date_Recurrence.php
index 68340ba3..379d54a9 100644
--- a/plugins/calendar/lib/Horde_Date_Recurrence.php
+++ b/plugins/calendar/lib/Horde_Date_Recurrence.php
@@ -1,6746 +1,5977 @@
<?php
/**
* This is a concatenated copy of the following files:
- * Horde/Date.php, PEAR/Date/Calc.php, Horde/Date/Recurrence.php
+ * PEAR/Date/Calc.php, Horde/Date/Recurrence.php
*/
-define('HORDE_DATE_SUNDAY', 0);
-define('HORDE_DATE_MONDAY', 1);
-define('HORDE_DATE_TUESDAY', 2);
-define('HORDE_DATE_WEDNESDAY', 3);
-define('HORDE_DATE_THURSDAY', 4);
-define('HORDE_DATE_FRIDAY', 5);
-define('HORDE_DATE_SATURDAY', 6);
-
-define('HORDE_DATE_MASK_SUNDAY', 1);
-define('HORDE_DATE_MASK_MONDAY', 2);
-define('HORDE_DATE_MASK_TUESDAY', 4);
-define('HORDE_DATE_MASK_WEDNESDAY', 8);
-define('HORDE_DATE_MASK_THURSDAY', 16);
-define('HORDE_DATE_MASK_FRIDAY', 32);
-define('HORDE_DATE_MASK_SATURDAY', 64);
-define('HORDE_DATE_MASK_WEEKDAYS', 62);
-define('HORDE_DATE_MASK_WEEKEND', 65);
-define('HORDE_DATE_MASK_ALLDAYS', 127);
-
-define('HORDE_DATE_MASK_SECOND', 1);
-define('HORDE_DATE_MASK_MINUTE', 2);
-define('HORDE_DATE_MASK_HOUR', 4);
-define('HORDE_DATE_MASK_DAY', 8);
-define('HORDE_DATE_MASK_MONTH', 16);
-define('HORDE_DATE_MASK_YEAR', 32);
-define('HORDE_DATE_MASK_ALLPARTS', 63);
-
-/**
- * Horde Date wrapper/logic class, including some calculation
- * functions.
- *
- * $Horde: framework/Date/Date.php,v 1.8.10.18 2008/09/17 08:46:04 jan Exp $
- *
- * @package Horde_Date
- */
-class Horde_Date {
-
- /**
- * Year
- *
- * @var integer
- */
- var $year;
-
- /**
- * Month
- *
- * @var integer
- */
- var $month;
-
- /**
- * Day
- *
- * @var integer
- */
- var $mday;
-
- /**
- * Hour
- *
- * @var integer
- */
- var $hour = 0;
-
- /**
- * Minute
- *
- * @var integer
- */
- var $min = 0;
-
- /**
- * Second
- *
- * @var integer
- */
- var $sec = 0;
-
- /**
- * Internally supported strftime() specifiers.
- *
- * @var string
- */
- var $_supportedSpecs = '%CdDeHImMnRStTyY';
-
- /**
- * Build a new date object. If $date contains date parts, use them to
- * initialize the object.
- *
- * Recognized formats:
- * - arrays with keys 'year', 'month', 'mday', 'day' (since Horde 3.2),
- * 'hour', 'min', 'minute' (since Horde 3.2), 'sec'
- * - objects with properties 'year', 'month', 'mday', 'hour', 'min', 'sec'
- * - yyyy-mm-dd hh:mm:ss (since Horde 3.1)
- * - yyyymmddhhmmss (since Horde 3.1)
- * - yyyymmddThhmmssZ (since Horde 3.1.4)
- * - unix timestamps
- */
- function Horde_Date($date = null)
- {
- if (function_exists('nl_langinfo')) {
- $this->_supportedSpecs .= 'bBpxX';
- }
-
- if (is_array($date) || is_object($date)) {
- foreach ($date as $key => $val) {
- if (in_array($key, array('year', 'month', 'mday', 'hour', 'min', 'sec'))) {
- $this->$key = (int)$val;
- }
- }
-
- // If $date['day'] is present and numeric we may have been passed
- // a Horde_Form_datetime array.
- if (is_array($date) && isset($date['day']) &&
- is_numeric($date['day'])) {
- $this->mday = (int)$date['day'];
- }
- // 'minute' key also from Horde_Form_datetime
- if (is_array($date) && isset($date['minute'])) {
- $this->min = $date['minute'];
- }
- } elseif (!is_null($date)) {
- // Match YYYY-MM-DD HH:MM:SS, YYYYMMDDHHMMSS and YYYYMMDD'T'HHMMSS'Z'.
- if (preg_match('/(\d{4})-?(\d{2})-?(\d{2})T? ?(\d{2}):?(\d{2}):?(\d{2})Z?/', $date, $parts)) {
- $this->year = (int)$parts[1];
- $this->month = (int)$parts[2];
- $this->mday = (int)$parts[3];
- $this->hour = (int)$parts[4];
- $this->min = (int)$parts[5];
- $this->sec = (int)$parts[6];
- } else {
- // Try as a timestamp.
- $parts = @getdate($date);
- if ($parts) {
- $this->year = $parts['year'];
- $this->month = $parts['mon'];
- $this->mday = $parts['mday'];
- $this->hour = $parts['hours'];
- $this->min = $parts['minutes'];
- $this->sec = $parts['seconds'];
- }
- }
- }
- }
-
- /**
- * @static
- */
- function isLeapYear($year)
- {
- if (strlen($year) != 4 || preg_match('/\D/', $year)) {
- return false;
- }
-
- return (($year % 4 == 0 && $year % 100 != 0) || $year % 400 == 0);
- }
-
- /**
- * Returns the day of the year (1-366) that corresponds to the
- * first day of the given week.
- *
- * TODO: with PHP 5.1+, see http://derickrethans.nl/calculating_start_and_end_dates_of_a_week.php
- *
- * @param integer $week The week of the year to find the first day of.
- * @param integer $year The year to calculate for.
- *
- * @return integer The day of the year of the first day of the given week.
- */
- function firstDayOfWeek($week, $year)
- {
- $jan1 = new Horde_Date(array('year' => $year, 'month' => 1, 'mday' => 1));
- $start = $jan1->dayOfWeek();
- if ($start > HORDE_DATE_THURSDAY) {
- $start -= 7;
- }
- return (($week * 7) - (7 + $start)) + 1;
- }
-
- /**
- * @static
- */
- function daysInMonth($month, $year)
- {
- if ($month == 2) {
- if (Horde_Date::isLeapYear($year)) {
- return 29;
- } else {
- return 28;
- }
- } elseif ($month == 4 || $month == 6 || $month == 9 || $month == 11) {
- return 30;
- } else {
- return 31;
- }
- }
-
- /**
- * Return the day of the week (0 = Sunday, 6 = Saturday) of this
- * object's date.
- *
- * @return integer The day of the week.
- */
- function dayOfWeek()
- {
- if ($this->month > 2) {
- $month = $this->month - 2;
- $year = $this->year;
- } else {
- $month = $this->month + 10;
- $year = $this->year - 1;
- }
-
- $day = (floor((13 * $month - 1) / 5) +
- $this->mday + ($year % 100) +
- floor(($year % 100) / 4) +
- floor(($year / 100) / 4) - 2 *
- floor($year / 100) + 77);
-
- return (int)($day - 7 * floor($day / 7));
- }
-
- /**
- * Returns the day number of the year (1 to 365/366).
- *
- * @return integer The day of the year.
- */
- function dayOfYear()
- {
- $monthTotals = array(0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334);
- $dayOfYear = $this->mday + $monthTotals[$this->month - 1];
- if (Horde_Date::isLeapYear($this->year) && $this->month > 2) {
- ++$dayOfYear;
- }
-
- return $dayOfYear;
- }
-
- /**
- * Returns the week of the month.
- *
- * @since Horde 3.2
- *
- * @return integer The week number.
- */
- function weekOfMonth()
- {
- return ceil($this->mday / 7);
- }
-
- /**
- * Returns the week of the year, first Monday is first day of first week.
- *
- * @return integer The week number.
- */
- function weekOfYear()
- {
- return $this->format('W');
- }
-
- /**
- * Return the number of weeks in the given year (52 or 53).
- *
- * @static
- *
- * @param integer $year The year to count the number of weeks in.
- *
- * @return integer $numWeeks The number of weeks in $year.
- */
- function weeksInYear($year)
- {
- // Find the last Thursday of the year.
- $day = 31;
- $date = new Horde_Date(array('year' => $year, 'month' => 12, 'mday' => $day, 'hour' => 0, 'min' => 0, 'sec' => 0));
- while ($date->dayOfWeek() != HORDE_DATE_THURSDAY) {
- --$date->mday;
- }
- return $date->weekOfYear();
- }
-
- /**
- * Set the date of this object to the $nth weekday of $weekday.
- *
- * @param integer $weekday The day of the week (0 = Sunday, etc).
- * @param integer $nth The $nth $weekday to set to (defaults to 1).
- */
- function setNthWeekday($weekday, $nth = 1)
- {
- if ($weekday < HORDE_DATE_SUNDAY || $weekday > HORDE_DATE_SATURDAY) {
- return false;
- }
-
- $this->mday = 1;
- $first = $this->dayOfWeek();
- if ($weekday < $first) {
- $this->mday = 8 + $weekday - $first;
- } else {
- $this->mday = $weekday - $first + 1;
- }
- $this->mday += 7 * $nth - 7;
-
- $this->correct();
-
- return true;
- }
-
- function dump($prefix = '')
- {
- echo ($prefix ? $prefix . ': ' : '') . $this->year . '-' . $this->month . '-' . $this->mday . "<br />\n";
- }
-
- /**
- * Is the date currently represented by this object a valid date?
- *
- * @return boolean Validity, counting leap years, etc.
- */
- function isValid()
- {
- if ($this->year < 0 || $this->year > 9999) {
- return false;
- }
- return checkdate($this->month, $this->mday, $this->year);
- }
-
- /**
- * Correct any over- or underflows in any of the date's members.
- *
- * @param integer $mask We may not want to correct some overflows.
- */
- function correct($mask = HORDE_DATE_MASK_ALLPARTS)
- {
- if ($mask & HORDE_DATE_MASK_SECOND) {
- while ($this->sec < 0) {
- --$this->min;
- $this->sec += 60;
- }
- while ($this->sec > 59) {
- ++$this->min;
- $this->sec -= 60;
- }
- }
-
- if ($mask & HORDE_DATE_MASK_MINUTE) {
- while ($this->min < 0) {
- --$this->hour;
- $this->min += 60;
- }
- while ($this->min > 59) {
- ++$this->hour;
- $this->min -= 60;
- }
- }
-
- if ($mask & HORDE_DATE_MASK_HOUR) {
- while ($this->hour < 0) {
- --$this->mday;
- $this->hour += 24;
- }
- while ($this->hour > 23) {
- ++$this->mday;
- $this->hour -= 24;
- }
- }
-
- if ($mask & HORDE_DATE_MASK_MONTH) {
- while ($this->month > 12) {
- ++$this->year;
- $this->month -= 12;
- }
- while ($this->month < 1) {
- --$this->year;
- $this->month += 12;
- }
- }
-
- if ($mask & HORDE_DATE_MASK_DAY) {
- while ($this->mday > Horde_Date::daysInMonth($this->month, $this->year)) {
- $this->mday -= Horde_Date::daysInMonth($this->month, $this->year);
- ++$this->month;
- $this->correct(HORDE_DATE_MASK_MONTH);
- }
- while ($this->mday < 1) {
- --$this->month;
- $this->correct(HORDE_DATE_MASK_MONTH);
- $this->mday += Horde_Date::daysInMonth($this->month, $this->year);
- }
- }
- }
-
- /**
- * Compare this date to another date object to see which one is
- * greater (later). Assumes that the dates are in the same
- * timezone.
- *
- * @param mixed $date The date to compare to.
- *
- * @return integer == 0 if the dates are equal
- * >= 1 if this date is greater (later)
- * <= -1 if the other date is greater (later)
- */
- function compareDate($date)
- {
- if (!is_object($date) || !is_a($date, 'Horde_Date')) {
- $date = new Horde_Date($date);
- }
-
- if ($this->year != $date->year) {
- return $this->year - $date->year;
- }
- if ($this->month != $date->month) {
- return $this->month - $date->month;
- }
-
- return $this->mday - $date->mday;
- }
-
- /**
- * Compare this to another date object by time, to see which one
- * is greater (later). Assumes that the dates are in the same
- * timezone.
- *
- * @param mixed $date The date to compare to.
- *
- * @return integer == 0 if the dates are equal
- * >= 1 if this date is greater (later)
- * <= -1 if the other date is greater (later)
- */
- function compareTime($date)
- {
- if (!is_object($date) || !is_a($date, 'Horde_Date')) {
- $date = new Horde_Date($date);
- }
-
- if ($this->hour != $date->hour) {
- return $this->hour - $date->hour;
- }
- if ($this->min != $date->min) {
- return $this->min - $date->min;
- }
-
- return $this->sec - $date->sec;
- }
-
- /**
- * Compare this to another date object, including times, to see
- * which one is greater (later). Assumes that the dates are in the
- * same timezone.
- *
- * @param mixed $date The date to compare to.
- *
- * @return integer == 0 if the dates are equal
- * >= 1 if this date is greater (later)
- * <= -1 if the other date is greater (later)
- */
- function compareDateTime($date)
- {
- if (!is_object($date) || !is_a($date, 'Horde_Date')) {
- $date = new Horde_Date($date);
- }
-
- if ($diff = $this->compareDate($date)) {
- return $diff;
- }
-
- return $this->compareTime($date);
- }
-
- /**
- * Get the time offset for local time zone.
- *
- * @param boolean $colon Place a colon between hours and minutes?
- *
- * @return string Timezone offset as a string in the format +HH:MM.
- */
- function tzOffset($colon = true)
- {
- $secs = $this->format('Z');
-
- if ($secs < 0) {
- $sign = '-';
- $secs = -$secs;
- } else {
- $sign = '+';
- }
- $colon = $colon ? ':' : '';
- $mins = intval(($secs + 30) / 60);
- return sprintf('%s%02d%s%02d',
- $sign, $mins / 60, $colon, $mins % 60);
- }
-
- /**
- * Return the unix timestamp representation of this date.
- *
- * @return integer A unix timestamp.
- */
- function timestamp()
- {
- if (class_exists('DateTime')) {
- return $this->format('U');
- } else {
- return Horde_Date::_mktime($this->hour, $this->min, $this->sec, $this->month, $this->mday, $this->year);
- }
- }
-
- /**
- * Return the unix timestamp representation of this date, 12:00am.
- *
- * @return integer A unix timestamp.
- */
- function datestamp()
- {
- if (class_exists('DateTime')) {
- $dt = new DateTime();
- $dt->setDate($this->year, $this->month, $this->mday);
- $dt->setTime(0, 0, 0);
- return $dt->format('U');
- } else {
- return Horde_Date::_mktime(0, 0, 0, $this->month, $this->mday, $this->year);
- }
- }
-
- /**
- * Format time using the specifiers available in date() or in the DateTime
- * class' format() method.
- *
- * @since Horde 3.3
- *
- * @param string $format
- *
- * @return string Formatted time.
- */
- function format($format)
- {
- if (class_exists('DateTime')) {
- $dt = new DateTime();
- $dt->setDate($this->year, $this->month, $this->mday);
- $dt->setTime($this->hour, $this->min, $this->sec);
- return $dt->format($format);
- } else {
- return date($format, $this->timestamp());
- }
- }
-
- /**
- * Format time in ISO-8601 format. Works correctly since Horde 3.2.
- *
- * @return string Date and time in ISO-8601 format.
- */
- function iso8601DateTime()
- {
- return $this->rfc3339DateTime() . $this->tzOffset();
- }
-
- /**
- * Format time in RFC 2822 format.
- *
- * @return string Date and time in RFC 2822 format.
- */
- function rfc2822DateTime()
- {
- return $this->format('D, j M Y H:i:s') . ' ' . $this->tzOffset(false);
- }
-
- /**
- * Format time in RFC 3339 format.
- *
- * @since Horde 3.1
- *
- * @return string Date and time in RFC 3339 format. The seconds part has
- * been added with Horde 3.2.
- */
- function rfc3339DateTime()
- {
- return $this->format('Y-m-d\TH:i:s');
- }
-
- /**
- * Format time to standard 'ctime' format.
- *
- * @return string Date and time.
- */
- function cTime()
- {
- return $this->format('D M j H:i:s Y');
- }
-
- /**
- * Format date and time using strftime() format.
- *
- * @since Horde 3.1
- *
- * @return string strftime() formatted date and time.
- */
- function strftime($format)
- {
- if (preg_match('/%[^' . $this->_supportedSpecs . ']/', $format)) {
- return strftime($format, $this->timestamp());
- } else {
- return $this->_strftime($format);
- }
- }
-
- /**
- * Format date and time using a limited set of the strftime() format.
- *
- * @return string strftime() formatted date and time.
- */
- function _strftime($format)
- {
- if (preg_match('/%[bBpxX]/', $format)) {
- require_once 'Horde/NLS.php';
- }
-
- return preg_replace(
- array('/%b/e',
- '/%B/e',
- '/%C/e',
- '/%d/e',
- '/%D/e',
- '/%e/e',
- '/%H/e',
- '/%I/e',
- '/%m/e',
- '/%M/e',
- '/%n/',
- '/%p/e',
- '/%R/e',
- '/%S/e',
- '/%t/',
- '/%T/e',
- '/%x/e',
- '/%X/e',
- '/%y/e',
- '/%Y/',
- '/%%/'),
- array('$this->_strftime(NLS::getLangInfo(constant(\'ABMON_\' . (int)$this->month)))',
- '$this->_strftime(NLS::getLangInfo(constant(\'MON_\' . (int)$this->month)))',
- '(int)($this->year / 100)',
- 'sprintf(\'%02d\', $this->mday)',
- '$this->_strftime(\'%m/%d/%y\')',
- 'sprintf(\'%2d\', $this->mday)',
- 'sprintf(\'%02d\', $this->hour)',
- 'sprintf(\'%02d\', $this->hour == 0 ? 12 : ($this->hour > 12 ? $this->hour - 12 : $this->hour))',
- 'sprintf(\'%02d\', $this->month)',
- 'sprintf(\'%02d\', $this->min)',
- "\n",
- '$this->_strftime(NLS::getLangInfo($this->hour < 12 ? AM_STR : PM_STR))',
- '$this->_strftime(\'%H:%M\')',
- 'sprintf(\'%02d\', $this->sec)',
- "\t",
- '$this->_strftime(\'%H:%M:%S\')',
- '$this->_strftime(NLS::getLangInfo(D_FMT))',
- '$this->_strftime(NLS::getLangInfo(T_FMT))',
- 'substr(sprintf(\'%04d\', $this->year), -2)',
- (int)$this->year,
- '%'),
- $format);
- }
-
- /**
- * mktime() implementation that supports dates outside of 1970-2038,
- * from http://phplens.com/phpeverywhere/adodb_date_library.
- *
- * @TODO remove in Horde 4
- *
- * This does NOT work with pre-1970 daylight saving times.
- *
- * @static
- */
- function _mktime($hr, $min, $sec, $mon = false, $day = false,
- $year = false, $is_dst = false, $is_gmt = false)
- {
- if ($mon === false) {
- return $is_gmt
- ? @gmmktime($hr, $min, $sec)
- : @mktime($hr, $min, $sec);
- }
-
- if ($year > 1901 && $year < 2038 &&
- ($year >= 1970 || version_compare(PHP_VERSION, '5.0.0', '>='))) {
- return $is_gmt
- ? @gmmktime($hr, $min, $sec, $mon, $day, $year)
- : @mktime($hr, $min, $sec, $mon, $day, $year);
- }
-
- $gmt_different = $is_gmt
- ? 0
- : (mktime(0, 0, 0, 1, 2, 1970, 0) - gmmktime(0, 0, 0, 1, 2, 1970, 0));
-
- $mon = intval($mon);
- $day = intval($day);
- $year = intval($year);
-
- if ($mon > 12) {
- $y = floor($mon / 12);
- $year += $y;
- $mon -= $y * 12;
- } elseif ($mon < 1) {
- $y = ceil((1 - $mon) / 12);
- $year -= $y;
- $mon += $y * 12;
- }
-
- $_day_power = 86400;
- $_hour_power = 3600;
- $_min_power = 60;
-
- $_month_table_normal = array('', 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
- $_month_table_leaf = array('', 31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31);
-
- $_total_date = 0;
- if ($year >= 1970) {
- for ($a = 1970; $a <= $year; $a++) {
- $leaf = Horde_Date::isLeapYear($a);
- if ($leaf == true) {
- $loop_table = $_month_table_leaf;
- $_add_date = 366;
- } else {
- $loop_table = $_month_table_normal;
- $_add_date = 365;
- }
- if ($a < $year) {
- $_total_date += $_add_date;
- } else {
- for ($b = 1; $b < $mon; $b++) {
- $_total_date += $loop_table[$b];
- }
- }
- }
-
- return ($_total_date + $day - 1) * $_day_power + $hr * $_hour_power + $min * $_min_power + $sec + $gmt_different;
- }
-
- for ($a = 1969 ; $a >= $year; $a--) {
- $leaf = Horde_Date::isLeapYear($a);
- if ($leaf == true) {
- $loop_table = $_month_table_leaf;
- $_add_date = 366;
- } else {
- $loop_table = $_month_table_normal;
- $_add_date = 365;
- }
- if ($a > $year) {
- $_total_date += $_add_date;
- } else {
- for ($b = 12; $b > $mon; $b--) {
- $_total_date += $loop_table[$b];
- }
- }
- }
-
- $_total_date += $loop_table[$mon] - $day;
- $_day_time = $hr * $_hour_power + $min * $_min_power + $sec;
- $_day_time = $_day_power - $_day_time;
- $ret = -($_total_date * $_day_power + $_day_time - $gmt_different);
- if ($ret < -12220185600) {
- // If earlier than 5 Oct 1582 - gregorian correction.
- return $ret + 10 * 86400;
- } elseif ($ret < -12219321600) {
- // If in limbo, reset to 15 Oct 1582.
- return -12219321600;
- } else {
- return $ret;
- }
- }
-
-}
-
-
-/* vim: set expandtab tabstop=4 shiftwidth=4 softtabstop=4 foldmethod=marker: */
+require_once(dirname(__FILE__) . '/Horde_Date.php');
// {{{ Header
/**
* Calculates, manipulates and retrieves dates
*
* It does not rely on 32-bit system time stamps, so it works dates
* before 1970 and after 2038.
*
* PHP versions 4 and 5
*
* LICENSE:
*
* Copyright (c) 1999-2007 Monte Ohrt, Pierre-Alain Joye, Daniel Convissor,
* C.A. Woodcock
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted under the terms of the BSD License.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
* FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
* BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
* LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
* ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
* POSSIBILITY OF SUCH DAMAGE.
*
* @category Date and Time
* @package Date
* @author Monte Ohrt <monte@ispi.net>
* @author Pierre-Alain Joye <pajoye@php.net>
* @author Daniel Convissor <danielc@php.net>
* @author C.A. Woodcock <c01234@netcomuk.co.uk>
* @copyright 1999-2007 Monte Ohrt, Pierre-Alain Joye, Daniel Convissor, C.A. Woodcock
* @license http://www.opensource.org/licenses/bsd-license.php
* BSD License
* @version CVS: $Id: Calc.php,v 1.57 2008/03/23 18:34:16 c01234 Exp $
* @link http://pear.php.net/package/Date
* @since File available since Release 1.2
*/
// }}}
// {{{ General constants:
if (!defined('DATE_CALC_BEGIN_WEEKDAY')) {
/**
* Defines what day starts the week
*
* Monday (1) is the international standard.
* Redefine this to 0 if you want weeks to begin on Sunday.
*/
define('DATE_CALC_BEGIN_WEEKDAY', 1);
}
if (!defined('DATE_CALC_FORMAT')) {
/**
* The default value for each method's $format parameter
*
* The default is '%Y%m%d'. To override this default, define
* this constant before including Calc.php.
*
* @since Constant available since Release 1.4.4
*/
define('DATE_CALC_FORMAT', '%Y%m%d');
}
// {{{ Date precision constants (used in 'round()' and 'trunc()'):
define('DATE_PRECISION_YEAR', -2);
define('DATE_PRECISION_MONTH', -1);
define('DATE_PRECISION_DAY', 0);
define('DATE_PRECISION_HOUR', 1);
define('DATE_PRECISION_10MINUTES', 2);
define('DATE_PRECISION_MINUTE', 3);
define('DATE_PRECISION_10SECONDS', 4);
define('DATE_PRECISION_SECOND', 5);
// }}}
// {{{ Class: Date_Calc
/**
* Calculates, manipulates and retrieves dates
*
* It does not rely on 32-bit system time stamps, so it works dates
* before 1970 and after 2038.
*
* @category Date and Time
* @package Date
* @author Monte Ohrt <monte@ispi.net>
* @author Daniel Convissor <danielc@php.net>
* @author C.A. Woodcock <c01234@netcomuk.co.uk>
* @copyright 1999-2007 Monte Ohrt, Pierre-Alain Joye, Daniel Convissor, C.A. Woodcock
* @license http://www.opensource.org/licenses/bsd-license.php
* BSD License
* @version Release: 1.5.0a1
* @link http://pear.php.net/package/Date
* @since Class available since Release 1.2
*/
class Date_Calc
{
// {{{ dateFormat()
/**
* Formats the date in the given format, much like strfmt()
*
* This function is used to alleviate the problem with 32-bit numbers for
* dates pre 1970 or post 2038, as strfmt() has on most systems.
* Most of the formatting options are compatible.
*
* Formatting options:
* <pre>
* %a abbreviated weekday name (Sun, Mon, Tue)
* %A full weekday name (Sunday, Monday, Tuesday)
* %b abbreviated month name (Jan, Feb, Mar)
* %B full month name (January, February, March)
* %d day of month (range 00 to 31)
* %e day of month, single digit (range 0 to 31)
* %E number of days since unspecified epoch (integer)
* (%E is useful for passing a date in a URL as
* an integer value. Then simply use
* daysToDate() to convert back to a date.)
* %j day of year (range 001 to 366)
* %m month as decimal number (range 1 to 12)
* %n newline character (\n)
* %t tab character (\t)
* %w weekday as decimal (0 = Sunday)
* %U week number of current year, first sunday as first week
* %y year as decimal (range 00 to 99)
* %Y year as decimal including century (range 0000 to 9999)
* %% literal '%'
* </pre>
*
* @param int $day the day of the month
* @param int $month the month
* @param int $year the year. Use the complete year instead of the
* abbreviated version. E.g. use 2005, not 05.
* @param string $format the format string
*
* @return string the date in the desired format
* @access public
* @static
*/
function dateFormat($day, $month, $year, $format)
{
if (!Date_Calc::isValidDate($day, $month, $year)) {
$year = Date_Calc::dateNow('%Y');
$month = Date_Calc::dateNow('%m');
$day = Date_Calc::dateNow('%d');
}
$output = '';
for ($strpos = 0; $strpos < strlen($format); $strpos++) {
$char = substr($format, $strpos, 1);
if ($char == '%') {
$nextchar = substr($format, $strpos + 1, 1);
switch($nextchar) {
case 'a':
$output .= Date_Calc::getWeekdayAbbrname($day, $month, $year);
break;
case 'A':
$output .= Date_Calc::getWeekdayFullname($day, $month, $year);
break;
case 'b':
$output .= Date_Calc::getMonthAbbrname($month);
break;
case 'B':
$output .= Date_Calc::getMonthFullname($month);
break;
case 'd':
$output .= sprintf('%02d', $day);
break;
case 'e':
$output .= $day;
break;
case 'E':
$output .= Date_Calc::dateToDays($day, $month, $year);
break;
case 'j':
$output .= Date_Calc::dayOfYear($day, $month, $year);
break;
case 'm':
$output .= sprintf('%02d', $month);
break;
case 'n':
$output .= "\n";
break;
case 't':
$output .= "\t";
break;
case 'w':
$output .= Date_Calc::dayOfWeek($day, $month, $year);
break;
case 'U':
$output .= Date_Calc::weekOfYear($day, $month, $year);
break;
case 'y':
$output .= sprintf('%0' .
($year < 0 ? '3' : '2') .
'd',
$year % 100);
break;
case "Y":
$output .= sprintf('%0' .
($year < 0 ? '5' : '4') .
'd',
$year);
break;
case '%':
$output .= '%';
break;
default:
$output .= $char.$nextchar;
}
$strpos++;
} else {
$output .= $char;
}
}
return $output;
}
// }}}
// {{{ dateNow()
/**
* Returns the current local date
*
* NOTE: This function retrieves the local date using strftime(),
* which may or may not be 32-bit safe on your system.
*
* @param string $format the string indicating how to format the output
*
* @return string the current date in the specified format
* @access public
* @static
*/
function dateNow($format = DATE_CALC_FORMAT)
{
return strftime($format, time());
}
// }}}
// {{{ getYear()
/**
* Returns the current local year in format CCYY
*
* @return string the current year in four digit format
* @access public
* @static
*/
function getYear()
{
return Date_Calc::dateNow('%Y');
}
// }}}
// {{{ getMonth()
/**
* Returns the current local month in format MM
*
* @return string the current month in two digit format
* @access public
* @static
*/
function getMonth()
{
return Date_Calc::dateNow('%m');
}
// }}}
// {{{ getDay()
/**
* Returns the current local day in format DD
*
* @return string the current day of the month in two digit format
* @access public
* @static
*/
function getDay()
{
return Date_Calc::dateNow('%d');
}
// }}}
// {{{ defaultCentury()
/**
* Turns a two digit year into a four digit year
*
* Return value depends on current year; the century chosen
* will be the one which forms the year that is closest
* to the current year. If the two possibilities are
* equidistant to the current year (i.e. 50 years in the past
* and 50 years in the future), then the past year is chosen.
*
* For example, if the current year is 2007:
* 03 - returns 2003
* 09 - returns 2009
* 56 - returns 2056 (closer to 2007 than 1956)
* 57 - returns 1957 (1957 and 2007 are equidistant, so previous century
* chosen)
* 58 - returns 1958
*
* @param int $year the 2 digit year
*
* @return int the 4 digit year
* @access public
* @static
*/
function defaultCentury($year)
{
$hn_century = intval(($hn_currentyear = date("Y")) / 100);
$hn_currentyear = $hn_currentyear % 100;
if ($year < 0 || $year >= 100)
$year = $year % 100;
if ($year - $hn_currentyear < -50)
return ($hn_century + 1) * 100 + $year;
else if ($year - $hn_currentyear < 50)
return $hn_century * 100 + $year;
else
return ($hn_century - 1) * 100 + $year;
}
// }}}
// {{{ getSecondsInYear()
/**
* Returns the total number of seconds in the given year
*
* This takes into account leap seconds.
*
* @param int $pn_year the year in four digit format
*
* @return int
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function getSecondsInYear($pn_year)
{
$pn_year = intval($pn_year);
static $ha_leapseconds;
if (!isset($ha_leapseconds)) {
$ha_leapseconds = array(1972 => 2,
1973 => 1,
1974 => 1,
1975 => 1,
1976 => 1,
1977 => 1,
1978 => 1,
1979 => 1,
1981 => 1,
1982 => 1,
1983 => 1,
1985 => 1,
1987 => 1,
1989 => 1,
1990 => 1,
1992 => 1,
1993 => 1,
1994 => 1,
1995 => 1,
1997 => 1,
1998 => 1,
2005 => 1);
}
$ret = Date_Calc::daysInYear($pn_year) * 86400;
if (isset($ha_leapseconds[$pn_year])) {
return $ret + $ha_leapseconds[$pn_year];
} else {
return $ret;
}
}
// }}}
// {{{ getSecondsInMonth()
/**
* Returns the total number of seconds in the given month
*
* This takes into account leap seconds.
*
* @param int $pn_month the month
* @param int $pn_year the year in four digit format
*
* @return int
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function getSecondsInMonth($pn_month, $pn_year)
{
$pn_month = intval($pn_month);
$pn_year = intval($pn_year);
static $ha_leapseconds;
if (!isset($ha_leapseconds)) {
$ha_leapseconds = array(1972 => array(6 => 1,
12 => 1),
1973 => array(12 => 1),
1974 => array(12 => 1),
1975 => array(12 => 1),
1976 => array(12 => 1),
1977 => array(12 => 1),
1978 => array(12 => 1),
1979 => array(12 => 1),
1981 => array(6 => 1),
1982 => array(6 => 1),
1983 => array(6 => 1),
1985 => array(6 => 1),
1987 => array(12 => 1),
1989 => array(12 => 1),
1990 => array(12 => 1),
1992 => array(6 => 1),
1993 => array(6 => 1),
1994 => array(6 => 1),
1995 => array(12 => 1),
1997 => array(6 => 1),
1998 => array(12 => 1),
2005 => array(12 => 1));
}
$ret = Date_Calc::daysInMonth($pn_month, $pn_year) * 86400;
if (isset($ha_leapseconds[$pn_year][$pn_month])) {
return $ret + $ha_leapseconds[$pn_year][$pn_month];
} else {
return $ret;
}
}
// }}}
// {{{ getSecondsInDay()
/**
* Returns the total number of seconds in the day of the given date
*
* This takes into account leap seconds.
*
* @param int $pn_day the day of the month
* @param int $pn_month the month
* @param int $pn_year the year in four digit format
*
* @return int
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function getSecondsInDay($pn_day, $pn_month, $pn_year)
{
// Note to developers:
//
// The leap seconds listed here are a matter of historical fact,
// that is, it is known on which exact day they occurred.
// However, the implementation of the class as a whole depends
// on the fact that they always occur at the end of the month
// (although it is assumed that they could occur in any month,
// even though practically they only occur in June or December).
//
// Do not define a leap second on a day of the month other than
// the last day without altering the implementation of the
// functions that depend on this one.
//
// It is possible, though, to define an un-leap second (i.e. a skipped
// second (I do not know what they are called), or a number of
// consecutive leap seconds).
$pn_day = intval($pn_day);
$pn_month = intval($pn_month);
$pn_year = intval($pn_year);
static $ha_leapseconds;
if (!isset($ha_leapseconds)) {
$ha_leapseconds = array(1972 => array(6 => array(30 => 1),
12 => array(31 => 1)),
1973 => array(12 => array(31 => 1)),
1974 => array(12 => array(31 => 1)),
1975 => array(12 => array(31 => 1)),
1976 => array(12 => array(31 => 1)),
1977 => array(12 => array(31 => 1)),
1978 => array(12 => array(31 => 1)),
1979 => array(12 => array(31 => 1)),
1981 => array(6 => array(30 => 1)),
1982 => array(6 => array(30 => 1)),
1983 => array(6 => array(30 => 1)),
1985 => array(6 => array(30 => 1)),
1987 => array(12 => array(31 => 1)),
1989 => array(12 => array(31 => 1)),
1990 => array(12 => array(31 => 1)),
1992 => array(6 => array(30 => 1)),
1993 => array(6 => array(30 => 1)),
1994 => array(6 => array(30 => 1)),
1995 => array(12 => array(31 => 1)),
1997 => array(6 => array(30 => 1)),
1998 => array(12 => array(31 => 1)),
2005 => array(12 => array(31 => 1)));
}
if (isset($ha_leapseconds[$pn_year][$pn_month][$pn_day])) {
return 86400 + $ha_leapseconds[$pn_year][$pn_month][$pn_day];
} else {
return 86400;
}
}
// }}}
// {{{ getSecondsInHour()
/**
* Returns the total number of seconds in the hour of the given date
*
* This takes into account leap seconds.
*
* @param int $pn_day the day of the month
* @param int $pn_month the month
* @param int $pn_year the year in four digit format
* @param int $pn_hour the hour
*
* @return int
* @access public
* @static
*/
function getSecondsInHour($pn_day, $pn_month, $pn_year, $pn_hour)
{
if ($pn_hour < 23)
return 3600;
else
return Date_Calc::getSecondsInDay($pn_day, $pn_month, $pn_year) -
82800;
}
// }}}
// {{{ getSecondsInMinute()
/**
* Returns the total number of seconds in the minute of the given hour
*
* This takes into account leap seconds.
*
* @param int $pn_day the day of the month
* @param int $pn_month the month
* @param int $pn_year the year in four digit format
* @param int $pn_hour the hour
* @param int $pn_minute the minute
*
* @return int
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function getSecondsInMinute($pn_day,
$pn_month,
$pn_year,
$pn_hour,
$pn_minute)
{
if ($pn_hour < 23 || $pn_minute < 59)
return 60;
else
return Date_Calc::getSecondsInDay($pn_day, $pn_month, $pn_year) -
86340;
}
// }}}
// {{{ secondsPastMidnight()
/**
* Returns the no of seconds since midnight (0-86399)
*
* @param int $pn_hour the hour of the day
* @param int $pn_minute the minute
* @param mixed $pn_second the second as integer or float
*
* @return mixed integer or float from 0-86399
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function secondsPastMidnight($pn_hour, $pn_minute, $pn_second)
{
return 3600 * $pn_hour + 60 * $pn_minute + $pn_second;
}
// }}}
// {{{ secondsPastMidnightToTime()
/**
* Returns the time as an array (i.e. hour, minute, second)
*
* @param mixed $pn_seconds the no of seconds since midnight (0-86399)
*
* @return mixed array of hour, minute (both as integers), second (as
* integer or float, depending on parameter)
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function secondsPastMidnightToTime($pn_seconds)
{
if ($pn_seconds >= 86400) {
return array(23, 59, $pn_seconds - 86340);
}
$hn_hour = intval($pn_seconds / 3600);
$hn_minute = intval(($pn_seconds - $hn_hour * 3600) / 60);
$hn_second = is_float($pn_seconds) ?
fmod($pn_seconds, 60) :
$pn_seconds % 60;
return array($hn_hour, $hn_minute, $hn_second);
}
// }}}
// {{{ secondsPastTheHour()
/**
* Returns the no of seconds since the last hour o'clock (0-3599)
*
* @param int $pn_minute the minute
* @param mixed $pn_second the second as integer or float
*
* @return mixed integer or float from 0-3599
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function secondsPastTheHour($pn_minute, $pn_second)
{
return 60 * $pn_minute + $pn_second;
}
// }}}
// {{{ addHours()
/**
* Returns the date the specified no of hours from the given date
*
* To subtract hours use a negative value for the '$pn_hours' parameter
*
* @param int $pn_hours hours to add
* @param int $pn_day the day of the month
* @param int $pn_month the month
* @param int $pn_year the year
* @param int $pn_hour the hour
*
* @return array array of year, month, day, hour
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function addHours($pn_hours, $pn_day, $pn_month, $pn_year, $pn_hour)
{
if ($pn_hours == 0)
return array((int) $pn_year,
(int) $pn_month,
(int) $pn_day,
(int) $pn_hour);
$hn_days = intval($pn_hours / 24);
$hn_hour = $pn_hour + $pn_hours % 24;
if ($hn_hour >= 24) {
++$hn_days;
$hn_hour -= 24;
} else if ($hn_hour < 0) {
--$hn_days;
$hn_hour += 24;
}
if ($hn_days == 0) {
$hn_year = $pn_year;
$hn_month = $pn_month;
$hn_day = $pn_day;
} else {
list($hn_year, $hn_month, $hn_day) =
explode(" ",
Date_Calc::addDays($hn_days,
$pn_day,
$pn_month,
$pn_year,
"%Y %m %d"));
}
return array((int) $hn_year, (int) $hn_month, (int) $hn_day, $hn_hour);
}
// }}}
// {{{ addMinutes()
/**
* Returns the date the specified no of minutes from the given date
*
* To subtract minutes use a negative value for the '$pn_minutes' parameter
*
* @param int $pn_minutes minutes to add
* @param int $pn_day the day of the month
* @param int $pn_month the month
* @param int $pn_year the year
* @param int $pn_hour the hour
* @param int $pn_minute the minute
*
* @return array array of year, month, day, hour, minute
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function addMinutes($pn_minutes,
$pn_day,
$pn_month,
$pn_year,
$pn_hour,
$pn_minute)
{
if ($pn_minutes == 0)
return array((int) $pn_year,
(int) $pn_month,
(int) $pn_day,
(int) $pn_hour,
(int) $pn_minute);
$hn_hours = intval($pn_minutes / 60);
$hn_minute = $pn_minute + $pn_minutes % 60;
if ($hn_minute >= 60) {
++$hn_hours;
$hn_minute -= 60;
} else if ($hn_minute < 0) {
--$hn_hours;
$hn_minute += 60;
}
if ($hn_hours == 0) {
$hn_year = $pn_year;
$hn_month = $pn_month;
$hn_day = $pn_day;
$hn_hour = $pn_hour;
} else {
list($hn_year, $hn_month, $hn_day, $hn_hour) =
Date_Calc::addHours($hn_hours,
$pn_day,
$pn_month,
$pn_year,
$pn_hour);
}
return array($hn_year, $hn_month, $hn_day, $hn_hour, $hn_minute);
}
// }}}
// {{{ addSeconds()
/**
* Returns the date the specified no of seconds from the given date
*
* If leap seconds are specified to be counted, the passed time must be UTC.
* To subtract seconds use a negative value for the '$pn_seconds' parameter.
*
* N.B. the return type of the second part of the date is float if
* either '$pn_seconds' or '$pn_second' is a float; otherwise, it
* is integer.
*
* @param mixed $pn_seconds seconds to add as integer or float
* @param int $pn_day the day of the month
* @param int $pn_month the month
* @param int $pn_year the year
* @param int $pn_hour the hour
* @param int $pn_minute the minute
* @param mixed $pn_second the second as integer or float
* @param bool $pb_countleap whether to count leap seconds (defaults to
* DATE_COUNT_LEAP_SECONDS)
*
* @return array array of year, month, day, hour, minute, second
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function addSeconds($pn_seconds,
$pn_day,
$pn_month,
$pn_year,
$pn_hour,
$pn_minute,
$pn_second,
$pb_countleap = DATE_COUNT_LEAP_SECONDS)
{
if ($pn_seconds == 0)
return array((int) $pn_year,
(int) $pn_month,
(int) $pn_day,
(int) $pn_hour,
(int) $pn_minute,
$pn_second);
if ($pb_countleap) {
$hn_seconds = $pn_seconds;
$hn_day = (int) $pn_day;
$hn_month = (int) $pn_month;
$hn_year = (int) $pn_year;
$hn_hour = (int) $pn_hour;
$hn_minute = (int) $pn_minute;
$hn_second = $pn_second;
$hn_days = Date_Calc::dateToDays($pn_day,
$pn_month,
$pn_year);
$hn_secondsofmonth = 86400 * ($hn_days -
Date_Calc::firstDayOfMonth($pn_month,
$pn_year)) +
Date_Calc::secondsPastMidnight($pn_hour,
$pn_minute,
$pn_second);
if ($hn_seconds > 0) {
// Advance to end of month:
//
if ($hn_secondsofmonth != 0 &&
$hn_secondsofmonth + $hn_seconds >=
($hn_secondsinmonth =
Date_Calc::getSecondsInMonth($hn_month, $hn_year))) {
$hn_seconds -= $hn_secondsinmonth - $hn_secondsofmonth;
$hn_secondsofmonth = 0;
list($hn_year, $hn_month) =
Date_Calc::nextMonth($hn_month, $hn_year);
$hn_day = Date_Calc::getFirstDayOfMonth($hn_month,
$hn_year);
$hn_hour = $hn_minute = $hn_second = 0;
}
// Advance to end of year:
//
if ($hn_secondsofmonth == 0 &&
$hn_month != Date_Calc::getFirstMonthOfYear($hn_year)) {
while ($hn_year == $pn_year &&
$hn_seconds >= ($hn_secondsinmonth =
Date_Calc::getSecondsInMonth($hn_month,
$hn_year))) {
$hn_seconds -= $hn_secondsinmonth;
list($hn_year, $hn_month) =
Date_Calc::nextMonth($hn_month, $hn_year);
$hn_day = Date_Calc::getFirstDayOfMonth($hn_month,
$hn_year);
}
}
if ($hn_secondsofmonth == 0) {
// Add years:
//
if ($hn_month == Date_Calc::getFirstMonthOfYear($hn_year)) {
while ($hn_seconds >= ($hn_secondsinyear =
Date_Calc::getSecondsInYear($hn_year))) {
$hn_seconds -= $hn_secondsinyear;
$hn_month = Date_Calc::getFirstMonthOfYear(++$hn_year);
$hn_day = Date_Calc::getFirstDayOfMonth($hn_month,
$hn_year);
}
}
// Add months:
//
while ($hn_seconds >= ($hn_secondsinmonth =
Date_Calc::getSecondsInMonth($hn_month, $hn_year))) {
$hn_seconds -= $hn_secondsinmonth;
list($hn_year, $hn_month) =
Date_Calc::nextMonth($hn_month, $hn_year);
$hn_day = Date_Calc::getFirstDayOfMonth($hn_month, $hn_year);
}
}
} else {
//
// (if $hn_seconds < 0)
// Go back to start of month:
//
if ($hn_secondsofmonth != 0 &&
-$hn_seconds >= $hn_secondsofmonth) {
$hn_seconds += $hn_secondsofmonth;
$hn_secondsofmonth = 0;
$hn_day = Date_Calc::getFirstDayOfMonth($hn_month,
$hn_year);
$hn_hour = $hn_minute = $hn_second = 0;
}
// Go back to start of year:
//
if ($hn_secondsofmonth == 0) {
while ($hn_month !=
Date_Calc::getFirstMonthOfYear($hn_year)) {
list($hn_year, $hn_prevmonth) =
Date_Calc::prevMonth($hn_month, $hn_year);
if (-$hn_seconds >= ($hn_secondsinmonth =
Date_Calc::getSecondsInMonth($hn_prevmonth,
$hn_year))) {
$hn_seconds += $hn_secondsinmonth;
$hn_month = $hn_prevmonth;
$hn_day = Date_Calc::getFirstDayOfMonth($hn_month,
$hn_year);
} else {
break;
}
}
}
if ($hn_secondsofmonth == 0) {
// Subtract years:
//
if ($hn_month == Date_Calc::getFirstMonthOfYear($hn_year)) {
while (-$hn_seconds >= ($hn_secondsinyear =
Date_Calc::getSecondsInYear($hn_year - 1))) {
$hn_seconds += $hn_secondsinyear;
$hn_month = Date_Calc::getFirstMonthOfYear(--$hn_year);
$hn_day = Date_Calc::getFirstDayOfMonth($hn_month,
$hn_year);
}
}
// Subtract months:
//
list($hn_pmyear, $hn_prevmonth) =
Date_Calc::prevMonth($hn_month, $hn_year);
while (-$hn_seconds >= ($hn_secondsinmonth =
Date_Calc::getSecondsInMonth($hn_prevmonth,
$hn_pmyear))) {
$hn_seconds += $hn_secondsinmonth;
$hn_year = $hn_pmyear;
$hn_month = $hn_prevmonth;
$hn_day = Date_Calc::getFirstDayOfMonth($hn_month,
$hn_year);
list($hn_pmyear, $hn_prevmonth) =
Date_Calc::prevMonth($hn_month, $hn_year);
}
}
}
if ($hn_seconds < 0 && $hn_secondsofmonth == 0) {
list($hn_year, $hn_month) =
Date_Calc::prevMonth($hn_month, $hn_year);
$hn_day = Date_Calc::getFirstDayOfMonth($hn_month, $hn_year);
$hn_seconds += Date_Calc::getSecondsInMonth($hn_month, $hn_year);
}
$hn_seconds += Date_Calc::secondsPastMidnight($hn_hour,
$hn_minute,
$hn_second);
if ($hn_seconds < 0) {
$hn_daysadd = intval($hn_seconds / 86400) - 1;
} else if ($hn_seconds < 86400) {
$hn_daysadd = 0;
} else {
$hn_daysadd = intval($hn_seconds / 86400) - 1;
}
if ($hn_daysadd != 0) {
list($hn_year, $hn_month, $hn_day) =
explode(" ",
Date_Calc::addDays($hn_daysadd,
$hn_day,
$hn_month,
$hn_year,
"%Y %m %d"));
$hn_seconds -= $hn_daysadd * 86400;
}
$hn_secondsinday = Date_Calc::getSecondsInDay($hn_day,
$hn_month,
$hn_year);
if ($hn_seconds >= $hn_secondsinday) {
list($hn_year, $hn_month, $hn_day) =
explode(" ",
Date_Calc::addDays(1,
$hn_day,
$hn_month,
$hn_year,
"%Y %m %d"));
$hn_seconds -= $hn_secondsinday;
}
list($hn_hour, $hn_minute, $hn_second) =
Date_Calc::secondsPastMidnightToTime($hn_seconds);
return array((int) $hn_year,
(int) $hn_month,
(int) $hn_day,
$hn_hour,
$hn_minute,
$hn_second);
} else {
// Assume every day has 86400 seconds exactly (ignore leap seconds):
//
$hn_minutes = intval($pn_seconds / 60);
if (is_float($pn_seconds)) {
$hn_second = $pn_second + fmod($pn_seconds, 60);
} else {
$hn_second = $pn_second + $pn_seconds % 60;
}
if ($hn_second >= 60) {
++$hn_minutes;
$hn_second -= 60;
} else if ($hn_second < 0) {
--$hn_minutes;
$hn_second += 60;
}
if ($hn_minutes == 0) {
$hn_year = $pn_year;
$hn_month = $pn_month;
$hn_day = $pn_day;
$hn_hour = $pn_hour;
$hn_minute = $pn_minute;
} else {
list($hn_year, $hn_month, $hn_day, $hn_hour, $hn_minute) =
Date_Calc::addMinutes($hn_minutes,
$pn_day,
$pn_month,
$pn_year,
$pn_hour,
$pn_minute);
}
return array($hn_year,
$hn_month,
$hn_day,
$hn_hour,
$hn_minute,
$hn_second);
}
}
// }}}
// {{{ dateToDays()
/**
* Converts a date in the proleptic Gregorian calendar to the no of days
* since 24th November, 4714 B.C.
*
* Returns the no of days since Monday, 24th November, 4714 B.C. in the
* proleptic Gregorian calendar (which is 24th November, -4713 using
* 'Astronomical' year numbering, and 1st January, 4713 B.C. in the
* proleptic Julian calendar). This is also the first day of the 'Julian
* Period' proposed by Joseph Scaliger in 1583, and the number of days
* since this date is known as the 'Julian Day'. (It is not directly
* to do with the Julian calendar, although this is where the name
* is derived from.)
*
* The algorithm is valid for all years (positive and negative), and
* also for years preceding 4714 B.C.
*
* @param int $day the day of the month
* @param int $month the month
* @param int $year the year (using 'Astronomical' year numbering)
*
* @return int the number of days since 24th November, 4714 B.C.
* @access public
* @static
*/
function dateToDays($day, $month, $year)
{
if ($month > 2) {
// March = 0, April = 1, ..., December = 9,
// January = 10, February = 11
$month -= 3;
} else {
$month += 9;
--$year;
}
$hb_negativeyear = $year < 0;
$century = intval($year / 100);
$year = $year % 100;
if ($hb_negativeyear) {
// Subtract 1 because year 0 is a leap year;
// And N.B. that we must treat the leap years as occurring
// one year earlier than they do, because for the purposes
// of calculation, the year starts on 1st March:
//
return intval((14609700 * $century + ($year == 0 ? 1 : 0)) / 400) +
intval((1461 * $year + 1) / 4) +
intval((153 * $month + 2) / 5) +
$day + 1721118;
} else {
return intval(146097 * $century / 4) +
intval(1461 * $year / 4) +
intval((153 * $month + 2) / 5) +
$day + 1721119;
}
}
// }}}
// {{{ daysToDate()
/**
* Converts no of days since 24th November, 4714 B.C. (in the proleptic
* Gregorian calendar, which is year -4713 using 'Astronomical' year
* numbering) to Gregorian calendar date
*
* Returned date belongs to the proleptic Gregorian calendar, using
* 'Astronomical' year numbering.
*
* The algorithm is valid for all years (positive and negative), and
* also for years preceding 4714 B.C. (i.e. for negative 'Julian Days'),
* and so the only limitation is platform-dependent (for 32-bit systems
* the maximum year would be something like about 1,465,190 A.D.).
*
* N.B. Monday, 24th November, 4714 B.C. is Julian Day '0'.
*
* @param int $days the number of days since 24th November, 4714 B.C.
* @param string $format the string indicating how to format the output
*
* @return string the date in the desired format
* @access public
* @static
*/
function daysToDate($days, $format = DATE_CALC_FORMAT)
{
$days = intval($days);
$days -= 1721119;
$century = floor((4 * $days - 1) / 146097);
$days = floor(4 * $days - 1 - 146097 * $century);
$day = floor($days / 4);
$year = floor((4 * $day + 3) / 1461);
$day = floor(4 * $day + 3 - 1461 * $year);
$day = floor(($day + 4) / 4);
$month = floor((5 * $day - 3) / 153);
$day = floor(5 * $day - 3 - 153 * $month);
$day = floor(($day + 5) / 5);
$year = $century * 100 + $year;
if ($month < 10) {
$month +=3;
} else {
$month -=9;
++$year;
}
return Date_Calc::dateFormat($day, $month, $year, $format);
}
// }}}
// {{{ getMonths()
/**
* Returns array of the month numbers, in order, for the given year
*
* @param int $pn_year the year (using 'Astronomical' year numbering)
*
* @return array array of integer month numbers, in order
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function getMonths($pn_year)
{
// N.B. Month numbers can be skipped but not duplicated:
//
return array(1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12);
}
// }}}
// {{{ getMonthNames()
/**
* Returns an array of month names
*
* Used to take advantage of the setlocale function to return
* language specific month names.
*
* TODO: cache values to some global array to avoid performance
* hits when called more than once.
*
* @param int $pb_abbreviated whether to return the abbreviated form of the
* months
*
* @return array associative array of integer month numbers, in
* order, to month names
* @access public
* @static
*/
function getMonthNames($pb_abbreviated = false)
{
$ret = array();
foreach (Date_Calc::getMonths(2001) as $i) {
$ret[$i] = strftime($pb_abbreviated ? '%b' : '%B',
mktime(0, 0, 0, $i, 1, 2001));
}
return $ret;
}
// }}}
// {{{ prevMonth()
/**
* Returns month and year of previous month
*
* @param int $pn_month the month
* @param int $pn_year the year (using 'Astronomical' year numbering)
*
* @return array array of year, month as integers
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function prevMonth($pn_month, $pn_year)
{
$ha_months = Date_Calc::getMonths($pn_year);
$hn_monthkey = array_search($pn_month, $ha_months);
if (array_key_exists($hn_monthkey - 1, $ha_months)) {
return array((int) $pn_year, $ha_months[$hn_monthkey - 1]);
} else {
$ha_months = Date_Calc::getMonths($pn_year - 1);
return array($pn_year - 1, end($ha_months));
}
}
// }}}
// {{{ nextMonth()
/**
* Returns month and year of next month
*
* @param int $pn_month the month
* @param int $pn_year the year (using 'Astronomical' year numbering)
*
* @return array array of year, month as integers
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function nextMonth($pn_month, $pn_year)
{
$ha_months = Date_Calc::getMonths($pn_year);
$hn_monthkey = array_search($pn_month, $ha_months);
if (array_key_exists($hn_monthkey + 1, $ha_months)) {
return array((int) $pn_year, $ha_months[$hn_monthkey + 1]);
} else {
$ha_months = Date_Calc::getMonths($pn_year + 1);
return array($pn_year + 1, $ha_months[0]);
}
}
// }}}
// {{{ addMonthsToDays()
/**
* Returns 'Julian Day' of the date the specified no of months
* from the given date
*
* To subtract months use a negative value for the '$pn_months'
* parameter
*
* @param int $pn_months months to add
* @param int $pn_days 'Julian Day', i.e. the no of days since 1st
* January, 4713 B.C.
*
* @return int 'Julian Day', i.e. the no of days since 1st January,
* 4713 B.C.
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function addMonthsToDays($pn_months, $pn_days)
{
if ($pn_months == 0)
return (int) $pn_days;
list($hn_year, $hn_month, $hn_day) =
explode(" ", Date_Calc::daysToDate($pn_days, "%Y %m %d"));
$hn_retmonth = $hn_month + $pn_months % 12;
$hn_retyear = $hn_year + intval($pn_months / 12);
if ($hn_retmonth < 1) {
$hn_retmonth += 12;
--$hn_retyear;
} else if ($hn_retmonth > 12) {
$hn_retmonth -= 12;
++$hn_retyear;
}
if (Date_Calc::isValidDate($hn_day, $hn_retmonth, $hn_retyear))
return Date_Calc::dateToDays($hn_day, $hn_retmonth, $hn_retyear);
// Calculate days since first of month:
//
$hn_dayoffset = $pn_days -
Date_Calc::firstDayOfMonth($hn_month, $hn_year);
$hn_retmonthfirstday = Date_Calc::firstDayOfMonth($hn_retmonth,
$hn_retyear);
$hn_retmonthlastday = Date_Calc::lastDayOfMonth($hn_retmonth,
$hn_retyear);
if ($hn_dayoffset > $hn_retmonthlastday - $hn_retmonthfirstday) {
return $hn_retmonthlastday;
} else {
return $hn_retmonthfirstday + $hn_dayoffset;
}
}
// }}}
// {{{ addMonths()
/**
* Returns the date the specified no of months from the given date
*
* To subtract months use a negative value for the '$pn_months'
* parameter
*
* @param int $pn_months months to add
* @param int $pn_day the day of the month, default is current local
* day
* @param int $pn_month the month, default is current local month
* @param int $pn_year the year in four digit format, default is
* current local year
* @param string $ps_format string specifying how to format the output
*
* @return string the date in the desired format
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function addMonths($pn_months,
$pn_day,
$pn_month,
$pn_year,
$ps_format = DATE_CALC_FORMAT)
{
if (is_null($pn_year)) {
$pn_year = Date_Calc::dateNow('%Y');
}
if (empty($pn_month)) {
$pn_month = Date_Calc::dateNow('%m');
}
if (empty($pn_day)) {
$pn_day = Date_Calc::dateNow('%d');
}
if ($pn_months == 0)
return Date_Calc::dateFormat($pn_day,
$pn_month,
$pn_year,
$ps_format);
$hn_days = Date_Calc::dateToDays($pn_day, $pn_month, $pn_year);
return Date_Calc::daysToDate(Date_Calc::addMonthsToDays($pn_months,
$hn_days),
$ps_format);
}
// }}}
// {{{ addYearsToDays()
/**
* Returns 'Julian Day' of the date the specified no of years
* from the given date
*
* To subtract years use a negative value for the '$pn_years'
* parameter
*
* @param int $pn_years years to add
* @param int $pn_days 'Julian Day', i.e. the no of days since 1st January,
* 4713 B.C.
*
* @return int 'Julian Day', i.e. the no of days since 1st January,
* 4713 B.C.
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function addYearsToDays($pn_years, $pn_days)
{
if ($pn_years == 0)
return (int) $pn_days;
list($hn_year, $hn_month, $hn_day) =
explode(" ", Date_Calc::daysToDate($pn_days, "%Y %m %d"));
$hn_retyear = $hn_year + $pn_years;
if (Date_Calc::isValidDate($hn_day, $hn_month, $hn_retyear))
return Date_Calc::dateToDays($hn_day, $hn_month, $hn_retyear);
$ha_months = Date_Calc::getMonths($hn_retyear);
if (in_array($hn_month, $ha_months)) {
$hn_retmonth = $hn_month;
// Calculate days since first of month:
//
$hn_dayoffset = $pn_days - Date_Calc::firstDayOfMonth($hn_month,
$hn_year);
$hn_retmonthfirstday = Date_Calc::firstDayOfMonth($hn_retmonth,
$hn_retyear);
$hn_retmonthlastday = Date_Calc::lastDayOfMonth($hn_retmonth,
$hn_retyear);
if ($hn_dayoffset > $hn_retmonthlastday - $hn_retmonthfirstday) {
return $hn_retmonthlastday;
} else {
return $hn_retmonthfirstday + $hn_dayoffset;
}
} else {
// Calculate days since first of year:
//
$hn_dayoffset = $pn_days - Date_Calc::firstDayOfYear($hn_year);
$hn_retyearfirstday = Date_Calc::firstDayOfYear($hn_retyear);
$hn_retyearlastday = Date_Calc::lastDayOfYear($hn_retyear);
if ($hn_dayoffset > $hn_retyearlastday - $hn_retyearfirstday) {
return $hn_retyearlastday;
} else {
return $hn_retyearfirstday + $hn_dayoffset;
}
}
}
// }}}
// {{{ addYears()
/**
* Returns the date the specified no of years from the given date
*
* To subtract years use a negative value for the '$pn_years'
* parameter
*
* @param int $pn_years years to add
* @param int $pn_day the day of the month, default is current local
* day
* @param int $pn_month the month, default is current local month
* @param int $pn_year the year in four digit format, default is
* current local year
* @param string $ps_format string specifying how to format the output
*
* @return string the date in the desired format
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function addYears($pn_years,
$pn_day,
$pn_month,
$pn_year,
$ps_format = DATE_CALC_FORMAT)
{
if (is_null($pn_year)) {
$pn_year = Date_Calc::dateNow('%Y');
}
if (empty($pn_month)) {
$pn_month = Date_Calc::dateNow('%m');
}
if (empty($pn_day)) {
$pn_day = Date_Calc::dateNow('%d');
}
if ($pn_years == 0)
return Date_Calc::dateFormat($pn_day,
$pn_month,
$pn_year,
$ps_format);
$hn_days = Date_Calc::dateToDays($pn_day, $pn_month, $pn_year);
return Date_Calc::daysToDate(Date_Calc::addYearsToDays($pn_years,
$hn_days),
$ps_format);
}
// }}}
// {{{ addDays()
/**
* Returns the date the specified no of days from the given date
*
* To subtract days use a negative value for the '$pn_days' parameter
*
* @param int $pn_days days to add
* @param int $pn_day the day of the month, default is current local
* day
* @param int $pn_month the month, default is current local month
* @param int $pn_year the year in four digit format, default is
* current local year
* @param string $ps_format string specifying how to format the output
*
* @return string the date in the desired format
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function addDays($pn_days,
$pn_day,
$pn_month,
$pn_year,
$ps_format = DATE_CALC_FORMAT)
{
if (is_null($pn_year)) {
$pn_year = Date_Calc::dateNow('%Y');
}
if (empty($pn_month)) {
$pn_month = Date_Calc::dateNow('%m');
}
if (empty($pn_day)) {
$pn_day = Date_Calc::dateNow('%d');
}
if ($pn_days == 0)
return Date_Calc::dateFormat($pn_day,
$pn_month,
$pn_year,
$ps_format);
return Date_Calc::daysToDate(Date_Calc::dateToDays($pn_day,
$pn_month,
$pn_year) +
$pn_days,
$ps_format);
}
// }}}
// {{{ getFirstDayOfMonth()
/**
* Returns first day of the specified month of specified year as integer
*
* @param int $pn_month the month
* @param int $pn_year the year (using 'Astronomical' year numbering)
*
* @return int number of first day of month
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function getFirstDayOfMonth($pn_month, $pn_year)
{
return 1;
}
// }}}
// {{{ getLastDayOfMonth()
/**
* Returns last day of the specified month of specified year as integer
*
* @param int $pn_month the month
* @param int $pn_year the year (using 'Astronomical' year numbering)
*
* @return int number of last day of month
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function getLastDayOfMonth($pn_month, $pn_year)
{
return Date_Calc::daysInMonth($pn_month, $pn_year);
}
// }}}
// {{{ firstDayOfMonth()
/**
* Returns the Julian Day of the first day of the month of the specified
* year (i.e. the no of days since 24th November, 4714 B.C.)
*
* @param int $pn_month the month
* @param int $pn_year the year (using 'Astronomical' year numbering)
*
* @return integer the number of days since 24th November, 4714 B.C.
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function firstDayOfMonth($pn_month, $pn_year)
{
return Date_Calc::dateToDays(Date_Calc::getFirstDayOfMonth($pn_month,
$pn_year),
$pn_month,
$pn_year);
}
// }}}
// {{{ lastDayOfMonth()
/**
* Returns the Julian Day of the last day of the month of the specified
* year (i.e. the no of days since 24th November, 4714 B.C.)
*
* @param int $pn_month the month
* @param int $pn_year the year (using 'Astronomical' year numbering)
*
* @return integer the number of days since 24th November, 4714 B.C.
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function lastDayOfMonth($pn_month, $pn_year)
{
list($hn_nmyear, $hn_nextmonth) = Date_Calc::nextMonth($pn_month,
$pn_year);
return Date_Calc::firstDayOfMonth($hn_nextmonth, $hn_nmyear) - 1;
}
// }}}
// {{{ getFirstMonthOfYear()
/**
* Returns first month of specified year as integer
*
* @param int $pn_year the year (using 'Astronomical' year numbering)
*
* @return int number of first month of year
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function getFirstMonthOfYear($pn_year)
{
$ha_months = Date_Calc::getMonths($pn_year);
return $ha_months[0];
}
// }}}
// {{{ firstDayOfYear()
/**
* Returns the Julian Day of the first day of the year (i.e. the no of
* days since 24th November, 4714 B.C.)
*
* @param int $pn_year the year (using 'Astronomical' year numbering)
*
* @return integer the number of days since 24th November, 4714 B.C.
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function firstDayOfYear($pn_year)
{
return Date_Calc::firstDayOfMonth(Date_Calc::getFirstMonthOfYear($pn_year),
$pn_year);
}
// }}}
// {{{ lastDayOfYear()
/**
* Returns the Julian Day of the last day of the year (i.e. the no of
* days since 24th November, 4714 B.C.)
*
* @param int $pn_year the year (using 'Astronomical' year numbering)
*
* @return integer the number of days since 24th November, 4714 B.C.
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function lastDayOfYear($pn_year)
{
return Date_Calc::firstDayOfYear($pn_year + 1) - 1;
}
// }}}
// {{{ dateToDaysJulian()
/**
* Converts a date in the proleptic Julian calendar to the no of days
* since 1st January, 4713 B.C.
*
* Returns the no of days since Monday, 1st January, 4713 B.C. in the
* proleptic Julian calendar (which is 1st January, -4712 using
* 'Astronomical' year numbering, and 24th November, 4713 B.C. in the
* proleptic Gregorian calendar). This is also the first day of the 'Julian
* Period' proposed by Joseph Scaliger in 1583, and the number of days
* since this date is known as the 'Julian Day'. (It is not directly
* to do with the Julian calendar, although this is where the name
* is derived from.)
*
* The algorithm is valid for all years (positive and negative), and
* also for years preceding 4713 B.C.
*
* @param int $day the day of the month
* @param int $month the month
* @param int $year the year (using 'Astronomical' year numbering)
*
* @return int the number of days since 1st January, 4713 B.C.
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function dateToDaysJulian($day, $month, $year)
{
if ($month > 2) {
// March = 0, April = 1, ..., December = 9,
// January = 10, February = 11
$month -= 3;
} else {
$month += 9;
--$year;
}
$hb_negativeyear = $year < 0;
if ($hb_negativeyear) {
// Subtract 1 because year 0 is a leap year;
// And N.B. that we must treat the leap years as occurring
// one year earlier than they do, because for the purposes
// of calculation, the year starts on 1st March:
//
return intval((1461 * $year + 1) / 4) +
intval((153 * $month + 2) / 5) +
$day + 1721116;
} else {
return intval(1461 * $year / 4) +
floor((153 * $month + 2) / 5) +
$day + 1721117;
}
}
// }}}
// {{{ daysToDateJulian()
/**
* Converts no of days since 1st January, 4713 B.C. (in the proleptic
* Julian calendar, which is year -4712 using 'Astronomical' year
* numbering) to Julian calendar date
*
* Returned date belongs to the proleptic Julian calendar, using
* 'Astronomical' year numbering.
*
* @param int $days the number of days since 1st January, 4713 B.C.
* @param string $format the string indicating how to format the output
*
* @return string the date in the desired format
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function daysToDateJulian($days, $format = DATE_CALC_FORMAT)
{
$days = intval($days);
$days -= 1721117;
$days = floor(4 * $days - 1);
$day = floor($days / 4);
$year = floor((4 * $day + 3) / 1461);
$day = floor(4 * $day + 3 - 1461 * $year);
$day = floor(($day + 4) / 4);
$month = floor((5 * $day - 3) / 153);
$day = floor(5 * $day - 3 - 153 * $month);
$day = floor(($day + 5) / 5);
if ($month < 10) {
$month +=3;
} else {
$month -=9;
++$year;
}
return Date_Calc::dateFormat($day, $month, $year, $format);
}
// }}}
// {{{ isoWeekDate()
/**
* Returns array defining the 'ISO Week Date' as defined in ISO 8601
*
* Expects a date in the proleptic Gregorian calendar using 'Astronomical'
* year numbering, that is, with a year 0. Algorithm is valid for all
* years (positive and negative).
*
* N.B. the ISO week day no for Sunday is defined as 7, whereas this
* class and its related functions defines Sunday as 0.
*
* @param int $pn_day the day of the month
* @param int $pn_month the month
* @param int $pn_year the year
*
* @return array array of ISO Year, ISO Week No, ISO Day No as
* integers
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function isoWeekDate($pn_day = 0, $pn_month = 0, $pn_year = null)
{
if (is_null($pn_year)) {
$pn_year = Date_Calc::dateNow('%Y');
}
if (empty($pn_month)) {
$pn_month = Date_Calc::dateNow('%m');
}
if (empty($pn_day)) {
$pn_day = Date_Calc::dateNow('%d');
}
$hn_jd = Date_Calc::dateToDays($pn_day, $pn_month, $pn_year);
$hn_wd = Date_Calc::daysToDayOfWeek($hn_jd);
if ($hn_wd == 0)
$hn_wd = 7;
$hn_jd1 = Date_Calc::firstDayOfYear($pn_year);
$hn_day = $hn_jd - $hn_jd1 + 1;
if ($hn_wd <= $hn_jd - Date_Calc::lastDayOfYear($pn_year) + 3) {
// ISO week is the first week of the next ISO year:
//
$hn_year = $pn_year + 1;
$hn_isoweek = 1;
} else {
switch ($hn_wd1 = Date_Calc::daysToDayOfWeek($hn_jd1)) {
case 1:
case 2:
case 3:
case 4:
// Monday - Thursday:
//
$hn_year = $pn_year;
$hn_isoweek = floor(($hn_day + $hn_wd1 - 2) / 7) + 1;
break;
case 0:
$hn_wd1 = 7;
case 5:
case 6:
// Friday - Sunday:
//
if ($hn_day <= 8 - $hn_wd1) {
// ISO week is the last week of the previous ISO year:
//
list($hn_year, $hn_lastmonth, $hn_lastday) =
explode(" ",
Date_Calc::daysToDate($hn_jd1 - 1, "%Y %m %d"));
list($hn_year, $hn_isoweek, $hn_pisoday) =
Date_Calc::isoWeekDate($hn_lastday,
$hn_lastmonth,
$hn_year);
} else {
$hn_year = $pn_year;
$hn_isoweek = floor(($hn_day + $hn_wd1 - 9) / 7) + 1;
}
break;
}
}
return array((int) $hn_year, (int) $hn_isoweek, (int) $hn_wd);
}
// }}}
// {{{ gregorianToISO()
/**
* Converts from Gregorian Year-Month-Day to ISO Year-WeekNumber-WeekDay
*
* Uses ISO 8601 definitions.
*
* @param int $day the day of the month
* @param int $month the month
* @param int $year the year. Use the complete year instead of the
* abbreviated version. E.g. use 2005, not 05.
*
* @return string the date in ISO Year-WeekNumber-WeekDay format
* @access public
* @static
*/
function gregorianToISO($day, $month, $year)
{
list($yearnumber, $weeknumber, $weekday) =
Date_Calc::isoWeekDate($day, $month, $year);
return sprintf("%04d", $yearnumber) .
'-' .
sprintf("%02d", $weeknumber) .
'-' .
$weekday;
}
// }}}
// {{{ weekOfYear4th()
/**
* Returns week of the year counting week 1 as the week that contains 4th
* January
*
* Week 1 is determined to be the week that includes the 4th January, and
* therefore can be defined as the first week of the year that has at least
* 4 days. The previous week is counted as week 52 or 53 of the previous
* year. Note that this definition depends on which day is the first day of
* the week, and that if this is not passed as the '$pn_firstdayofweek'
* parameter, the default is assumed.
*
* Note also that the last day week of the year is likely to extend into
* the following year, except in the case that the last day of the week
* falls on 31st December.
*
* Also note that this is very similar to the ISO week returned by
* 'isoWeekDate()', the difference being that the ISO week always has
* 7 days, and if the 4th of January is a Friday, for example,
* ISO week 1 would start on Monday, 31st December in the previous year,
* whereas the week defined by this function would start on 1st January,
* but would be only 6 days long. Of course you can also set the day
* of the week, whereas the ISO week starts on a Monday by definition.
*
* Returned week is an integer from 1 to 53.
*
* @param int $pn_day the day of the month, default is current
* local day
* @param int $pn_month the month, default is current local month
* @param int $pn_year the year in four digit format, default is
* current local year
* @param int $pn_firstdayofweek optional integer specifying the first day
* of the week
*
* @return array array of year, week no as integers
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function weekOfYear4th($pn_day = 0,
$pn_month = 0,
$pn_year = null,
$pn_firstdayofweek = DATE_CALC_BEGIN_WEEKDAY)
{
if (is_null($pn_year)) {
$pn_year = Date_Calc::dateNow('%Y');
}
if (empty($pn_month)) {
$pn_month = Date_Calc::dateNow('%m');
}
if (empty($pn_day)) {
$pn_day = Date_Calc::dateNow('%d');
}
$hn_wd1 = Date_Calc::daysToDayOfWeek(Date_Calc::firstDayOfYear($pn_year));
$hn_day = Date_Calc::dayOfYear($pn_day, $pn_month, $pn_year);
$hn_week = floor(($hn_day +
(10 + $hn_wd1 - $pn_firstdayofweek) % 7 +
3) / 7);
if ($hn_week > 0) {
$hn_year = $pn_year;
} else {
// Week number is the last week of the previous year:
//
list($hn_year, $hn_lastmonth, $hn_lastday) =
explode(" ",
Date_Calc::daysToDate(Date_Calc::lastDayOfYear($pn_year - 1),
"%Y %m %d"));
list($hn_year, $hn_week) =
Date_Calc::weekOfYear4th($hn_lastday,
$hn_lastmonth,
$hn_year,
$pn_firstdayofweek);
}
return array((int) $hn_year, (int) $hn_week);
}
// }}}
// {{{ weekOfYear7th()
/**
* Returns week of the year counting week 1 as the week that contains 7th
* January
*
* Week 1 is determined to be the week that includes the 7th January, and
* therefore can be defined as the first full week of the year. The
* previous week is counted as week 52 or 53 of the previous year. Note
* that this definition depends on which day is the first day of the week,
* and that if this is not passed as the '$pn_firstdayofweek' parameter, the
* default is assumed.
*
* Note also that the last day week of the year is likely to extend into
* the following year, except in the case that the last day of the week
* falls on 31st December.
*
* Returned week is an integer from 1 to 53.
*
* @param int $pn_day the day of the month, default is current
* local day
* @param int $pn_month the month, default is current local month
* @param int $pn_year the year in four digit format, default is
* current local year
* @param int $pn_firstdayofweek optional integer specifying the first day
* of the week
*
* @return array array of year, week no as integers
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function weekOfYear7th($pn_day = 0,
$pn_month = 0,
$pn_year = null,
$pn_firstdayofweek = DATE_CALC_BEGIN_WEEKDAY)
{
if (is_null($pn_year)) {
$pn_year = Date_Calc::dateNow('%Y');
}
if (empty($pn_month)) {
$pn_month = Date_Calc::dateNow('%m');
}
if (empty($pn_day)) {
$pn_day = Date_Calc::dateNow('%d');
}
$hn_wd1 = Date_Calc::daysToDayOfWeek(Date_Calc::firstDayOfYear($pn_year));
$hn_day = Date_Calc::dayOfYear($pn_day, $pn_month, $pn_year);
$hn_week = floor(($hn_day + (6 + $hn_wd1 - $pn_firstdayofweek) % 7) / 7);
if ($hn_week > 0) {
$hn_year = $pn_year;
} else {
// Week number is the last week of the previous ISO year:
//
list($hn_year, $hn_lastmonth, $hn_lastday) = explode(" ", Date_Calc::daysToDate(Date_Calc::lastDayOfYear($pn_year - 1), "%Y %m %d"));
list($hn_year, $hn_week) = Date_Calc::weekOfYear7th($hn_lastday, $hn_lastmonth, $hn_year, $pn_firstdayofweek);
}
return array((int) $hn_year, (int) $hn_week);
}
// }}}
// {{{ dateSeason()
/**
* Determines julian date of the given season
*
* Adapted from previous work in Java by James Mark Hamilton.
*
* @param string $season the season to get the date for: VERNALEQUINOX,
* SUMMERSOLSTICE, AUTUMNALEQUINOX,
* or WINTERSOLSTICE
* @param string $year the year in four digit format. Must be between
* -1000 B.C. and 3000 A.D.
*
* @return float the julian date the season starts on
* @access public
* @static
*/
function dateSeason($season, $year = 0)
{
if ($year == '') {
$year = Date_Calc::dateNow('%Y');
}
if (($year >= -1000) && ($year <= 1000)) {
$y = $year / 1000.0;
switch ($season) {
case 'VERNALEQUINOX':
$juliandate = (((((((-0.00071 * $y) - 0.00111) * $y) + 0.06134) * $y) + 365242.1374) * $y) + 1721139.29189;
break;
case 'SUMMERSOLSTICE':
$juliandate = (((((((0.00025 * $y) + 0.00907) * $y) - 0.05323) * $y) + 365241.72562) * $y) + 1721233.25401;
break;
case 'AUTUMNALEQUINOX':
$juliandate = (((((((0.00074 * $y) - 0.00297) * $y) - 0.11677) * $y) + 365242.49558) * $y) + 1721325.70455;
break;
case 'WINTERSOLSTICE':
default:
$juliandate = (((((((-0.00006 * $y) - 0.00933) * $y) - 0.00769) * $y) + 365242.88257) * $y) + 1721414.39987;
}
} elseif (($year > 1000) && ($year <= 3000)) {
$y = ($year - 2000) / 1000;
switch ($season) {
case 'VERNALEQUINOX':
$juliandate = (((((((-0.00057 * $y) - 0.00411) * $y) + 0.05169) * $y) + 365242.37404) * $y) + 2451623.80984;
break;
case 'SUMMERSOLSTICE':
$juliandate = (((((((-0.0003 * $y) + 0.00888) * $y) + 0.00325) * $y) + 365241.62603) * $y) + 2451716.56767;
break;
case 'AUTUMNALEQUINOX':
$juliandate = (((((((0.00078 * $y) + 0.00337) * $y) - 0.11575) * $y) + 365242.01767) * $y) + 2451810.21715;
break;
case 'WINTERSOLSTICE':
default:
$juliandate = (((((((0.00032 * $y) - 0.00823) * $y) - 0.06223) * $y) + 365242.74049) * $y) + 2451900.05952;
}
}
return $juliandate;
}
// }}}
// {{{ dayOfYear()
/**
* Returns number of days since 31 December of year before given date
*
* @param int $pn_day the day of the month, default is current local day
* @param int $pn_month the month, default is current local month
* @param int $pn_year the year in four digit format, default is current
* local year
*
* @return int
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function dayOfYear($pn_day = 0, $pn_month = 0, $pn_year = null)
{
if (is_null($pn_year)) {
$pn_year = Date_Calc::dateNow('%Y');
}
if (empty($pn_month)) {
$pn_month = Date_Calc::dateNow('%m');
}
if (empty($pn_day)) {
$pn_day = Date_Calc::dateNow('%d');
}
$hn_jd = Date_Calc::dateToDays($pn_day, $pn_month, $pn_year);
$hn_jd1 = Date_Calc::firstDayOfYear($pn_year);
return $hn_jd - $hn_jd1 + 1;
}
// }}}
// {{{ julianDate()
/**
* Returns number of days since 31 December of year before given date
*
* @param int $pn_day the day of the month, default is current local day
* @param int $pn_month the month, default is current local month
* @param int $pn_year the year in four digit format, default is current
* local year
*
* @return int
* @access public
* @static
* @deprecated Method deprecated in Release 1.5.0
*/
function julianDate($pn_day = 0, $pn_month = 0, $pn_year = null)
{
return Date_Calc::dayOfYear($pn_day, $pn_month, $pn_year);
}
// }}}
// {{{ getWeekdayFullname()
/**
* Returns the full weekday name for the given date
*
* @param int $pn_day the day of the month, default is current local day
* @param int $pn_month the month, default is current local month
* @param int $pn_year the year in four digit format, default is current
* local year
*
* @return string the full name of the day of the week
* @access public
* @static
*/
function getWeekdayFullname($pn_day = 0, $pn_month = 0, $pn_year = null)
{
if (is_null($pn_year)) {
$pn_year = Date_Calc::dateNow('%Y');
}
if (empty($pn_month)) {
$pn_month = Date_Calc::dateNow('%m');
}
if (empty($pn_day)) {
$pn_day = Date_Calc::dateNow('%d');
}
$weekday_names = Date_Calc::getWeekDays();
$weekday = Date_Calc::dayOfWeek($pn_day, $pn_month, $pn_year);
return $weekday_names[$weekday];
}
// }}}
// {{{ getWeekdayAbbrname()
/**
* Returns the abbreviated weekday name for the given date
*
* @param int $pn_day the day of the month, default is current local day
* @param int $pn_month the month, default is current local month
* @param int $pn_year the year in four digit format, default is current
* local year
* @param int $length the length of abbreviation
*
* @return string the abbreviated name of the day of the week
* @access public
* @static
* @see Date_Calc::getWeekdayFullname()
*/
function getWeekdayAbbrname($pn_day = 0,
$pn_month = 0,
$pn_year = null,
$length = 3)
{
if (is_null($pn_year)) {
$pn_year = Date_Calc::dateNow('%Y');
}
if (empty($pn_month)) {
$pn_month = Date_Calc::dateNow('%m');
}
if (empty($pn_day)) {
$pn_day = Date_Calc::dateNow('%d');
}
$weekday_names = Date_Calc::getWeekDays(true);
$weekday = Date_Calc::dayOfWeek($pn_day, $pn_month, $pn_year);
return $weekday_names[$weekday];
}
// }}}
// {{{ getMonthFullname()
/**
* Returns the full month name for the given month
*
* @param int $month the month
*
* @return string the full name of the month
* @access public
* @static
*/
function getMonthFullname($month)
{
$month = (int)$month;
if (empty($month)) {
$month = (int)Date_Calc::dateNow('%m');
}
$month_names = Date_Calc::getMonthNames();
return $month_names[$month];
}
// }}}
// {{{ getMonthAbbrname()
/**
* Returns the abbreviated month name for the given month
*
* @param int $month the month
* @param int $length the length of abbreviation
*
* @return string the abbreviated name of the month
* @access public
* @static
* @see Date_Calc::getMonthFullname
*/
function getMonthAbbrname($month, $length = 3)
{
$month = (int)$month;
if (empty($month)) {
$month = Date_Calc::dateNow('%m');
}
$month_names = Date_Calc::getMonthNames(true);
return $month_names[$month];
}
// }}}
// {{{ getMonthFromFullname()
/**
* Returns the numeric month from the month name or an abreviation
*
* Both August and Aug would return 8.
*
* @param string $month the name of the month to examine.
* Case insensitive.
*
* @return int the month's number
* @access public
* @static
*/
function getMonthFromFullName($month)
{
$month = strtolower($month);
$months = Date_Calc::getMonthNames();
while (list($id, $name) = each($months)) {
if (ereg($month, strtolower($name))) {
return $id;
}
}
return 0;
}
// }}}
// {{{ getWeekDays()
/**
* Returns an array of week day names
*
* Used to take advantage of the setlocale function to return language
* specific week days.
*
* @param int $pb_abbreviated whether to return the abbreviated form of the
* days
*
* @return array an array of week-day names
* @access public
* @static
*/
function getWeekDays($pb_abbreviated = false)
{
for ($i = 0; $i < 7; $i++) {
$weekdays[$i] = strftime($pb_abbreviated ? '%a' : '%A',
mktime(0, 0, 0, 1, $i, 2001));
}
return $weekdays;
}
// }}}
// {{{ daysToDayOfWeek()
/**
* Returns day of week for specified 'Julian Day'
*
* The algorithm is valid for all years (positive and negative), and
* also for years preceding 4714 B.C. (i.e. for negative 'Julian Days'),
* and so the only limitation is platform-dependent (for 32-bit systems
* the maximum year would be something like about 1,465,190 A.D.).
*
* N.B. Monday, 24th November, 4714 B.C. is Julian Day '0'.
*
* @param int $pn_days the number of days since 24th November, 4714 B.C.
*
* @return int integer from 0 to 7 where 0 represents Sunday
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function daysToDayOfWeek($pn_days)
{
// On Julian day 0 the day is Monday (PHP day 1):
//
$ret = ($pn_days + 1) % 7;
return $ret < 0 ? $ret + 7 : $ret;
}
// }}}
// {{{ dayOfWeek()
/**
* Returns day of week for given date (0 = Sunday)
*
* The algorithm is valid for all years (positive and negative).
*
* @param int $day the day of the month, default is current local day
* @param int $month the month, default is current local month
* @param int $year the year in four digit format, default is current
* local year
*
* @return int the number of the day in the week
* @access public
* @static
*/
function dayOfWeek($day = null, $month = null, $year = null)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
if (empty($month)) {
$month = Date_Calc::dateNow('%m');
}
if (empty($day)) {
$day = Date_Calc::dateNow('%d');
}
// if ($month <= 2) {
// $month += 12;
// --$year;
// }
// $wd = ($day +
// intval((13 * $month + 3) / 5) +
// $year +
// floor($year / 4) -
// floor($year / 100) +
// floor($year / 400) +
// 1) % 7;
// return (int) ($wd < 0 ? $wd + 7 : $wd);
return Date_Calc::daysToDayOfWeek(Date_Calc::dateToDays($day,
$month,
$year));
}
// }}}
// {{{ weekOfYearAbsolute()
/**
* Returns week of the year counting week 1 as 1st-7th January,
* regardless of what day 1st January falls on
*
* Returned value is an integer from 1 to 53. Week 53 will start on
* 31st December and have only one day, except in a leap year, in
* which it will start a day earlier and contain two days.
*
* @param int $pn_day the day of the month, default is current local day
* @param int $pn_month the month, default is current local month
* @param int $pn_year the year in four digit format, default is current
* local year
*
* @return int integer from 1 to 53
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function weekOfYearAbsolute($pn_day = 0, $pn_month = 0, $pn_year = null)
{
if (is_null($pn_year)) {
$pn_year = Date_Calc::dateNow('%Y');
}
if (empty($pn_month)) {
$pn_month = Date_Calc::dateNow('%m');
}
if (empty($pn_day)) {
$pn_day = Date_Calc::dateNow('%d');
}
$hn_day = Date_Calc::dayOfYear($pn_day, $pn_month, $pn_year);
return intval(($hn_day + 6) / 7);
}
// }}}
// {{{ weekOfYear1st()
/**
* Returns week of the year counting week 1 as the week that contains 1st
* January
*
* Week 1 is determined to be the week that includes the 1st January, even
* if this week extends into the previous year, in which case the week will
* only contain between 1 and 6 days of the current year. Note that this
* definition depends on which day is the first day of the week, and that if
* this is not passed as the '$pn_firstdayofweek' parameter, the default is
* assumed.
*
* Note also that the last day week of the year is also likely to contain
* less than seven days, except in the case that the last day of the week
* falls on 31st December.
*
* Returned value is an integer from 1 to 54. The year will only contain
* 54 weeks in the case of a leap year in which 1st January is the last day
* of the week, and 31st December is the first day of the week. In this
* case, both weeks 1 and 54 will contain one day only.
*
* @param int $pn_day the day of the month, default is current
* local day
* @param int $pn_month the month, default is current local month
* @param int $pn_year the year in four digit format, default is
* current local year
* @param int $pn_firstdayofweek optional integer specifying the first day
* of the week
*
* @return int integer from 1 to 54
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function weekOfYear1st($pn_day = 0,
$pn_month = 0,
$pn_year = null,
$pn_firstdayofweek = DATE_CALC_BEGIN_WEEKDAY)
{
if (is_null($pn_year)) {
$pn_year = Date_Calc::dateNow('%Y');
}
if (empty($pn_month)) {
$pn_month = Date_Calc::dateNow('%m');
}
if (empty($pn_day)) {
$pn_day = Date_Calc::dateNow('%d');
}
$hn_wd1 = Date_Calc::daysToDayOfWeek(Date_Calc::firstDayOfYear($pn_year));
$hn_day = Date_Calc::dayOfYear($pn_day, $pn_month, $pn_year);
return floor(($hn_day + (7 + $hn_wd1 - $pn_firstdayofweek) % 7 + 6) / 7);
}
// }}}
// {{{ weekOfYear()
/**
* Returns week of the year, where first Sunday is first day of first week
*
* N.B. this function is equivalent to calling:
*
* <code>Date_Calc::weekOfYear7th($day, $month, $year, 0)</code>
*
* Returned week is an integer from 1 to 53.
*
* @param int $pn_day the day of the month, default is current local day
* @param int $pn_month the month, default is current local month
* @param int $pn_year the year in four digit format, default is current
* local year
*
* @return int integer from 1 to 53
* @access public
* @static
* @see Date_Calc::weekOfYear7th
* @deprecated Method deprecated in Release 1.5.0
*/
function weekOfYear($pn_day = 0, $pn_month = 0, $pn_year = null)
{
$ha_week = Date_Calc::weekOfYear7th($pn_day, $pn_month, $pn_year, 0);
return $ha_week[1];
}
// }}}
// {{{ weekOfMonthAbsolute()
/**
* Returns week of the month counting week 1 as 1st-7th of the month,
* regardless of what day the 1st falls on
*
* Returned value is an integer from 1 to 5. Week 5 will start on
* the 29th of the month and have between 1 and 3 days, except
* in February in a non-leap year, when there will be 4 weeks only.
*
* @param int $pn_day the day of the month, default is current local day
*
* @return int integer from 1 to 5
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function weekOfMonthAbsolute($pn_day = 0)
{
if (empty($pn_day)) {
$pn_day = Date_Calc::dateNow('%d');
}
return intval(($pn_day + 6) / 7);
}
// }}}
// {{{ weekOfMonth()
/**
* Alias for 'weekOfMonthAbsolute()'
*
* @param int $pn_day the day of the month, default is current local day
*
* @return int integer from 1 to 5
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function weekOfMonth($pn_day = 0)
{
return Date_Calc::weekOfMonthAbsolute($pn_day);
}
// }}}
// {{{ quarterOfYear()
/**
* Returns quarter of the year for given date
*
* @param int $day the day of the month, default is current local day
* @param int $month the month, default is current local month
* @param int $year the year in four digit format, default is current
* local year
*
* @return int the number of the quarter in the year
* @access public
* @static
*/
function quarterOfYear($day = 0, $month = 0, $year = null)
{
if (empty($month)) {
$month = Date_Calc::dateNow('%m');
}
return intval(($month - 1) / 3 + 1);
}
// }}}
// {{{ daysInMonth()
/**
* Returns the number of days in the given month
*
* @param int $month the month, default is current local month
* @param int $year the year in four digit format, default is current
* local year
*
* @return int the number of days the month has
* @access public
* @static
*/
function daysInMonth($month = 0, $year = null)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
if (empty($month)) {
$month = Date_Calc::dateNow('%m');
}
return Date_Calc::lastDayOfMonth($month, $year) -
Date_Calc::firstDayOfMonth($month, $year) +
1;
}
// }}}
// {{{ daysInYear()
/**
* Returns the number of days in the given year
*
* @param int $year the year in four digit format, default is current local
* year
*
* @return int the number of days the year has
* @access public
* @static
*/
function daysInYear($year = null)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
return Date_Calc::firstDayOfYear($year + 1) -
Date_Calc::firstDayOfYear($year);
}
// }}}
// {{{ weeksInMonth()
/**
* Returns the number of rows on a calendar month
*
* Useful for determining the number of rows when displaying a typical
* month calendar.
*
* @param int $month the month, default is current local month
* @param int $year the year in four digit format, default is current
* local year
*
* @return int the number of weeks the month has
* @access public
* @static
*/
function weeksInMonth($month = 0, $year = null)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
if (empty($month)) {
$month = Date_Calc::dateNow('%m');
}
$FDOM = Date_Calc::firstOfMonthWeekday($month, $year);
if (DATE_CALC_BEGIN_WEEKDAY==1 && $FDOM==0) {
$first_week_days = 7 - $FDOM + DATE_CALC_BEGIN_WEEKDAY;
$weeks = 1;
} elseif (DATE_CALC_BEGIN_WEEKDAY==0 && $FDOM == 6) {
$first_week_days = 7 - $FDOM + DATE_CALC_BEGIN_WEEKDAY;
$weeks = 1;
} else {
$first_week_days = DATE_CALC_BEGIN_WEEKDAY - $FDOM;
$weeks = 0;
}
$first_week_days %= 7;
return ceil((Date_Calc::daysInMonth($month, $year)
- $first_week_days) / 7) + $weeks;
}
// }}}
// {{{ getCalendarWeek()
/**
* Return an array with days in week
*
* @param int $day the day of the month, default is current local day
* @param int $month the month, default is current local month
* @param int $year the year in four digit format, default is current
* local year
* @param string $format the string indicating how to format the output
*
* @return array $week[$weekday]
* @access public
* @static
*/
function getCalendarWeek($day = 0, $month = 0, $year = null,
$format = DATE_CALC_FORMAT)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
if (empty($month)) {
$month = Date_Calc::dateNow('%m');
}
if (empty($day)) {
$day = Date_Calc::dateNow('%d');
}
$week_array = array();
// date for the column of week
$curr_day = Date_Calc::beginOfWeek($day, $month, $year, '%E');
for ($counter = 0; $counter <= 6; $counter++) {
$week_array[$counter] = Date_Calc::daysToDate($curr_day, $format);
$curr_day++;
}
return $week_array;
}
// }}}
// {{{ getCalendarMonth()
/**
* Return a set of arrays to construct a calendar month for the given date
*
* @param int $month the month, default is current local month
* @param int $year the year in four digit format, default is current
* local year
* @param string $format the string indicating how to format the output
*
* @return array $month[$row][$col]
* @access public
* @static
*/
function getCalendarMonth($month = 0, $year = null,
$format = DATE_CALC_FORMAT)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
if (empty($month)) {
$month = Date_Calc::dateNow('%m');
}
$month_array = array();
// date for the first row, first column of calendar month
if (DATE_CALC_BEGIN_WEEKDAY == 1) {
if (Date_Calc::firstOfMonthWeekday($month, $year) == 0) {
$curr_day = Date_Calc::firstDayOfMonth($month, $year) - 6;
} else {
$curr_day = Date_Calc::firstDayOfMonth($month, $year)
- Date_Calc::firstOfMonthWeekday($month, $year) + 1;
}
} else {
$curr_day = (Date_Calc::firstDayOfMonth($month, $year)
- Date_Calc::firstOfMonthWeekday($month, $year));
}
// number of days in this month
$daysInMonth = Date_Calc::daysInMonth($month, $year);
$weeksInMonth = Date_Calc::weeksInMonth($month, $year);
for ($row_counter = 0; $row_counter < $weeksInMonth; $row_counter++) {
for ($column_counter = 0; $column_counter <= 6; $column_counter++) {
$month_array[$row_counter][$column_counter] =
Date_Calc::daysToDate($curr_day, $format);
$curr_day++;
}
}
return $month_array;
}
// }}}
// {{{ getCalendarYear()
/**
* Return a set of arrays to construct a calendar year for the given date
*
* @param int $year the year in four digit format, default current
* local year
* @param string $format the string indicating how to format the output
*
* @return array $year[$month][$row][$col]
* @access public
* @static
*/
function getCalendarYear($year = null, $format = DATE_CALC_FORMAT)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
$year_array = array();
for ($curr_month = 0; $curr_month <= 11; $curr_month++) {
$year_array[$curr_month] =
Date_Calc::getCalendarMonth($curr_month + 1,
$year, $format);
}
return $year_array;
}
// }}}
// {{{ prevDay()
/**
* Returns date of day before given date
*
* @param int $day the day of the month, default is current local day
* @param int $month the month, default is current local month
* @param int $year the year in four digit format, default is current
* local year
* @param string $format the string indicating how to format the output
*
* @return string the date in the desired format
* @access public
* @static
*/
function prevDay($day = 0, $month = 0, $year = null,
$format = DATE_CALC_FORMAT)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
if (empty($month)) {
$month = Date_Calc::dateNow('%m');
}
if (empty($day)) {
$day = Date_Calc::dateNow('%d');
}
return Date_Calc::addDays(-1, $day, $month, $year, $format);
}
// }}}
// {{{ nextDay()
/**
* Returns date of day after given date
*
* @param int $day the day of the month, default is current local day
* @param int $month the month, default is current local month
* @param int $year the year in four digit format, default is current
* local year
* @param string $format the string indicating how to format the output
*
* @return string the date in the desired format
* @access public
* @static
*/
function nextDay($day = 0,
$month = 0,
$year = null,
$format = DATE_CALC_FORMAT)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
if (empty($month)) {
$month = Date_Calc::dateNow('%m');
}
if (empty($day)) {
$day = Date_Calc::dateNow('%d');
}
return Date_Calc::addDays(1, $day, $month, $year, $format);
}
// }}}
// {{{ prevWeekday()
/**
* Returns date of the previous weekday, skipping from Monday to Friday
*
* @param int $day the day of the month, default is current local day
* @param int $month the month, default is current local month
* @param int $year the year in four digit format, default is current
* local year
* @param string $format the string indicating how to format the output
*
* @return string the date in the desired format
* @access public
* @static
*/
function prevWeekday($day = 0, $month = 0, $year = null,
$format = DATE_CALC_FORMAT)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
if (empty($month)) {
$month = Date_Calc::dateNow('%m');
}
if (empty($day)) {
$day = Date_Calc::dateNow('%d');
}
$days = Date_Calc::dateToDays($day, $month, $year);
if (Date_Calc::dayOfWeek($day, $month, $year) == 1) {
$days -= 3;
} elseif (Date_Calc::dayOfWeek($day, $month, $year) == 0) {
$days -= 2;
} else {
$days -= 1;
}
return Date_Calc::daysToDate($days, $format);
}
// }}}
// {{{ nextWeekday()
/**
* Returns date of the next weekday of given date, skipping from
* Friday to Monday
*
* @param int $day the day of the month, default is current local day
* @param int $month the month, default is current local month
* @param int $year the year in four digit format, default is current
* local year
* @param string $format the string indicating how to format the output
*
* @return string the date in the desired format
* @access public
* @static
*/
function nextWeekday($day = 0, $month = 0, $year = null,
$format = DATE_CALC_FORMAT)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
if (empty($month)) {
$month = Date_Calc::dateNow('%m');
}
if (empty($day)) {
$day = Date_Calc::dateNow('%d');
}
$days = Date_Calc::dateToDays($day, $month, $year);
if (Date_Calc::dayOfWeek($day, $month, $year) == 5) {
$days += 3;
} elseif (Date_Calc::dayOfWeek($day, $month, $year) == 6) {
$days += 2;
} else {
$days += 1;
}
return Date_Calc::daysToDate($days, $format);
}
// }}}
// {{{ daysToPrevDayOfWeek()
/**
* Returns 'Julian Day' of the previous specific day of the week
* from the given date.
*
* @param int $dow the day of the week (0 = Sunday)
* @param int $days 'Julian Day', i.e. the no of days since 1st
* January, 4713 B.C.
* @param bool $onorbefore if true and days are same, returns current day
*
* @return int 'Julian Day', i.e. the no of days since 1st January,
* 4713 B.C.
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function daysToPrevDayOfWeek($dow, $days, $onorbefore = false)
{
$curr_weekday = Date_Calc::daysToDayOfWeek($days);
if ($curr_weekday == $dow) {
if ($onorbefore) {
return $days;
} else {
return $days - 7;
}
} else if ($curr_weekday < $dow) {
return $days - 7 + $dow - $curr_weekday;
} else {
return $days - $curr_weekday + $dow;
}
}
// }}}
// {{{ prevDayOfWeek()
/**
* Returns date of the previous specific day of the week
* from the given date
*
* @param int $dow the day of the week (0 = Sunday)
* @param int $day the day of the month, default is current local
* day
* @param int $month the month, default is current local month
* @param int $year the year in four digit format, default is
* current local year
* @param string $format the string indicating how to format the output
* @param bool $onorbefore if true and days are same, returns current day
*
* @return string the date in the desired format
* @access public
* @static
*/
function prevDayOfWeek($dow,
$day = 0,
$month = 0,
$year = null,
$format = DATE_CALC_FORMAT,
$onorbefore = false)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
if (empty($month)) {
$month = Date_Calc::dateNow('%m');
}
if (empty($day)) {
$day = Date_Calc::dateNow('%d');
}
$days = Date_Calc::dateToDays($day, $month, $year);
$days = Date_Calc::daysToPrevDayOfWeek($dow, $days, $onorbefore);
return Date_Calc::daysToDate($days, $format);
}
// }}}
// {{{ daysToNextDayOfWeek()
/**
* Returns 'Julian Day' of the next specific day of the week
* from the given date.
*
* @param int $dow the day of the week (0 = Sunday)
* @param int $days 'Julian Day', i.e. the no of days since 1st
* January, 4713 B.C.
* @param bool $onorafter if true and days are same, returns current day
*
* @return int 'Julian Day', i.e. the no of days since 1st January,
* 4713 B.C.
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function daysToNextDayOfWeek($dow, $days, $onorafter = false)
{
$curr_weekday = Date_Calc::daysToDayOfWeek($days);
if ($curr_weekday == $dow) {
if ($onorafter) {
return $days;
} else {
return $days + 7;
}
} else if ($curr_weekday > $dow) {
return $days + 7 - $curr_weekday + $dow;
} else {
return $days + $dow - $curr_weekday;
}
}
// }}}
// {{{ nextDayOfWeek()
/**
* Returns date of the next specific day of the week
* from the given date
*
* @param int $dow the day of the week (0 = Sunday)
* @param int $day the day of the month, default is current local
* day
* @param int $month the month, default is current local month
* @param int $year the year in four digit format, default is
* current local year
* @param string $format the string indicating how to format the output
* @param bool $onorafter if true and days are same, returns current day
*
* @return string the date in the desired format
* @access public
* @static
*/
function nextDayOfWeek($dow,
$day = 0,
$month = 0,
$year = null,
$format = DATE_CALC_FORMAT,
$onorafter = false)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
if (empty($month)) {
$month = Date_Calc::dateNow('%m');
}
if (empty($day)) {
$day = Date_Calc::dateNow('%d');
}
$days = Date_Calc::dateToDays($day, $month, $year);
$days = Date_Calc::daysToNextDayOfWeek($dow, $days, $onorafter);
return Date_Calc::daysToDate($days, $format);
}
// }}}
// {{{ prevDayOfWeekOnOrBefore()
/**
* Returns date of the previous specific day of the week
* on or before the given date
*
* @param int $dow the day of the week (0 = Sunday)
* @param int $day the day of the month, default is current local day
* @param int $month the month, default is current local month
* @param int $year the year in four digit format, default is current
* local year
* @param string $format the string indicating how to format the output
*
* @return string the date in the desired format
* @access public
* @static
*/
function prevDayOfWeekOnOrBefore($dow,
$day = 0,
$month = 0,
$year = null,
$format = DATE_CALC_FORMAT)
{
return Date_Calc::prevDayOfWeek($dow,
$day,
$month,
$year,
$format,
true);
}
// }}}
// {{{ nextDayOfWeekOnOrAfter()
/**
* Returns date of the next specific day of the week
* on or after the given date
*
* @param int $dow the day of the week (0 = Sunday)
* @param int $day the day of the month, default is current local day
* @param int $month the month, default is current local month
* @param int $year the year in four digit format, default is current
* local year
* @param string $format the string indicating how to format the output
*
* @return string the date in the desired format
* @access public
* @static
*/
function nextDayOfWeekOnOrAfter($dow,
$day = 0,
$month = 0,
$year = null,
$format = DATE_CALC_FORMAT)
{
return Date_Calc::nextDayOfWeek($dow,
$day,
$month,
$year,
$format,
true);
}
// }}}
// {{{ beginOfWeek()
/**
* Find the month day of the beginning of week for given date,
* using DATE_CALC_BEGIN_WEEKDAY
*
* Can return weekday of prev month.
*
* @param int $day the day of the month, default is current local day
* @param int $month the month, default is current local month
* @param int $year the year in four digit format, default is current
* local year
* @param string $format the string indicating how to format the output
*
* @return string the date in the desired format
* @access public
* @static
*/
function beginOfWeek($day = 0, $month = 0, $year = null,
$format = DATE_CALC_FORMAT)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
if (empty($month)) {
$month = Date_Calc::dateNow('%m');
}
if (empty($day)) {
$day = Date_Calc::dateNow('%d');
}
$hn_days = Date_Calc::dateToDays($day, $month, $year);
$this_weekday = Date_Calc::daysToDayOfWeek($hn_days);
$interval = (7 - DATE_CALC_BEGIN_WEEKDAY + $this_weekday) % 7;
return Date_Calc::daysToDate($hn_days - $interval, $format);
}
// }}}
// {{{ endOfWeek()
/**
* Find the month day of the end of week for given date,
* using DATE_CALC_BEGIN_WEEKDAY
*
* Can return weekday of following month.
*
* @param int $day the day of the month, default is current local day
* @param int $month the month, default is current local month
* @param int $year the year in four digit format, default is current
* local year
* @param string $format the string indicating how to format the output
*
* @return string the date in the desired format
* @access public
* @static
*/
function endOfWeek($day = 0, $month = 0, $year = null,
$format = DATE_CALC_FORMAT)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
if (empty($month)) {
$month = Date_Calc::dateNow('%m');
}
if (empty($day)) {
$day = Date_Calc::dateNow('%d');
}
$hn_days = Date_Calc::dateToDays($day, $month, $year);
$this_weekday = Date_Calc::daysToDayOfWeek($hn_days);
$interval = (6 + DATE_CALC_BEGIN_WEEKDAY - $this_weekday) % 7;
return Date_Calc::daysToDate($hn_days + $interval, $format);
}
// }}}
// {{{ beginOfPrevWeek()
/**
* Find the month day of the beginning of week before given date,
* using DATE_CALC_BEGIN_WEEKDAY
*
* Can return weekday of prev month.
*
* @param int $day the day of the month, default is current local day
* @param int $month the month, default is current local month
* @param int $year the year in four digit format, default is current
* local year
* @param string $format the string indicating how to format the output
*
* @return string the date in the desired format
* @access public
* @static
*/
function beginOfPrevWeek($day = 0, $month = 0, $year = null,
$format = DATE_CALC_FORMAT)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
if (empty($month)) {
$month = Date_Calc::dateNow('%m');
}
if (empty($day)) {
$day = Date_Calc::dateNow('%d');
}
list($hn_pwyear, $hn_pwmonth, $hn_pwday) =
explode(" ", Date_Calc::daysToDate(Date_Calc::dateToDays($day,
$month,
$year) - 7,
'%Y %m %d'));
return Date_Calc::beginOfWeek($hn_pwday,
$hn_pwmonth,
$hn_pwyear,
$format);
}
// }}}
// {{{ beginOfNextWeek()
/**
* Find the month day of the beginning of week after given date,
* using DATE_CALC_BEGIN_WEEKDAY
*
* Can return weekday of prev month.
*
* @param int $day the day of the month, default is current local day
* @param int $month the month, default is current local month
* @param int $year the year in four digit format, default is current
* local year
* @param string $format the string indicating how to format the output
*
* @return string the date in the desired format
* @access public
* @static
*/
function beginOfNextWeek($day = 0, $month = 0, $year = null,
$format = DATE_CALC_FORMAT)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
if (empty($month)) {
$month = Date_Calc::dateNow('%m');
}
if (empty($day)) {
$day = Date_Calc::dateNow('%d');
}
list($hn_pwyear, $hn_pwmonth, $hn_pwday) =
explode(" ",
Date_Calc::daysToDate(Date_Calc::dateToDays($day,
$month,
$year) + 7,
'%Y %m %d'));
return Date_Calc::beginOfWeek($hn_pwday,
$hn_pwmonth,
$hn_pwyear,
$format);
}
// }}}
// {{{ beginOfMonth()
/**
* Return date of first day of month of given date
*
* @param int $month the month, default is current local month
* @param int $year the year in four digit format, default is current
* local year
* @param string $format the string indicating how to format the output
*
* @return string the date in the desired format
* @access public
* @static
* @see Date_Calc::beginOfMonthBySpan()
* @deprecated Method deprecated in Release 1.4.4
*/
function beginOfMonth($month = 0, $year = null, $format = DATE_CALC_FORMAT)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
if (empty($month)) {
$month = Date_Calc::dateNow('%m');
}
return Date_Calc::dateFormat(Date_Calc::getFirstDayOfMonth($month,
$year),
$month,
$year,
$format);
}
// }}}
// {{{ endOfMonth()
/**
* Return date of last day of month of given date
*
* @param int $month the month, default is current local month
* @param int $year the year in four digit format, default is current
* local year
* @param string $format the string indicating how to format the output
*
* @return string the date in the desired format
* @access public
* @static
* @see Date_Calc::beginOfMonthBySpan()
* @since Method available since Release 1.5.0
* @deprecated Method deprecated in Release 1.5.0
*/
function endOfMonth($month = 0, $year = null, $format = DATE_CALC_FORMAT)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
if (empty($month)) {
$month = Date_Calc::dateNow('%m');
}
return Date_Calc::daysToDate(Date_Calc::lastDayOfMonth($month, $year),
$format);
}
// }}}
// {{{ beginOfPrevMonth()
/**
* Returns date of the first day of previous month of given date
*
* @param mixed $dummy irrelevant parameter
* @param int $month the month, default is current local month
* @param int $year the year in four digit format, default is current
* local year
* @param string $format the string indicating how to format the output
*
* @return string the date in the desired format
* @access public
* @static
* @see Date_Calc::beginOfMonthBySpan()
* @deprecated Method deprecated in Release 1.4.4
*/
function beginOfPrevMonth($dummy = null,
$month = 0,
$year = null,
$format = DATE_CALC_FORMAT)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
if (empty($month)) {
$month = Date_Calc::dateNow('%m');
}
list($hn_pmyear, $hn_prevmonth) = Date_Calc::prevMonth($month, $year);
return Date_Calc::dateFormat(Date_Calc::getFirstDayOfMonth($hn_prevmonth,
$hn_pmyear),
$hn_prevmonth,
$hn_pmyear,
$format);
}
// }}}
// {{{ endOfPrevMonth()
/**
* Returns date of the last day of previous month for given date
*
* @param mixed $dummy irrelevant parameter
* @param int $month the month, default is current local month
* @param int $year the year in four digit format, default is current
* local year
* @param string $format the string indicating how to format the output
*
* @return string the date in the desired format
* @access public
* @static
* @see Date_Calc::endOfMonthBySpan()
* @deprecated Method deprecated in Release 1.4.4
*/
function endOfPrevMonth($dummy = null,
$month = 0,
$year = null,
$format = DATE_CALC_FORMAT)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
if (empty($month)) {
$month = Date_Calc::dateNow('%m');
}
return Date_Calc::daysToDate(Date_Calc::firstDayOfMonth($month,
$year) - 1,
$format);
}
// }}}
// {{{ beginOfNextMonth()
/**
* Returns date of begin of next month of given date
*
* @param mixed $dummy irrelevant parameter
* @param int $month the month, default is current local month
* @param int $year the year in four digit format, default is current
* local year
* @param string $format the string indicating how to format the output
*
* @return string the date in the desired format
* @access public
* @static
* @see Date_Calc::beginOfMonthBySpan()
* @deprecated Method deprecated in Release 1.4.4
*/
function beginOfNextMonth($dummy = null,
$month = 0,
$year = null,
$format = DATE_CALC_FORMAT)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
if (empty($month)) {
$month = Date_Calc::dateNow('%m');
}
list($hn_nmyear, $hn_nextmonth) = Date_Calc::nextMonth($month, $year);
return Date_Calc::dateFormat(Date_Calc::getFirstDayOfMonth($hn_nextmonth,
$hn_nmyear),
$hn_nextmonth,
$hn_nmyear,
$format);
}
// }}}
// {{{ endOfNextMonth()
/**
* Returns date of the last day of next month of given date
*
* @param mixed $dummy irrelevant parameter
* @param int $month the month, default is current local month
* @param int $year the year in four digit format, default is current
* local year
* @param string $format the string indicating how to format the output
*
* @return string the date in the desired format
* @access public
* @static
* @see Date_Calc::endOfMonthBySpan()
* @deprecated Method deprecated in Release 1.4.4
*/
function endOfNextMonth($dummy = null,
$month = 0,
$year = null,
$format = DATE_CALC_FORMAT)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
if (empty($month)) {
$month = Date_Calc::dateNow('%m');
}
list($hn_nmyear, $hn_nextmonth) = Date_Calc::nextMonth($month, $year);
return Date_Calc::daysToDate(Date_Calc::lastDayOfMonth($hn_nextmonth,
$hn_nmyear),
$format);
}
// }}}
// {{{ beginOfMonthBySpan()
/**
* Returns date of the first day of the month in the number of months
* from the given date
*
* @param int $months the number of months from the date provided.
* Positive numbers go into the future.
* Negative numbers go into the past.
* 0 is the month presented in $month.
* @param string $month the month, default is current local month
* @param string $year the year in four digit format, default is the
* current local year
* @param string $format the string indicating how to format the output
*
* @return string the date in the desired format
* @access public
* @static
* @since Method available since Release 1.4.4
*/
function beginOfMonthBySpan($months = 0,
$month = 0,
$year = null,
$format = DATE_CALC_FORMAT)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
if (empty($month)) {
$month = Date_Calc::dateNow('%m');
}
return Date_Calc::addMonths($months,
Date_Calc::getFirstDayOfMonth($month, $year),
$month,
$year,
$format);
}
// }}}
// {{{ endOfMonthBySpan()
/**
* Returns date of the last day of the month in the number of months
* from the given date
*
* @param int $months the number of months from the date provided.
* Positive numbers go into the future.
* Negative numbers go into the past.
* 0 is the month presented in $month.
* @param string $month the month, default is current local month
* @param string $year the year in four digit format, default is the
* current local year
* @param string $format the string indicating how to format the output
*
* @return string the date in the desired format
* @access public
* @static
* @since Method available since Release 1.4.4
*/
function endOfMonthBySpan($months = 0,
$month = 0,
$year = null,
$format = DATE_CALC_FORMAT)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
if (empty($month)) {
$month = Date_Calc::dateNow('%m');
}
$hn_days = Date_Calc::addMonthsToDays($months + 1,
Date_Calc::firstDayOfMonth($month, $year)) - 1;
return Date_Calc::daysToDate($hn_days, $format);
}
// }}}
// {{{ firstOfMonthWeekday()
/**
* Find the day of the week for the first of the month of given date
*
* @param int $month the month, default is current local month
* @param int $year the year in four digit format, default is current
* local year
*
* @return int number of weekday for the first day, 0=Sunday
* @access public
* @static
*/
function firstOfMonthWeekday($month = 0, $year = null)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
if (empty($month)) {
$month = Date_Calc::dateNow('%m');
}
return Date_Calc::daysToDayOfWeek(Date_Calc::firstDayOfMonth($month,
$year));
}
// }}}
// {{{ nWeekdayOfMonth()
/**
* Calculates the date of the Nth weekday of the month,
* such as the second Saturday of January 2000
*
* @param int $week the number of the week to get
* (1 = first, etc. Also can be 'last'.)
* @param int $dow the day of the week (0 = Sunday)
* @param int $month the month
* @param int $year the year. Use the complete year instead of the
* abbreviated version. E.g. use 2005, not 05.
* @param string $format the string indicating how to format the output
*
* @return string the date in the desired format
* @access public
* @static
*/
function nWeekdayOfMonth($week, $dow, $month, $year,
$format = DATE_CALC_FORMAT)
{
if (is_numeric($week)) {
$DOW1day = ($week - 1) * 7 + 1;
$DOW1 = Date_Calc::dayOfWeek($DOW1day, $month, $year);
$wdate = ($week - 1) * 7 + 1 + (7 + $dow - $DOW1) % 7;
if ($wdate > Date_Calc::daysInMonth($month, $year)) {
return -1;
} else {
return Date_Calc::dateFormat($wdate, $month, $year, $format);
}
} elseif ($week == 'last' && $dow < 7) {
$lastday = Date_Calc::daysInMonth($month, $year);
$lastdow = Date_Calc::dayOfWeek($lastday, $month, $year);
$diff = $dow - $lastdow;
if ($diff > 0) {
return Date_Calc::dateFormat($lastday - (7 - $diff), $month,
$year, $format);
} else {
return Date_Calc::dateFormat($lastday + $diff, $month,
$year, $format);
}
} else {
return -1;
}
}
// }}}
// {{{ isValidDate()
/**
* Returns true for valid date, false for invalid date
*
* Uses the proleptic Gregorian calendar, with the year 0 (1 B.C.)
* assumed to be valid and also assumed to be a leap year.
*
* @param int $day the day of the month
* @param int $month the month
* @param int $year the year. Use the complete year instead of the
* abbreviated version. E.g. use 2005, not 05.
*
* @return bool
* @access public
* @static
*/
function isValidDate($day, $month, $year)
{
if ($day < 1 || $month < 1 || $month > 12)
return false;
if ($month == 2) {
if (Date_Calc::isLeapYearGregorian($year)) {
return $day <= 29;
} else {
return $day <= 28;
}
} elseif ($month == 4 || $month == 6 || $month == 9 || $month == 11) {
return $day <= 30;
} else {
return $day <= 31;
}
}
// }}}
// {{{ isLeapYearGregorian()
/**
* Returns true for a leap year, else false
*
* Uses the proleptic Gregorian calendar. The year 0 (1 B.C.) is
* assumed in this algorithm to be a leap year. The function is
* valid for all years, positive and negative.
*
* @param int $year the year. Use the complete year instead of the
* abbreviated version. E.g. use 2005, not 05.
*
* @return bool
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function isLeapYearGregorian($year = null)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
return (($year % 4 == 0) &&
($year % 100 != 0)) ||
($year % 400 == 0);
}
// }}}
// {{{ isLeapYearJulian()
/**
* Returns true for a leap year, else false
*
* Uses the proleptic Julian calendar. The year 0 (1 B.C.) is
* assumed in this algorithm to be a leap year. The function is
* valid for all years, positive and negative.
*
* @param int $year the year. Use the complete year instead of the
* abbreviated version. E.g. use 2005, not 05.
*
* @return boolean
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function isLeapYearJulian($year = null)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
return $year % 4 == 0;
}
// }}}
// {{{ isLeapYear()
/**
* Returns true for a leap year, else false
*
* @param int $year the year. Use the complete year instead of the
* abbreviated version. E.g. use 2005, not 05.
*
* @return boolean
* @access public
* @static
*/
function isLeapYear($year = null)
{
if (is_null($year)) {
$year = Date_Calc::dateNow('%Y');
}
if ($year < 1582) {
// pre Gregorio XIII - 1582
return Date_Calc::isLeapYearJulian($year);
} else {
// post Gregorio XIII - 1582
return Date_Calc::isLeapYearGregorian($year);
}
}
// }}}
// {{{ isFutureDate()
/**
* Determines if given date is a future date from now
*
* @param int $day the day of the month
* @param int $month the month
* @param int $year the year. Use the complete year instead of the
* abbreviated version. E.g. use 2005, not 05.
*
* @return bool
* @access public
* @static
*/
function isFutureDate($day, $month, $year)
{
$this_year = Date_Calc::dateNow('%Y');
$this_month = Date_Calc::dateNow('%m');
$this_day = Date_Calc::dateNow('%d');
if ($year > $this_year) {
return true;
} elseif ($year == $this_year) {
if ($month > $this_month) {
return true;
} elseif ($month == $this_month) {
if ($day > $this_day) {
return true;
}
}
}
return false;
}
// }}}
// {{{ isPastDate()
/**
* Determines if given date is a past date from now
*
* @param int $day the day of the month
* @param int $month the month
* @param int $year the year. Use the complete year instead of the
* abbreviated version. E.g. use 2005, not 05.
*
* @return boolean
* @access public
* @static
*/
function isPastDate($day, $month, $year)
{
$this_year = Date_Calc::dateNow('%Y');
$this_month = Date_Calc::dateNow('%m');
$this_day = Date_Calc::dateNow('%d');
if ($year < $this_year) {
return true;
} elseif ($year == $this_year) {
if ($month < $this_month) {
return true;
} elseif ($month == $this_month) {
if ($day < $this_day) {
return true;
}
}
}
return false;
}
// }}}
// {{{ dateDiff()
/**
* Returns number of days between two given dates
*
* @param int $day1 the day of the month
* @param int $month1 the month
* @param int $year1 the year. Use the complete year instead of the
* abbreviated version. E.g. use 2005, not 05.
* @param int $day2 the day of the month
* @param int $month2 the month
* @param int $year2 the year. Use the complete year instead of the
* abbreviated version. E.g. use 2005, not 05.
*
* @return int the absolute number of days between the two dates.
* If an error occurs, -1 is returned.
* @access public
* @static
*/
function dateDiff($day1, $month1, $year1, $day2, $month2, $year2)
{
if (!Date_Calc::isValidDate($day1, $month1, $year1)) {
return -1;
}
if (!Date_Calc::isValidDate($day2, $month2, $year2)) {
return -1;
}
return abs(Date_Calc::dateToDays($day1, $month1, $year1)
- Date_Calc::dateToDays($day2, $month2, $year2));
}
// }}}
// {{{ compareDates()
/**
* Compares two dates
*
* @param int $day1 the day of the month
* @param int $month1 the month
* @param int $year1 the year. Use the complete year instead of the
* abbreviated version. E.g. use 2005, not 05.
* @param int $day2 the day of the month
* @param int $month2 the month
* @param int $year2 the year. Use the complete year instead of the
* abbreviated version. E.g. use 2005, not 05.
*
* @return int 0 if the dates are equal. 1 if date 1 is later, -1
* if date 1 is earlier.
* @access public
* @static
*/
function compareDates($day1, $month1, $year1, $day2, $month2, $year2)
{
$ndays1 = Date_Calc::dateToDays($day1, $month1, $year1);
$ndays2 = Date_Calc::dateToDays($day2, $month2, $year2);
if ($ndays1 == $ndays2) {
return 0;
}
return ($ndays1 > $ndays2) ? 1 : -1;
}
// }}}
// {{{ round()
/**
* Rounds the date according to the specified precision
*
* The precision parameter must be one of the following constants:
*
* <code>DATE_PRECISION_YEAR</code>
* <code>DATE_PRECISION_MONTH</code>
* <code>DATE_PRECISION_DAY</code>
* <code>DATE_PRECISION_HOUR</code>
* <code>DATE_PRECISION_10MINUTES</code>
* <code>DATE_PRECISION_MINUTE</code>
* <code>DATE_PRECISION_10SECONDS</code>
* <code>DATE_PRECISION_SECOND</code>
*
* The precision can also be specified as an integral offset from
* one of these constants, where the offset reflects a precision
* of 10 to the power of the offset greater than the constant.
* For example:
*
* <code>DATE_PRECISION_YEAR - 1</code> rounds the date to the nearest 10
* years
* <code>DATE_PRECISION_YEAR - 3</code> rounds the date to the nearest 1000
* years
* <code>DATE_PRECISION_SECOND + 1</code> rounds the date to 1 decimal
* point of a second
* <code>DATE_PRECISION_SECOND + 1</code> rounds the date to 3 decimal
* points of a second
* <code>DATE_PRECISION_SECOND + 1</code> rounds the date to the nearest 10
* seconds (thus it is equivalent to
* DATE_PRECISION_10SECONDS)
*
* N.B. This function requires a time in UTC if both the precision is at
* least DATE_PRECISION_SECOND and leap seconds are being counted, otherwise
* any local time is acceptable.
*
* @param int $pn_precision a 'DATE_PRECISION_*' constant
* @param int $pn_day the day of the month
* @param int $pn_month the month
* @param int $pn_year the year
* @param int $pn_hour the hour
* @param int $pn_minute the minute
* @param mixed $pn_second the second as integer or float
* @param bool $pb_countleap whether to count leap seconds (defaults to
* DATE_COUNT_LEAP_SECONDS)
*
* @return array array of year, month, day, hour, minute, second
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function round($pn_precision,
$pn_day,
$pn_month,
$pn_year,
$pn_hour = 0,
$pn_minute = 0,
$pn_second = 0,
$pb_countleap = DATE_COUNT_LEAP_SECONDS)
{
if ($pn_precision <= DATE_PRECISION_YEAR) {
$hn_month = 0;
$hn_day = 0;
$hn_hour = 0;
$hn_minute = 0;
$hn_second = 0;
if ($pn_precision < DATE_PRECISION_YEAR) {
$hn_year = round($pn_year, $pn_precision - DATE_PRECISION_YEAR);
} else {
// Check part-year:
//
$hn_midyear = (Date_Calc::firstDayOfYear($pn_year + 1) -
Date_Calc::firstDayOfYear($pn_year)) / 2;
if (($hn_days = Date_Calc::dayOfYear($pn_day,
$pn_month,
$pn_year)) <=
$hn_midyear - 1) {
$hn_year = $pn_year;
} else if ($hn_days >= $hn_midyear) {
// Round up:
//
$hn_year = $pn_year + 1;
} else {
// Take time into account:
//
$hn_partday = Date_Calc::secondsPastMidnight($pn_hour,
$pn_minute,
$pn_second) /
86400;
if ($hn_partday >= $hn_midyear - $hn_days) {
// Round up:
//
$hn_year = $pn_year + 1;
} else {
$hn_year = $pn_year;
}
}
}
} else if ($pn_precision == DATE_PRECISION_MONTH) {
$hn_year = $pn_year;
$hn_day = 0;
$hn_hour = 0;
$hn_minute = 0;
$hn_second = 0;
$hn_firstofmonth = Date_Calc::firstDayOfMonth($pn_month, $pn_year);
$hn_midmonth = (Date_Calc::lastDayOfMonth($pn_month, $pn_year) +
1 -
$hn_firstofmonth) / 2;
if (($hn_days = Date_Calc::dateToDays($pn_day,
$pn_month,
$pn_year) -
$hn_firstofmonth) <= $hn_midmonth - 1) {
$hn_month = $pn_month;
} else if ($hn_days >= $hn_midmonth) {
// Round up:
//
list($hn_year, $hn_month) = Date_Calc::nextMonth($pn_month,
$pn_year);
} else {
// Take time into account:
//
$hn_partday = Date_Calc::secondsPastMidnight($pn_hour,
$pn_minute,
$pn_second) /
86400;
if ($hn_partday >= $hn_midmonth - $hn_days) {
// Round up:
//
list($hn_year, $hn_month) = Date_Calc::nextMonth($pn_month,
$pn_year);
} else {
$hn_month = $pn_month;
}
}
} else if ($pn_precision == DATE_PRECISION_DAY) {
$hn_year = $pn_year;
$hn_month = $pn_month;
$hn_hour = 0;
$hn_minute = 0;
$hn_second = 0;
if (Date_Calc::secondsPastMidnight($pn_hour,
$pn_minute,
$pn_second) >= 43200) {
// Round up:
//
list($hn_year, $hn_month, $hn_day) =
explode(" ", Date_Calc::nextDay($pn_day,
$pn_month,
$pn_year,
"%Y %m %d"));
} else {
$hn_day = $pn_day;
}
} else if ($pn_precision == DATE_PRECISION_HOUR) {
$hn_year = $pn_year;
$hn_month = $pn_month;
$hn_day = $pn_day;
$hn_minute = 0;
$hn_second = 0;
if (Date_Calc::secondsPastTheHour($pn_minute, $pn_second) >= 1800) {
// Round up:
//
list($hn_year, $hn_month, $hn_day, $hn_hour) =
Date_Calc::addHours(1,
$pn_day,
$pn_month,
$pn_year,
$pn_hour);
} else {
$hn_hour = $pn_hour;
}
} else if ($pn_precision <= DATE_PRECISION_MINUTE) {
$hn_year = $pn_year;
$hn_month = $pn_month;
$hn_day = $pn_day;
$hn_hour = $pn_hour;
$hn_second = 0;
if ($pn_precision < DATE_PRECISION_MINUTE) {
$hn_minute = round($pn_minute,
$pn_precision - DATE_PRECISION_MINUTE);
} else {
// Check seconds:
//
if ($pn_second >= 30) {
// Round up:
//
list($hn_year,
$hn_month,
$hn_day,
$hn_hour,
$hn_minute) =
Date_Calc::addMinutes(1,
$pn_day,
$pn_month,
$pn_year,
$pn_hour,
$pn_minute);
} else {
$hn_minute = $pn_minute;
}
}
} else {
// Precision is at least (DATE_PRECISION_SECOND - 1):
//
$hn_year = $pn_year;
$hn_month = $pn_month;
$hn_day = $pn_day;
$hn_hour = $pn_hour;
$hn_minute = $pn_minute;
$hn_second = round($pn_second,
$pn_precision - DATE_PRECISION_SECOND);
if (fmod($hn_second, 1) == 0.0) {
$hn_second = (int) $hn_second;
if ($hn_second != intval($pn_second)) {
list($hn_year,
$hn_month,
$hn_day,
$hn_hour,
$hn_minute,
$hn_second) =
Date_Calc::addSeconds($hn_second - intval($pn_second),
$pn_day,
$pn_month,
$pn_year,
$pn_hour,
$pn_minute,
intval($pn_second),
$pn_precision >=
DATE_PRECISION_SECOND &&
$pb_countleap);
//
// (N.B. if rounded to nearest 10 seconds,
// user does not expect seconds to be '60')
}
}
}
return array((int) $hn_year,
(int) $hn_month,
(int) $hn_day,
(int) $hn_hour,
(int) $hn_minute,
$hn_second);
}
// }}}
// {{{ roundSeconds()
/**
* Rounds seconds up or down to the nearest specified unit
*
* @param int $pn_precision number of digits after the decimal point
* @param int $pn_day the day of the month
* @param int $pn_month the month
* @param int $pn_year the year
* @param int $pn_hour the hour
* @param int $pn_minute the minute
* @param mixed $pn_second the second as integer or float
* @param bool $pb_countleap whether to count leap seconds (defaults to
* DATE_COUNT_LEAP_SECONDS)
*
* @return array array of year, month, day, hour, minute, second
* @access public
* @static
* @since Method available since Release 1.5.0
*/
function roundSeconds($pn_precision,
$pn_day,
$pn_month,
$pn_year,
$pn_hour,
$pn_minute,
$pn_second,
$pb_countleap = DATE_COUNT_LEAP_SECONDS)
{
return Date_Calc::round(DATE_PRECISION_SECOND + $pn_precision,
$pn_day,
$pn_month,
$pn_year,
$pn_hour,
$pn_minute,
$pn_second);
}
// }}}
}
// }}}
/*
* Local variables:
* mode: php
* tab-width: 4
* c-basic-offset: 4
* c-hanging-comment-ender-p: nil
* End:
*/
/**
* This file contains the Horde_Date_Recurrence class and according constants.
*
* $Horde: framework/Date/Date/Recurrence.php,v 1.7.2.16 2010-10-14 14:18:05 jan Exp $
*
* Copyright 2007-2009 The Horde Project (http://www.horde.org/)
*
* See the enclosed file COPYING for license information (LGPL). If you
* did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
*
* @since Horde 3.2
* @package Horde_Date
*/
/** Horde_Date */
// require_once 'Horde/Date.php';
/** Date_Calc */
//require_once 'Date/Calc.php';
/** No recurrence. */
define('HORDE_DATE_RECUR_NONE', 0);
/** Recurs daily. */
define('HORDE_DATE_RECUR_DAILY', 1);
/** Recurs weekly. */
define('HORDE_DATE_RECUR_WEEKLY', 2);
/** Recurs monthly on the same date. */
define('HORDE_DATE_RECUR_MONTHLY_DATE', 3);
/** Recurs monthly on the same week day. */
define('HORDE_DATE_RECUR_MONTHLY_WEEKDAY', 4);
/** Recurs yearly on the same date. */
define('HORDE_DATE_RECUR_YEARLY_DATE', 5);
/** Recurs yearly on the same day of the year. */
define('HORDE_DATE_RECUR_YEARLY_DAY', 6);
/** Recurs yearly on the same week day. */
define('HORDE_DATE_RECUR_YEARLY_WEEKDAY', 7);
/**
* The Horde_Date_Recurrence class implements algorithms for calculating
* recurrences of events, including several recurrence types, intervals,
* exceptions, and conversion from and to vCalendar and iCalendar recurrence
* rules.
*
* All methods expecting dates as parameters accept all values that the
* Horde_Date constructor accepts, i.e. a timestamp, another Horde_Date
* object, an ISO time string or a hash.
*
* @author Jan Schneider <jan@horde.org>
* @since Horde 3.2
* @package Horde_Date
*/
class Horde_Date_Recurrence {
/**
* The start time of the event.
*
* @var Horde_Date
*/
var $start;
/**
* The end date of the recurrence interval.
*
* @var Horde_Date
*/
var $recurEnd = null;
/**
* The number of recurrences.
*
* @var integer
*/
var $recurCount = null;
/**
* The type of recurrence this event follows. HORDE_DATE_RECUR_* constant.
*
* @var integer
*/
var $recurType = HORDE_DATE_RECUR_NONE;
/**
* The length of time between recurrences. The time unit depends on the
* recurrence type.
*
* @var integer
*/
var $recurInterval = 1;
/**
* Any additional recurrence data.
*
* @var integer
*/
var $recurData = null;
/**
* BYDAY recurrence number
*
* @var integer
*/
var $recurNthDay = 0;
/**
* BYMONTH recurrence data
*
* @var array
*/
var $recurMonths = array();
/**
* All the exceptions from recurrence for this event.
*
* @var array
*/
var $exceptions = array();
/**
* All the dates this recurrence has been marked as completed.
*
* @var array
*/
var $completions = array();
/**
* Constructor.
*
* @param Horde_Date $start Start of the recurring event.
*/
function Horde_Date_Recurrence($start)
{
$this->start = new Horde_Date($start);
}
/**
* Checks if this event recurs on a given day of the week.
*
* @param integer $dayMask A mask consisting of HORDE_DATE_MASK_*
* constants specifying the day(s) to check.
*
* @return boolean True if this event recurs on the given day(s).
*/
function recurOnDay($dayMask)
{
return ($this->recurData & $dayMask);
}
/**
* Specifies the days this event recurs on.
*
* @param integer $dayMask A mask consisting of HORDE_DATE_MASK_*
* constants specifying the day(s) to recur on.
*/
function setRecurOnDay($dayMask)
{
$this->recurData = $dayMask;
}
/**
* Returns the days this event recurs on.
*
* @return integer A mask consisting of HORDE_DATE_MASK_* constants
* specifying the day(s) this event recurs on.
*/
function getRecurOnDays()
{
return $this->recurData;
}
/**
* Specifies the months for yearly (weekday) recurrence
*
* @param array $months List of months (integers) this event recurs on.
*/
function setRecurByMonth($months)
{
$this->recurMonths = (array)$months;
}
/**
* Returns a list of months this yearly event recurs on
*
* @return array List of months (integers) this event recurs on.
*/
function getRecurByMonth()
{
return $this->recurMonths;
}
/**
*
* @param integer $nthDay The nth weekday of month to repeat events on
*/
function setRecurNthWeekday($nthDay)
{
$this->recurNthDay = (int)$nthDay;
}
/**
*
* @return integer The nth weekday of month to repeat events.
*/
function getRecurNthWeekday()
{
return $this->recurNthDay;
}
/**
* Returns whether this event has a specific recurrence type.
*
* @param integer $recurrence HORDE_DATE_RECUR_* constant of the
* recurrence type to check for.
*
* @return boolean True if the event has the specified recurrence type.
*/
function hasRecurType($recurrence)
{
return ($recurrence == $this->recurType);
}
/**
* Sets a recurrence type for this event.
*
* @param integer $recurrence A HORDE_DATE_RECUR_* constant.
*/
function setRecurType($recurrence)
{
$this->recurType = $recurrence;
}
/**
* Returns recurrence type of this event.
*
* @return integer A HORDE_DATE_RECUR_* constant.
*/
function getRecurType()
{
return $this->recurType;
}
/**
* Returns a description of this event's recurring type.
*
* @return string Human readable recurring type.
*/
function getRecurName()
{
switch ($this->getRecurType()) {
case HORDE_DATE_RECUR_NONE: return _("No recurrence");
case HORDE_DATE_RECUR_DAILY: return _("Daily");
case HORDE_DATE_RECUR_WEEKLY: return _("Weekly");
case HORDE_DATE_RECUR_MONTHLY_DATE:
case HORDE_DATE_RECUR_MONTHLY_WEEKDAY: return _("Monthly");
case HORDE_DATE_RECUR_YEARLY_DATE:
case HORDE_DATE_RECUR_YEARLY_DAY:
case HORDE_DATE_RECUR_YEARLY_WEEKDAY: return _("Yearly");
}
}
/**
* Sets the length of time between recurrences of this event.
*
* @param integer $interval The time between recurrences.
*/
function setRecurInterval($interval)
{
if ($interval > 0) {
$this->recurInterval = $interval;
}
}
/**
* Retrieves the length of time between recurrences of this event.
*
* @return integer The number of seconds between recurrences.
*/
function getRecurInterval()
{
return $this->recurInterval;
}
/**
* Sets the number of recurrences of this event.
*
* @param integer $count The number of recurrences.
*/
function setRecurCount($count)
{
if ($count > 0) {
$this->recurCount = (int)$count;
// Recurrence counts and end dates are mutually exclusive.
$this->recurEnd = null;
} else {
$this->recurCount = null;
}
}
/**
* Retrieves the number of recurrences of this event.
*
* @return integer The number recurrences.
*/
function getRecurCount()
{
return $this->recurCount;
}
/**
* Returns whether this event has a recurrence with a fixed count.
*
* @return boolean True if this recurrence has a fixed count.
*/
function hasRecurCount()
{
return isset($this->recurCount);
}
/**
* Sets the start date of the recurrence interval.
*
* @param Horde_Date $start The recurrence start.
*/
function setRecurStart($start)
{
$this->start = new Horde_Date($start);
}
/**
* Retrieves the start date of the recurrence interval.
*
* @return Horde_Date The recurrence start.
*/
function getRecurStart()
{
return $this->start;
}
/**
* Sets the end date of the recurrence interval.
*
* @param Horde_Date $end The recurrence end.
*/
function setRecurEnd($end)
{
if (!empty($end)) {
// Recurrence counts and end dates are mutually exclusive.
$this->recurCount = null;
}
$this->recurEnd = new Horde_Date($end);
}
/**
* Retrieves the end date of the recurrence interval.
*
* @return Horde_Date The recurrence end.
*/
function getRecurEnd()
{
return $this->recurEnd;
}
/**
* Returns whether this event has a recurrence end.
*
* @return boolean True if this recurrence ends.
*/
function hasRecurEnd()
{
return isset($this->recurEnd) && isset($this->recurEnd->year) &&
$this->recurEnd->year != 9999;
}
/**
* Finds the next recurrence of this event that's after $afterDate.
*
* @param Horde_Date $afterDate Return events after this date.
*
* @return Horde_Date|boolean The date of the next recurrence or false
* if the event does not recur after
* $afterDate.
*/
function nextRecurrence($afterDate)
{
$after = new Horde_Date($afterDate);
$after->correct();
if ($this->start->compareDateTime($after) >= 0) {
return new Horde_Date($this->start);
}
if ($this->recurInterval == 0) {
return false;
}
switch ($this->getRecurType()) {
case HORDE_DATE_RECUR_DAILY:
$diff = Date_Calc::dateDiff($this->start->mday, $this->start->month, $this->start->year, $after->mday, $after->month, $after->year);
$recur = ceil($diff / $this->recurInterval);
if ($this->recurCount && $recur >= $this->recurCount) {
return false;
}
$recur *= $this->recurInterval;
$next = new Horde_Date($this->start);
list($next->mday, $next->month, $next->year) = explode('/', Date_Calc::daysToDate(Date_Calc::dateToDays($next->mday, $next->month, $next->year) + $recur, '%e/%m/%Y'));
if ((!$this->hasRecurEnd() ||
$next->compareDateTime($this->recurEnd) <= 0) &&
$next->compareDateTime($after) >= 0) {
return new Horde_Date($next);
}
break;
case HORDE_DATE_RECUR_WEEKLY:
if (empty($this->recurData)) {
return false;
}
list($start_week->mday, $start_week->month, $start_week->year) = explode('/', Date_Calc::beginOfWeek($this->start->mday, $this->start->month, $this->start->year, '%e/%m/%Y'));
$start_week->hour = $this->start->hour;
$start_week->min = $this->start->min;
$start_week->sec = $this->start->sec;
list($after_week->mday, $after_week->month, $after_week->year) = explode('/', Date_Calc::beginOfWeek($after->mday, $after->month, $after->year, '%e/%m/%Y'));
$after_week_end = new Horde_Date($after_week);
$after_week_end->mday += 7;
$after_week_end->correct();
$diff = Date_Calc::dateDiff($start_week->mday, $start_week->month, $start_week->year,
$after_week->mday, $after_week->month, $after_week->year);
$interval = $this->recurInterval * 7;
$repeats = floor($diff / $interval);
if ($diff % $interval < 7) {
$recur = $diff;
} else {
/**
* If the after_week is not in the first week interval the
* search needs to skip ahead a complete interval. The way it is
* calculated here means that an event that occurs every second
* week on Monday and Wednesday with the event actually starting
* on Tuesday or Wednesday will only have one incidence in the
* first week.
*/
$recur = $interval * ($repeats + 1);
}
if ($this->hasRecurCount()) {
$recurrences = 0;
/**
* Correct the number of recurrences by the number of events
* that lay between the start of the start week and the
* recurrence start.
*/
$next = new Horde_Date($start_week);
while ($next->compareDateTime($this->start) < 0) {
if ($this->recurOnDay((int)pow(2, $next->dayOfWeek()))) {
$recurrences--;
}
++$next->mday;
$next->correct();
}
if ($repeats > 0) {
$weekdays = $this->recurData;
$total_recurrences_per_week = 0;
while ($weekdays > 0) {
if ($weekdays % 2) {
$total_recurrences_per_week++;
}
$weekdays = ($weekdays - ($weekdays % 2)) / 2;
}
$recurrences += $total_recurrences_per_week * $repeats;
}
}
$next = $start_week;
list($next->mday, $next->month, $next->year) = explode('/', Date_Calc::daysToDate(Date_Calc::dateToDays($next->mday, $next->month, $next->year) + $recur, '%e/%m/%Y'));
$next = new Horde_Date($next);
while ($next->compareDateTime($after) < 0 &&
$next->compareDateTime($after_week_end) < 0) {
if ($this->hasRecurCount()
&& $next->compareDateTime($after) < 0
&& $this->recurOnDay((int)pow(2, $next->dayOfWeek()))) {
$recurrences++;
}
++$next->mday;
$next->correct();
}
if ($this->hasRecurCount() &&
$recurrences >= $this->recurCount) {
return false;
}
if (!$this->hasRecurEnd() ||
$next->compareDateTime($this->recurEnd) <= 0) {
if ($next->compareDateTime($after_week_end) >= 0) {
return $this->nextRecurrence($after_week_end);
}
while (!$this->recurOnDay((int)pow(2, $next->dayOfWeek())) &&
$next->compareDateTime($after_week_end) < 0) {
++$next->mday;
$next->correct();
}
if (!$this->hasRecurEnd() ||
$next->compareDateTime($this->recurEnd) <= 0) {
if ($next->compareDateTime($after_week_end) >= 0) {
return $this->nextRecurrence($after_week_end);
} else {
return $next;
}
}
}
break;
case HORDE_DATE_RECUR_MONTHLY_DATE:
$start = new Horde_Date($this->start);
if ($after->compareDateTime($start) < 0) {
$after = $start;
}
// If we're starting past this month's recurrence of the event,
// look in the next month on the day the event recurs.
if ($after->mday > $start->mday) {
++$after->month;
$after->mday = $start->mday;
$after->correct();
}
// Adjust $start to be the first match.
$offset = ($after->month - $start->month) + ($after->year - $start->year) * 12;
$offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
if ($this->recurCount &&
($offset / $this->recurInterval) >= $this->recurCount) {
return false;
}
$start->month += $offset;
$count = $offset / $this->recurInterval;
do {
if ($this->recurCount &&
$count++ >= $this->recurCount) {
return false;
}
// Don't correct for day overflow; we just skip February 30th,
// for example.
$start->correct(HORDE_DATE_MASK_MONTH);
// Bail if we've gone past the end of recurrence.
if ($this->hasRecurEnd() &&
$this->recurEnd->compareDateTime($start) < 0) {
return false;
}
if ($start->isValid()) {
return $start;
}
// If the interval is 12, and the date isn't valid, then we
// need to see if February 29th is an option. If not, then the
// event will _never_ recur, and we need to stop checking to
// avoid an infinite loop.
if ($this->recurInterval == 12 && ($start->month != 2 || $start->mday > 29)) {
return false;
}
// Add the recurrence interval.
$start->month += $this->recurInterval;
} while (true);
break;
case HORDE_DATE_RECUR_MONTHLY_WEEKDAY:
// Start with the start date of the event.
$estart = new Horde_Date($this->start);
// What day of the week, and week of the month, do we recur on?
if ($this->recurNthDay != 0) {
$nth = $this->recurNthDay < 0 ? 'last' : $this->recurNthDay;
$weekday = log($this->recurData, 2);
} else {
$nth = ceil($this->start->mday / 7);
$weekday = $estart->dayOfWeek();
}
// Adjust $estart to be the first candidate.
$offset = ($after->month - $estart->month) + ($after->year - $estart->year) * 12;
$offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
// Adjust our working date until it's after $after.
$estart->month += $offset - $this->recurInterval;
$count = $offset / $this->recurInterval;
do {
if ($this->recurCount &&
$count++ >= $this->recurCount) {
return false;
}
$estart->month += $this->recurInterval;
$estart->correct();
$next = new Horde_Date($estart);
if ($this->recurNthDay) {
list($next->mday, $next->month, $next->year) = explode('/', Date_Calc::nWeekdayOfMonth($nth, $weekday, $estart->month, $estart->year, '%e/%m/%Y'));
} else {
$next->setNthWeekday($weekday, $nth);
}
if ($next->compareDateTime($after) < 0) {
// We haven't made it past $after yet, try again.
continue;
}
if ($this->hasRecurEnd() &&
$next->compareDateTime($this->recurEnd) > 0) {
// We've gone past the end of recurrence; we can give up
// now.
return false;
}
// We have a candidate to return.
break;
} while (true);
return $next;
case HORDE_DATE_RECUR_YEARLY_DATE:
// Start with the start date of the event.
$estart = new Horde_Date($this->start);
if ($after->month > $estart->month ||
($after->month == $estart->month && $after->mday > $estart->mday)) {
++$after->year;
$after->month = $estart->month;
$after->mday = $estart->mday;
}
// Seperate case here for February 29th
if ($estart->month == 2 && $estart->mday == 29) {
while (!Horde_Date::isLeapYear($after->year)) {
++$after->year;
}
}
// Adjust $estart to be the first candidate.
$offset = $after->year - $estart->year;
if ($offset > 0) {
$offset = floor(($offset + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
$estart->year += $offset;
}
// We've gone past the end of recurrence; give up.
if ($this->recurCount &&
$offset >= $this->recurCount) {
return false;
}
if ($this->hasRecurEnd() &&
$this->recurEnd->compareDateTime($estart) < 0) {
return false;
}
return $estart;
case HORDE_DATE_RECUR_YEARLY_DAY:
// Check count first.
$dayofyear = $this->start->dayOfYear();
$count = ($after->year - $this->start->year) / $this->recurInterval + 1;
if ($this->recurCount &&
($count > $this->recurCount ||
($count == $this->recurCount &&
$after->dayOfYear() > $dayofyear))) {
return false;
}
// Start with a rough interval.
$estart = new Horde_Date($this->start);
$estart->year += floor($count - 1) * $this->recurInterval;
// Now add the difference to the required day of year.
$estart->mday += $dayofyear - $estart->dayOfYear();
$estart->correct();
// Add an interval if the estimation was wrong.
if ($estart->compareDate($after) < 0) {
$estart->year += $this->recurInterval;
$estart->mday += $dayofyear - $estart->dayOfYear();
$estart->correct();
}
// We've gone past the end of recurrence; give up.
if ($this->hasRecurEnd() &&
$this->recurEnd->compareDateTime($estart) < 0) {
return false;
}
return $estart;
case HORDE_DATE_RECUR_YEARLY_WEEKDAY:
// Start with the start date of the event.
$estart = new Horde_Date($this->start);
// What day of the week, and week of the month, do we recur on?
if ($this->recurNthDay != 0) {
$nth = $this->recurNthDay < 0 ? 'last' : $this->recurNthDay;
$weekday = log($this->recurData, 2);
} else {
$nth = ceil($this->start->mday / 7);
$weekday = $estart->dayOfWeek();
}
// set month from recurrence rule (FIXME: support more than one month)
if ($this->recurMonths) {
$estart->month = $this->recurMonths[0];
}
// Adjust $estart to be the first candidate.
$offset = floor(($after->year - $estart->year + $this->recurInterval - 1) / $this->recurInterval) * $this->recurInterval;
// Adjust our working date until it's after $after.
$estart->year += $offset - $this->recurInterval;
$count = $offset / $this->recurInterval;
do {
if ($this->recurCount &&
$count++ >= $this->recurCount) {
return false;
}
$estart->year += $this->recurInterval;
$estart->correct();
$next = new Horde_Date($estart);
if ($this->recurNthDay) {
list($next->mday, $next->month, $next->year) = explode('/', Date_Calc::nWeekdayOfMonth($nth, $weekday, $estart->month, $estart->year, '%e/%m/%Y'));
} else {
$next->setNthWeekday($weekday, $nth);
}
if ($next->compareDateTime($after) < 0) {
// We haven't made it past $after yet, try again.
continue;
}
if ($this->hasRecurEnd() &&
$next->compareDateTime($this->recurEnd) > 0) {
// We've gone past the end of recurrence; we can give up
// now.
return false;
}
// We have a candidate to return.
break;
} while (true);
return $next;
}
// We didn't find anything, the recurType was bad, or something else
// went wrong - return false.
return false;
}
/**
* Returns whether this event has any date that matches the recurrence
* rules and is not an exception.
*
* @return boolean True if an active recurrence exists.
*/
function hasActiveRecurrence()
{
if (!$this->hasRecurEnd()) {
return true;
}
$next = $this->nextRecurrence(new Horde_Date($this->start));
while (is_object($next)) {
if (!$this->hasException($next->year, $next->month, $next->mday) &&
!$this->hasCompletion($next->year, $next->month, $next->mday)) {
return true;
}
$next = $this->nextRecurrence(array('year' => $next->year,
'month' => $next->month,
'mday' => $next->mday + 1,
'hour' => $next->hour,
'min' => $next->min,
'sec' => $next->sec));
}
return false;
}
/**
* Returns the next active recurrence.
*
* @param Horde_Date $afterDate Return events after this date.
*
* @return Horde_Date|boolean The date of the next active
* recurrence or false if the event
* has no active recurrence after
* $afterDate.
*/
function nextActiveRecurrence($afterDate)
{
$next = $this->nextRecurrence($afterDate);
while (is_object($next)) {
if (!$this->hasException($next->year, $next->month, $next->mday) &&
!$this->hasCompletion($next->year, $next->month, $next->mday)) {
return $next;
}
$next->mday++;
$next = $this->nextRecurrence($next);
}
return false;
}
/**
* Adds an exception to a recurring event.
*
* @param integer $year The year of the execption.
* @param integer $month The month of the execption.
* @param integer $mday The day of the month of the exception.
*/
function addException($year, $month, $mday)
{
$this->exceptions[] = sprintf('%04d%02d%02d', $year, $month, $mday);
}
/**
* Deletes an exception from a recurring event.
*
* @param integer $year The year of the execption.
* @param integer $month The month of the execption.
* @param integer $mday The day of the month of the exception.
*/
function deleteException($year, $month, $mday)
{
$key = array_search(sprintf('%04d%02d%02d', $year, $month, $mday), $this->exceptions);
if ($key !== false) {
unset($this->exceptions[$key]);
}
}
/**
* Checks if an exception exists for a given reccurence of an event.
*
* @param integer $year The year of the reucrance.
* @param integer $month The month of the reucrance.
* @param integer $mday The day of the month of the reucrance.
*
* @return boolean True if an exception exists for the given date.
*/
function hasException($year, $month, $mday)
{
return in_array(sprintf('%04d%02d%02d', $year, $month, $mday),
$this->getExceptions());
}
/**
* Retrieves all the exceptions for this event.
*
* @return array Array containing the dates of all the exceptions in
* YYYYMMDD form.
*/
function getExceptions()
{
return $this->exceptions;
}
/**
* Adds a completion to a recurring event.
*
* @param integer $year The year of the execption.
* @param integer $month The month of the execption.
* @param integer $mday The day of the month of the completion.
*/
function addCompletion($year, $month, $mday)
{
$this->completions[] = sprintf('%04d%02d%02d', $year, $month, $mday);
}
/**
* Deletes a completion from a recurring event.
*
* @param integer $year The year of the execption.
* @param integer $month The month of the execption.
* @param integer $mday The day of the month of the completion.
*/
function deleteCompletion($year, $month, $mday)
{
$key = array_search(sprintf('%04d%02d%02d', $year, $month, $mday), $this->completions);
if ($key !== false) {
unset($this->completions[$key]);
}
}
/**
* Checks if a completion exists for a given reccurence of an event.
*
* @param integer $year The year of the reucrance.
* @param integer $month The month of the recurrance.
* @param integer $mday The day of the month of the recurrance.
*
* @return boolean True if a completion exists for the given date.
*/
function hasCompletion($year, $month, $mday)
{
return in_array(sprintf('%04d%02d%02d', $year, $month, $mday),
$this->getCompletions());
}
/**
* Retrieves all the completions for this event.
*
* @return array Array containing the dates of all the completions in
* YYYYMMDD form.
*/
function getCompletions()
{
return $this->completions;
}
/**
* Parses a vCalendar 1.0 recurrence rule.
*
* @link http://www.imc.org/pdi/vcal-10.txt
* @link http://www.shuchow.com/vCalAddendum.html
*
* @param string $rrule A vCalendar 1.0 conform RRULE value.
*/
function fromRRule10($rrule)
{
if (!$rrule) {
return;
}
if (!preg_match('/([A-Z]+)(\d+)?(.*)/', $rrule, $matches)) {
// No recurrence data - event does not recur.
$this->setRecurType(HORDE_DATE_RECUR_NONE);
}
// Always default the recurInterval to 1.
$this->setRecurInterval(!empty($matches[2]) ? $matches[2] : 1);
$remainder = trim($matches[3]);
switch ($matches[1]) {
case 'D':
$this->setRecurType(HORDE_DATE_RECUR_DAILY);
break;
case 'W':
$this->setRecurType(HORDE_DATE_RECUR_WEEKLY);
if (!empty($remainder)) {
$maskdays = array('SU' => HORDE_DATE_MASK_SUNDAY,
'MO' => HORDE_DATE_MASK_MONDAY,
'TU' => HORDE_DATE_MASK_TUESDAY,
'WE' => HORDE_DATE_MASK_WEDNESDAY,
'TH' => HORDE_DATE_MASK_THURSDAY,
'FR' => HORDE_DATE_MASK_FRIDAY,
'SA' => HORDE_DATE_MASK_SATURDAY);
$mask = 0;
while (preg_match('/^ ?[A-Z]{2} ?/', $remainder, $matches)) {
$day = trim($matches[0]);
$remainder = substr($remainder, strlen($matches[0]));
$mask |= $maskdays[$day];
}
$this->setRecurOnDay($mask);
} else {
// Recur on the day of the week of the original recurrence.
$maskdays = array(HORDE_DATE_SUNDAY => HORDE_DATE_MASK_SUNDAY,
HORDE_DATE_MONDAY => HORDE_DATE_MASK_MONDAY,
HORDE_DATE_TUESDAY => HORDE_DATE_MASK_TUESDAY,
HORDE_DATE_WEDNESDAY => HORDE_DATE_MASK_WEDNESDAY,
HORDE_DATE_THURSDAY => HORDE_DATE_MASK_THURSDAY,
HORDE_DATE_FRIDAY => HORDE_DATE_MASK_FRIDAY,
HORDE_DATE_SATURDAY => HORDE_DATE_MASK_SATURDAY);
$this->setRecurOnDay($maskdays[$this->start->dayOfWeek()]);
}
break;
case 'MP':
$this->setRecurType(HORDE_DATE_RECUR_MONTHLY_WEEKDAY);
break;
case 'MD':
$this->setRecurType(HORDE_DATE_RECUR_MONTHLY_DATE);
break;
case 'YM':
$this->setRecurType(HORDE_DATE_RECUR_YEARLY_DATE);
break;
case 'YD':
$this->setRecurType(HORDE_DATE_RECUR_YEARLY_DAY);
break;
}
// We don't support modifiers at the moment, strip them.
while ($remainder && !preg_match('/^(#\d+|\d{8})($| |T\d{6})/', $remainder)) {
$remainder = substr($remainder, 1);
}
if (!empty($remainder)) {
if (strpos($remainder, '#') === 0) {
$this->setRecurCount(substr($remainder, 1));
} else {
list($year, $month, $mday) = sscanf($remainder, '%04d%02d%02d');
$this->setRecurEnd(new Horde_Date(array('year' => $year,
'month' => $month,
'mday' => $mday)));
}
}
}
/**
* Creates a vCalendar 1.0 recurrence rule.
*
* @link http://www.imc.org/pdi/vcal-10.txt
* @link http://www.shuchow.com/vCalAddendum.html
*
* @param Horde_iCalendar $calendar A Horde_iCalendar object instance.
*
* @return string A vCalendar 1.0 conform RRULE value.
*/
function toRRule10($calendar)
{
switch ($this->recurType) {
case HORDE_DATE_RECUR_NONE:
return '';
case HORDE_DATE_RECUR_DAILY:
$rrule = 'D' . $this->recurInterval;
break;
case HORDE_DATE_RECUR_WEEKLY:
$rrule = 'W' . $this->recurInterval;
$vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
for ($i = 0; $i <= 7 ; ++$i) {
if ($this->recurOnDay(pow(2, $i))) {
$rrule .= ' ' . $vcaldays[$i];
}
}
break;
case HORDE_DATE_RECUR_MONTHLY_DATE:
$rrule = 'MD' . $this->recurInterval . ' ' . trim($this->start->mday);
break;
case HORDE_DATE_RECUR_MONTHLY_WEEKDAY:
$nth_weekday = (int)($this->start->mday / 7);
if (($this->start->mday % 7) > 0) {
$nth_weekday++;
}
$vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
$rrule = 'MP' . $this->recurInterval . ' ' . $nth_weekday . '+ ' . $vcaldays[$this->start->dayOfWeek()];
break;
case HORDE_DATE_RECUR_YEARLY_DATE:
$rrule = 'YM' . $this->recurInterval . ' ' . trim($this->start->month);
break;
case HORDE_DATE_RECUR_YEARLY_DAY:
$rrule = 'YD' . $this->recurInterval . ' ' . $this->start->dayOfYear();
break;
default:
return '';
}
if ($this->hasRecurEnd()) {
$recurEnd = new Horde_Date($this->recurEnd);
$recurEnd->mday++;
return $rrule . ' ' . $calendar->_exportDateTime($recurEnd);
}
return $rrule . ' #' . (int)$this->getRecurCount();
}
/**
* Parses an iCalendar 2.0 recurrence rule.
*
* @link http://rfc.net/rfc2445.html#s4.3.10
* @link http://rfc.net/rfc2445.html#s4.8.5
* @link http://www.shuchow.com/vCalAddendum.html
*
* @param string $rrule An iCalendar 2.0 conform RRULE value.
*/
function fromRRule20($rrule)
{
// Parse the recurrence rule into keys and values.
$rdata = array();
$parts = explode(';', $rrule);
foreach ($parts as $part) {
list($key, $value) = explode('=', $part, 2);
$rdata[strtoupper($key)] = $value;
}
if (isset($rdata['FREQ'])) {
// Always default the recurInterval to 1.
$this->setRecurInterval(isset($rdata['INTERVAL']) ? $rdata['INTERVAL'] : 1);
$maskdays = array('SU' => HORDE_DATE_MASK_SUNDAY,
'MO' => HORDE_DATE_MASK_MONDAY,
'TU' => HORDE_DATE_MASK_TUESDAY,
'WE' => HORDE_DATE_MASK_WEDNESDAY,
'TH' => HORDE_DATE_MASK_THURSDAY,
'FR' => HORDE_DATE_MASK_FRIDAY,
'SA' => HORDE_DATE_MASK_SATURDAY);
switch (strtoupper($rdata['FREQ'])) {
case 'DAILY':
$this->setRecurType(HORDE_DATE_RECUR_DAILY);
break;
case 'WEEKLY':
$this->setRecurType(HORDE_DATE_RECUR_WEEKLY);
if (isset($rdata['BYDAY'])) {
$days = explode(',', $rdata['BYDAY']);
$mask = 0;
foreach ($days as $day) {
$mask |= $maskdays[$day];
}
$this->setRecurOnDay($mask);
} else {
// Recur on the day of the week of the original
// recurrence.
$maskdays = array(
HORDE_DATE_SUNDAY => HORDE_DATE_MASK_SUNDAY,
HORDE_DATE_MONDAY => HORDE_DATE_MASK_MONDAY,
HORDE_DATE_TUESDAY => HORDE_DATE_MASK_TUESDAY,
HORDE_DATE_WEDNESDAY => HORDE_DATE_MASK_WEDNESDAY,
HORDE_DATE_THURSDAY => HORDE_DATE_MASK_THURSDAY,
HORDE_DATE_FRIDAY => HORDE_DATE_MASK_FRIDAY,
HORDE_DATE_SATURDAY => HORDE_DATE_MASK_SATURDAY);
$this->setRecurOnDay($maskdays[$this->start->dayOfWeek()]);
}
break;
case 'MONTHLY':
if (isset($rdata['BYDAY'])) {
$this->setRecurType(HORDE_DATE_RECUR_MONTHLY_WEEKDAY);
if (preg_match('/(-?[1-4])([A-Z]+)/', $rdata['BYDAY'], $m)) {
$this->setRecurOnDay($maskdays[$m[2]]);
$this->setRecurNthWeekday($m[1]);
}
} else {
$this->setRecurType(HORDE_DATE_RECUR_MONTHLY_DATE);
}
break;
case 'YEARLY':
if (isset($rdata['BYYEARDAY'])) {
$this->setRecurType(HORDE_DATE_RECUR_YEARLY_DAY);
} elseif (isset($rdata['BYDAY'])) {
$this->setRecurType(HORDE_DATE_RECUR_YEARLY_WEEKDAY);
if (preg_match('/(-?[1-4])([A-Z]+)/', $rdata['BYDAY'], $m)) {
$this->setRecurOnDay($maskdays[$m[2]]);
$this->setRecurNthWeekday($m[1]);
}
if ($rdata['BYMONTH']) {
$months = explode(',', $rdata['BYMONTH']);
$this->setRecurByMonth($months);
}
} else {
$this->setRecurType(HORDE_DATE_RECUR_YEARLY_DATE);
}
break;
}
if (isset($rdata['UNTIL'])) {
list($year, $month, $mday) = sscanf($rdata['UNTIL'],
'%04d%02d%02d');
$this->setRecurEnd(new Horde_Date(array('year' => $year,
'month' => $month,
'mday' => $mday)));
}
if (isset($rdata['COUNT'])) {
$this->setRecurCount($rdata['COUNT']);
}
} else {
// No recurrence data - event does not recur.
$this->setRecurType(HORDE_DATE_RECUR_NONE);
}
}
/**
* Creates an iCalendar 2.0 recurrence rule.
*
* @link http://rfc.net/rfc2445.html#s4.3.10
* @link http://rfc.net/rfc2445.html#s4.8.5
* @link http://www.shuchow.com/vCalAddendum.html
*
* @param Horde_iCalendar $calendar A Horde_iCalendar object instance.
*
* @return string An iCalendar 2.0 conform RRULE value.
*/
function toRRule20($calendar)
{
switch ($this->recurType) {
case HORDE_DATE_RECUR_NONE:
return '';
case HORDE_DATE_RECUR_DAILY:
$rrule = 'FREQ=DAILY;INTERVAL=' . $this->recurInterval;
break;
case HORDE_DATE_RECUR_WEEKLY:
$rrule = 'FREQ=WEEKLY;INTERVAL=' . $this->recurInterval . ';BYDAY=';
$vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
for ($i = $flag = 0; $i <= 7 ; ++$i) {
if ($this->recurOnDay(pow(2, $i))) {
if ($flag) {
$rrule .= ',';
}
$rrule .= $vcaldays[$i];
$flag = true;
}
}
break;
case HORDE_DATE_RECUR_MONTHLY_DATE:
$rrule = 'FREQ=MONTHLY;INTERVAL=' . $this->recurInterval;
break;
case HORDE_DATE_RECUR_MONTHLY_WEEKDAY:
if ($this->recurNthDay != 0) {
$nth_weekday = $this->recurNthDay;
$day_of_week = log($this->recurData, 2);
} else {
$day_of_week = $this->start->dayOfWeek();
$nth_weekday = (int)($this->start->mday / 7);
if (($this->start->mday % 7) > 0) {
$nth_weekday++;
}
}
$vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
$rrule = 'FREQ=MONTHLY;INTERVAL=' . $this->recurInterval
. ';BYDAY=' . $nth_weekday . $vcaldays[$day_of_week];
break;
case HORDE_DATE_RECUR_YEARLY_DATE:
$rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval;
break;
case HORDE_DATE_RECUR_YEARLY_DAY:
$rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval
. ';BYYEARDAY=' . $this->start->dayOfYear();
break;
case HORDE_DATE_RECUR_YEARLY_WEEKDAY:
if ($this->recurNthDay != 0) {
$nth_weekday = $this->recurNthDay;
$day_of_week = log($this->recurData, 2);
} else {
$day_of_week = $this->start->dayOfWeek();
$nth_weekday = (int)($this->start->mday / 7);
if (($this->start->mday % 7) > 0) {
$nth_weekday++;
}
}
$months = !empty($this->recurMonths) ? join(',', $this->recurMonths) : $this->start->month;
$vcaldays = array('SU', 'MO', 'TU', 'WE', 'TH', 'FR', 'SA');
$rrule = 'FREQ=YEARLY;INTERVAL=' . $this->recurInterval
. ';BYDAY='
. $nth_weekday
. $vcaldays[$day_of_week]
. ';BYMONTH=' . $months;
break;
}
if ($this->hasRecurEnd()) {
$recurEnd = new Horde_Date($this->recurEnd);
$recurEnd->mday++;
$rrule .= ';UNTIL=' . $calendar->_exportDateTime($recurEnd);
}
if ($count = $this->getRecurCount()) {
$rrule .= ';COUNT=' . $count;
}
return $rrule;
}
/**
* Parses the recurrence data from a hash.
*
* @param array $hash The hash to convert.
*
* @return boolean True if the hash seemed valid, false otherwise.
*/
function fromHash($hash)
{
if (!isset($hash['interval']) || !isset($hash['interval']) ||
!isset($hash['range-type'])) {
$this->setRecurType(HORDE_DATE_RECUR_NONE);
return false;
}
$month2number = array(
'january' => 1,
'february' => 2,
'march' => 3,
'april' => 4,
'may' => 5,
'june' => 6,
'july' => 7,
'august' => 8,
'september' => 9,
'october' => 10,
'november' => 11,
'december' => 12,
);
$this->setRecurInterval((int) $hash['interval']);
$parse_day = false;
$set_daymask = false;
$update_month = false;
$update_daynumber = false;
$update_weekday = false;
$nth_weekday = -1;
switch ($hash['cycle']) {
case 'daily':
$this->setRecurType(HORDE_DATE_RECUR_DAILY);
break;
case 'weekly':
$this->setRecurType(HORDE_DATE_RECUR_WEEKLY);
$parse_day = true;
$set_daymask = true;
break;
case 'monthly':
if (!isset($hash['daynumber'])) {
$this->setRecurType(HORDE_DATE_RECUR_NONE);
return false;
}
switch ($hash['type']) {
case 'daynumber':
$this->setRecurType(HORDE_DATE_RECUR_MONTHLY_DATE);
$update_daynumber = true;
break;
case 'weekday':
$this->setRecurType(HORDE_DATE_RECUR_MONTHLY_WEEKDAY);
$this->setRecurNthWeekday($hash['daynumber']);
$parse_day = true;
$set_daymask = true;
break;
}
break;
case 'yearly':
if (!isset($hash['type'])) {
$this->setRecurType(HORDE_DATE_RECUR_NONE);
return false;
}
switch ($hash['type']) {
case 'monthday':
$this->setRecurType(HORDE_DATE_RECUR_YEARLY_DATE);
$update_month = true;
$update_daynumber = true;
break;
case 'yearday':
if (!isset($hash['month'])) {
$this->setRecurType(HORDE_DATE_RECUR_NONE);
return false;
}
$this->setRecurType(HORDE_DATE_RECUR_YEARLY_DAY);
// Start counting days in January.
$hash['month'] = 'january';
$update_month = true;
$update_daynumber = true;
break;
case 'weekday':
if (!isset($hash['daynumber'])) {
$this->setRecurType(HORDE_DATE_RECUR_NONE);
return false;
}
$this->setRecurType(HORDE_DATE_RECUR_YEARLY_WEEKDAY);
$this->setRecurNthWeekday($hash['daynumber']);
$parse_day = true;
$set_daymask = true;
if ($hash['month'] && isset($month2number[$hash['month']])) {
$this->setRecurByMonth($month2number[$hash['month']]);
}
break;
}
}
switch ($hash['range-type']) {
case 'number':
if (!isset($hash['range'])) {
$this->setRecurType(HORDE_DATE_RECUR_NONE);
return false;
}
$this->setRecurCount((int) $hash['range']);
break;
case 'date':
$recur_end = new Horde_Date($hash['range']);
$recur_end->hour = 23;
$recur_end->min = 59;
$recur_end->sec = 59;
$this->setRecurEnd($recur_end);
break;
}
// Need to parse <day>?
$last_found_day = -1;
if ($parse_day) {
if (!isset($hash['day'])) {
$this->setRecurType(HORDE_DATE_RECUR_NONE);
return false;
}
$mask = 0;
$bits = array(
'monday' => HORDE_DATE_MASK_MONDAY,
'tuesday' => HORDE_DATE_MASK_TUESDAY,
'wednesday' => HORDE_DATE_MASK_WEDNESDAY,
'thursday' => HORDE_DATE_MASK_THURSDAY,
'friday' => HORDE_DATE_MASK_FRIDAY,
'saturday' => HORDE_DATE_MASK_SATURDAY,
'sunday' => HORDE_DATE_MASK_SUNDAY,
);
$days = array(
'monday' => HORDE_DATE_MONDAY,
'tuesday' => HORDE_DATE_TUESDAY,
'wednesday' => HORDE_DATE_WEDNESDAY,
'thursday' => HORDE_DATE_THURSDAY,
'friday' => HORDE_DATE_FRIDAY,
'saturday' => HORDE_DATE_SATURDAY,
'sunday' => HORDE_DATE_SUNDAY,
);
foreach ($hash['day'] as $day) {
// Validity check.
if (empty($day) || !isset($bits[$day])) {
continue;
}
$mask |= $bits[$day];
$last_found_day = $days[$day];
}
if ($set_daymask) {
$this->setRecurOnDay($mask);
}
}
if ($update_month || $update_daynumber || $update_weekday) {
if ($update_month) {
if (isset($month2number[$hash['month']])) {
$this->start->month = $month2number[$hash['month']];
}
}
if ($update_daynumber) {
if (!isset($hash['daynumber'])) {
$this->setRecurType(HORDE_DATE_RECUR_NONE);
return false;
}
$this->start->mday = $hash['daynumber'];
}
if ($update_weekday) {
$this->start->setNthWeekday($last_found_day, $nth_weekday);
}
$this->start->correct();
}
// Exceptions.
if (isset($hash['exceptions'])) {
$this->exceptions = $hash['exceptions'];
}
if (isset($hash['completions'])) {
$this->completions = $hash['completions'];
}
return true;
}
/**
* Export this object into a hash.
*
* @return array The recurrence hash.
*/
function toHash()
{
if ($this->getRecurType() == HORDE_DATE_RECUR_NONE) {
return array();
}
$day2number = array(
0 => 'sunday',
1 => 'monday',
2 => 'tuesday',
3 => 'wednesday',
4 => 'thursday',
5 => 'friday',
6 => 'saturday'
);
$month2number = array(
1 => 'january',
2 => 'february',
3 => 'march',
4 => 'april',
5 => 'may',
6 => 'june',
7 => 'july',
8 => 'august',
9 => 'september',
10 => 'october',
11 => 'november',
12 => 'december'
);
$hash = array('interval' => $this->getRecurInterval());
$start = $this->getRecurStart();
switch ($this->getRecurType()) {
case HORDE_DATE_RECUR_DAILY:
$hash['cycle'] = 'daily';
break;
case HORDE_DATE_RECUR_WEEKLY:
$hash['cycle'] = 'weekly';
$bits = array(
'monday' => HORDE_DATE_MASK_MONDAY,
'tuesday' => HORDE_DATE_MASK_TUESDAY,
'wednesday' => HORDE_DATE_MASK_WEDNESDAY,
'thursday' => HORDE_DATE_MASK_THURSDAY,
'friday' => HORDE_DATE_MASK_FRIDAY,
'saturday' => HORDE_DATE_MASK_SATURDAY,
'sunday' => HORDE_DATE_MASK_SUNDAY,
);
$days = array();
foreach($bits as $name => $bit) {
if ($this->recurOnDay($bit)) {
$days[] = $name;
}
}
$hash['day'] = $days;
break;
case HORDE_DATE_RECUR_MONTHLY_DATE:
$hash['cycle'] = 'monthly';
$hash['type'] = 'daynumber';
$hash['daynumber'] = $start->mday;
break;
case HORDE_DATE_RECUR_MONTHLY_WEEKDAY:
$hash['cycle'] = 'monthly';
$hash['type'] = 'weekday';
$hash['daynumber'] = $start->weekOfMonth();
$hash['day'] = array ($day2number[$start->dayOfWeek()]);
break;
case HORDE_DATE_RECUR_YEARLY_DATE:
$hash['cycle'] = 'yearly';
$hash['type'] = 'monthday';
$hash['daynumber'] = $start->mday;
$hash['month'] = $month2number[$start->month];
break;
case HORDE_DATE_RECUR_YEARLY_DAY:
$hash['cycle'] = 'yearly';
$hash['type'] = 'yearday';
$hash['daynumber'] = $start->dayOfYear();
break;
case HORDE_DATE_RECUR_YEARLY_WEEKDAY:
$hash['cycle'] = 'yearly';
$hash['type'] = 'weekday';
$hash['daynumber'] = $start->weekOfMonth();
$hash['day'] = array ($day2number[$start->dayOfWeek()]);
$hash['month'] = $month2number[$start->month];
}
if ($this->hasRecurCount()) {
$hash['range-type'] = 'number';
$hash['range'] = $this->getRecurCount();
} elseif ($this->hasRecurEnd()) {
$date = $this->getRecurEnd();
$hash['range-type'] = 'date';
$hash['range'] = $date->datestamp();
} else {
$hash['range-type'] = 'none';
$hash['range'] = '';
}
// Recurrence exceptions
$hash['exceptions'] = $this->exceptions;
$hash['completions'] = $this->completions;
return $hash;
}
}
diff --git a/plugins/calendar/lib/Horde_iCalendar.php b/plugins/calendar/lib/Horde_iCalendar.php
new file mode 100644
index 00000000..f8981708
--- /dev/null
+++ b/plugins/calendar/lib/Horde_iCalendar.php
@@ -0,0 +1,3289 @@
+<?php
+
+/**
+ * This is a concatenated copy of the following files:
+ * Horde/String.php, Horde/iCalendar.php, Horde/iCalendar/*.php
+ */
+
+require_once(dirname(__FILE__) . '/Horde_Date.php');
+
+
+$GLOBALS['_HORDE_STRING_CHARSET'] = 'iso-8859-1';
+
+/**
+ * The String:: class provides static methods for charset and locale safe
+ * string manipulation.
+ *
+ * $Horde: framework/Util/String.php,v 1.43.6.38 2009-09-15 16:36:14 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author Jan Schneider <jan@horde.org>
+ * @since Horde 3.0
+ * @package Horde_Util
+ */
+class String {
+
+ /**
+ * Caches the result of extension_loaded() calls.
+ *
+ * @param string $ext The extension name.
+ *
+ * @return boolean Is the extension loaded?
+ *
+ * @see Util::extensionExists()
+ */
+ function extensionExists($ext)
+ {
+ static $cache = array();
+
+ if (!isset($cache[$ext])) {
+ $cache[$ext] = extension_loaded($ext);
+ }
+
+ return $cache[$ext];
+ }
+
+ /**
+ * Sets a default charset that the String:: methods will use if none is
+ * explicitly specified.
+ *
+ * @param string $charset The charset to use as the default one.
+ */
+ function setDefaultCharset($charset)
+ {
+ $GLOBALS['_HORDE_STRING_CHARSET'] = $charset;
+ if (String::extensionExists('mbstring') &&
+ function_exists('mb_regex_encoding')) {
+ $old_error = error_reporting(0);
+ mb_regex_encoding(String::_mbstringCharset($charset));
+ error_reporting($old_error);
+ }
+ }
+
+ /**
+ * Converts a string from one charset to another.
+ *
+ * Works only if either the iconv or the mbstring extension
+ * are present and best if both are available.
+ * The original string is returned if conversion failed or none
+ * of the extensions were available.
+ *
+ * @param mixed $input The data to be converted. If $input is an an array,
+ * the array's values get converted recursively.
+ * @param string $from The string's current charset.
+ * @param string $to The charset to convert the string to. If not
+ * specified, the global variable
+ * $_HORDE_STRING_CHARSET will be used.
+ *
+ * @return mixed The converted input data.
+ */
+ function convertCharset($input, $from, $to = null)
+ {
+ /* Don't bother converting numbers. */
+ if (is_numeric($input)) {
+ return $input;
+ }
+
+ /* Get the user's default character set if none passed in. */
+ if (is_null($to)) {
+ $to = $GLOBALS['_HORDE_STRING_CHARSET'];
+ }
+
+ /* If the from and to character sets are identical, return now. */
+ if ($from == $to) {
+ return $input;
+ }
+ $from = String::lower($from);
+ $to = String::lower($to);
+ if ($from == $to) {
+ return $input;
+ }
+
+ if (is_array($input)) {
+ $tmp = array();
+ reset($input);
+ while (list($key, $val) = each($input)) {
+ $tmp[String::_convertCharset($key, $from, $to)] = String::convertCharset($val, $from, $to);
+ }
+ return $tmp;
+ }
+ if (is_object($input)) {
+ // PEAR_Error objects are almost guaranteed to contain recursion,
+ // which will cause a segfault in PHP. We should never reach
+ // this line, but add a check and a log message to help the devs
+ // track down and fix this issue.
+ if (is_a($input, 'PEAR_Error')) {
+ Horde::logMessage('Called convertCharset() on a PEAR_Error object. ' . print_r($input, true), __FILE__, __LINE__, PEAR_LOG_DEBUG);
+ return '';
+ }
+ $vars = get_object_vars($input);
+ while (list($key, $val) = each($vars)) {
+ $input->$key = String::convertCharset($val, $from, $to);
+ }
+ return $input;
+ }
+
+ if (!is_string($input)) {
+ return $input;
+ }
+
+ return String::_convertCharset($input, $from, $to);
+ }
+
+ /**
+ * Internal function used to do charset conversion.
+ *
+ * @access private
+ *
+ * @param string $input See String::convertCharset().
+ * @param string $from See String::convertCharset().
+ * @param string $to See String::convertCharset().
+ *
+ * @return string The converted string.
+ */
+ function _convertCharset($input, $from, $to)
+ {
+ $output = '';
+ $from_check = (($from == 'iso-8859-1') || ($from == 'us-ascii'));
+ $to_check = (($to == 'iso-8859-1') || ($to == 'us-ascii'));
+
+ /* Use utf8_[en|de]code() if possible and if the string isn't too
+ * large (less than 16 MB = 16 * 1024 * 1024 = 16777216 bytes) - these
+ * functions use more memory. */
+ if (strlen($input) < 16777216 || !(String::extensionExists('iconv') || String::extensionExists('mbstring'))) {
+ if ($from_check && ($to == 'utf-8')) {
+ return utf8_encode($input);
+ }
+
+ if (($from == 'utf-8') && $to_check) {
+ return utf8_decode($input);
+ }
+ }
+
+ /* First try iconv with transliteration. */
+ if (($from != 'utf7-imap') &&
+ ($to != 'utf7-imap') &&
+ String::extensionExists('iconv')) {
+ /* We need to tack an extra character temporarily because of a bug
+ * in iconv() if the last character is not a 7 bit ASCII
+ * character. */
+ $oldTrackErrors = ini_set('track_errors', 1);
+ unset($php_errormsg);
+ $output = @iconv($from, $to . '//TRANSLIT', $input . 'x');
+ $output = (isset($php_errormsg)) ? false : String::substr($output, 0, -1, $to);
+ ini_set('track_errors', $oldTrackErrors);
+ }
+
+ /* Next try mbstring. */
+ if (!$output && String::extensionExists('mbstring')) {
+ $old_error = error_reporting(0);
+ $output = mb_convert_encoding($input, $to, String::_mbstringCharset($from));
+ error_reporting($old_error);
+ }
+
+ /* At last try imap_utf7_[en|de]code if appropriate. */
+ if (!$output && String::extensionExists('imap')) {
+ if ($from_check && ($to == 'utf7-imap')) {
+ return @imap_utf7_encode($input);
+ }
+ if (($from == 'utf7-imap') && $to_check) {
+ return @imap_utf7_decode($input);
+ }
+ }
+
+ return (!$output) ? $input : $output;
+ }
+
+ /**
+ * Makes a string lowercase.
+ *
+ * @param string $string The string to be converted.
+ * @param boolean $locale If true the string will be converted based on a
+ * given charset, locale independent else.
+ * @param string $charset If $locale is true, the charset to use when
+ * converting. If not provided the current charset.
+ *
+ * @return string The string with lowercase characters
+ */
+ function lower($string, $locale = false, $charset = null)
+ {
+ static $lowers;
+
+ if ($locale) {
+ /* The existence of mb_strtolower() depends on the platform. */
+ if (String::extensionExists('mbstring') &&
+ function_exists('mb_strtolower')) {
+ if (is_null($charset)) {
+ $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+ }
+ $old_error = error_reporting(0);
+ $ret = mb_strtolower($string, String::_mbstringCharset($charset));
+ error_reporting($old_error);
+ if (!empty($ret)) {
+ return $ret;
+ }
+ }
+ return strtolower($string);
+ }
+
+ if (!isset($lowers)) {
+ $lowers = array();
+ }
+ if (!isset($lowers[$string])) {
+ $language = setlocale(LC_CTYPE, 0);
+ setlocale(LC_CTYPE, 'C');
+ $lowers[$string] = strtolower($string);
+ setlocale(LC_CTYPE, $language);
+ }
+
+ return $lowers[$string];
+ }
+
+ /**
+ * Makes a string uppercase.
+ *
+ * @param string $string The string to be converted.
+ * @param boolean $locale If true the string will be converted based on a
+ * given charset, locale independent else.
+ * @param string $charset If $locale is true, the charset to use when
+ * converting. If not provided the current charset.
+ *
+ * @return string The string with uppercase characters
+ */
+ function upper($string, $locale = false, $charset = null)
+ {
+ static $uppers;
+
+ if ($locale) {
+ /* The existence of mb_strtoupper() depends on the
+ * platform. */
+ if (function_exists('mb_strtoupper')) {
+ if (is_null($charset)) {
+ $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+ }
+ $old_error = error_reporting(0);
+ $ret = mb_strtoupper($string, String::_mbstringCharset($charset));
+ error_reporting($old_error);
+ if (!empty($ret)) {
+ return $ret;
+ }
+ }
+ return strtoupper($string);
+ }
+
+ if (!isset($uppers)) {
+ $uppers = array();
+ }
+ if (!isset($uppers[$string])) {
+ $language = setlocale(LC_CTYPE, 0);
+ setlocale(LC_CTYPE, 'C');
+ $uppers[$string] = strtoupper($string);
+ setlocale(LC_CTYPE, $language);
+ }
+
+ return $uppers[$string];
+ }
+
+ /**
+ * Returns a string with the first letter capitalized if it is
+ * alphabetic.
+ *
+ * @param string $string The string to be capitalized.
+ * @param boolean $locale If true the string will be converted based on a
+ * given charset, locale independent else.
+ * @param string $charset The charset to use, defaults to current charset.
+ *
+ * @return string The capitalized string.
+ */
+ function ucfirst($string, $locale = false, $charset = null)
+ {
+ if ($locale) {
+ $first = String::substr($string, 0, 1, $charset);
+ if (String::isAlpha($first, $charset)) {
+ $string = String::upper($first, true, $charset) . String::substr($string, 1, null, $charset);
+ }
+ } else {
+ $string = String::upper(substr($string, 0, 1), false) . substr($string, 1);
+ }
+ return $string;
+ }
+
+ /**
+ * Returns part of a string.
+ *
+ * @param string $string The string to be converted.
+ * @param integer $start The part's start position, zero based.
+ * @param integer $length The part's length.
+ * @param string $charset The charset to use when calculating the part's
+ * position and length, defaults to current
+ * charset.
+ *
+ * @return string The string's part.
+ */
+ function substr($string, $start, $length = null, $charset = null)
+ {
+ if (is_null($length)) {
+ $length = String::length($string, $charset) - $start;
+ }
+
+ if ($length == 0) {
+ return '';
+ }
+
+ /* Try iconv. */
+ if (function_exists('iconv_substr')) {
+ if (is_null($charset)) {
+ $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+ }
+
+ $old_error = error_reporting(0);
+ $ret = iconv_substr($string, $start, $length, $charset);
+ error_reporting($old_error);
+ /* iconv_substr() returns false on failure. */
+ if ($ret !== false) {
+ return $ret;
+ }
+ }
+
+ /* Try mbstring. */
+ if (String::extensionExists('mbstring')) {
+ if (is_null($charset)) {
+ $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+ }
+ $old_error = error_reporting(0);
+ $ret = mb_substr($string, $start, $length, String::_mbstringCharset($charset));
+ error_reporting($old_error);
+ /* mb_substr() returns empty string on failure. */
+ if (strlen($ret)) {
+ return $ret;
+ }
+ }
+
+ return substr($string, $start, $length);
+ }
+
+ /**
+ * Returns the character (not byte) length of a string.
+ *
+ * @param string $string The string to return the length of.
+ * @param string $charset The charset to use when calculating the string's
+ * length.
+ *
+ * @return string The string's part.
+ */
+ function length($string, $charset = null)
+ {
+ if (is_null($charset)) {
+ $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+ }
+ $charset = String::lower($charset);
+ if ($charset == 'utf-8' || $charset == 'utf8') {
+ return strlen(utf8_decode($string));
+ }
+ if (String::extensionExists('mbstring')) {
+ $old_error = error_reporting(0);
+ $ret = mb_strlen($string, String::_mbstringCharset($charset));
+ error_reporting($old_error);
+ if (!empty($ret)) {
+ return $ret;
+ }
+ }
+ return strlen($string);
+ }
+
+ /**
+ * Returns the numeric position of the first occurrence of $needle
+ * in the $haystack string.
+ *
+ * @param string $haystack The string to search through.
+ * @param string $needle The string to search for.
+ * @param integer $offset Allows to specify which character in haystack
+ * to start searching.
+ * @param string $charset The charset to use when searching for the
+ * $needle string.
+ *
+ * @return integer The position of first occurrence.
+ */
+ function pos($haystack, $needle, $offset = 0, $charset = null)
+ {
+ if (String::extensionExists('mbstring')) {
+ if (is_null($charset)) {
+ $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+ }
+ $track_errors = ini_set('track_errors', 1);
+ $old_error = error_reporting(0);
+ $ret = mb_strpos($haystack, $needle, $offset, String::_mbstringCharset($charset));
+ error_reporting($old_error);
+ ini_set('track_errors', $track_errors);
+ if (!isset($php_errormsg)) {
+ return $ret;
+ }
+ }
+ return strpos($haystack, $needle, $offset);
+ }
+
+ /**
+ * Returns a string padded to a certain length with another string.
+ *
+ * This method behaves exactly like str_pad but is multibyte safe.
+ *
+ * @param string $input The string to be padded.
+ * @param integer $length The length of the resulting string.
+ * @param string $pad The string to pad the input string with. Must
+ * be in the same charset like the input string.
+ * @param const $type The padding type. One of STR_PAD_LEFT,
+ * STR_PAD_RIGHT, or STR_PAD_BOTH.
+ * @param string $charset The charset of the input and the padding
+ * strings.
+ *
+ * @return string The padded string.
+ */
+ function pad($input, $length, $pad = ' ', $type = STR_PAD_RIGHT,
+ $charset = null)
+ {
+ $mb_length = String::length($input, $charset);
+ $sb_length = strlen($input);
+ $pad_length = String::length($pad, $charset);
+
+ /* Return if we already have the length. */
+ if ($mb_length >= $length) {
+ return $input;
+ }
+
+ /* Shortcut for single byte strings. */
+ if ($mb_length == $sb_length && $pad_length == strlen($pad)) {
+ return str_pad($input, $length, $pad, $type);
+ }
+
+ switch ($type) {
+ case STR_PAD_LEFT:
+ $left = $length - $mb_length;
+ $output = String::substr(str_repeat($pad, ceil($left / $pad_length)), 0, $left, $charset) . $input;
+ break;
+ case STR_PAD_BOTH:
+ $left = floor(($length - $mb_length) / 2);
+ $right = ceil(($length - $mb_length) / 2);
+ $output = String::substr(str_repeat($pad, ceil($left / $pad_length)), 0, $left, $charset) .
+ $input .
+ String::substr(str_repeat($pad, ceil($right / $pad_length)), 0, $right, $charset);
+ break;
+ case STR_PAD_RIGHT:
+ $right = $length - $mb_length;
+ $output = $input . String::substr(str_repeat($pad, ceil($right / $pad_length)), 0, $right, $charset);
+ break;
+ }
+
+ return $output;
+ }
+
+ /**
+ * Wraps the text of a message.
+ *
+ * @since Horde 3.2
+ *
+ * @param string $string String containing the text to wrap.
+ * @param integer $width Wrap the string at this number of
+ * characters.
+ * @param string $break Character(s) to use when breaking lines.
+ * @param boolean $cut Whether to cut inside words if a line
+ * can't be wrapped.
+ * @param string $charset Character set to use when breaking lines.
+ * @param boolean $line_folding Whether to apply line folding rules per
+ * RFC 822 or similar. The correct break
+ * characters including leading whitespace
+ * have to be specified too.
+ *
+ * @return string String containing the wrapped text.
+ */
+ function wordwrap($string, $width = 75, $break = "\n", $cut = false,
+ $charset = null, $line_folding = false)
+ {
+ /* Get the user's default character set if none passed in. */
+ if (is_null($charset)) {
+ $charset = $GLOBALS['_HORDE_STRING_CHARSET'];
+ }
+ $charset = String::_mbstringCharset($charset);
+ $string = String::convertCharset($string, $charset, 'utf-8');
+ $wrapped = '';
+
+ while (String::length($string, 'utf-8') > $width) {
+ $line = String::substr($string, 0, $width, 'utf-8');
+ $string = String::substr($string, String::length($line, 'utf-8'), null, 'utf-8');
+ // Make sure didn't cut a word, unless we want hard breaks anyway.
+ if (!$cut && preg_match('/^(.+?)((\s|\r?\n).*)/us', $string, $match)) {
+ $line .= $match[1];
+ $string = $match[2];
+ }
+ // Wrap at existing line breaks.
+ if (preg_match('/^(.*?)(\r?\n)(.*)$/u', $line, $match)) {
+ $wrapped .= $match[1] . $match[2];
+ $string = $match[3] . $string;
+ continue;
+ }
+ // Wrap at the last colon or semicolon followed by a whitespace if
+ // doing line folding.
+ if ($line_folding &&
+ preg_match('/^(.*?)(;|:)(\s+.*)$/u', $line, $match)) {
+ $wrapped .= $match[1] . $match[2] . $break;
+ $string = $match[3] . $string;
+ continue;
+ }
+ // Wrap at the last whitespace of $line.
+ if ($line_folding) {
+ $sub = '(.+[^\s])';
+ } else {
+ $sub = '(.*)';
+ }
+ if (preg_match('/^' . $sub . '(\s+)(.*)$/u', $line, $match)) {
+ $wrapped .= $match[1] . $break;
+ $string = ($line_folding ? $match[2] : '') . $match[3] . $string;
+ continue;
+ }
+ // Hard wrap if necessary.
+ if ($cut) {
+ $wrapped .= $line . $break;
+ continue;
+ }
+ $wrapped .= $line;
+ }
+
+ return String::convertCharset($wrapped . $string, 'utf-8', $charset);
+ }
+
+ /**
+ * Wraps the text of a message.
+ *
+ * @param string $text String containing the text to wrap.
+ * @param integer $length Wrap $text at this number of characters.
+ * @param string $break_char Character(s) to use when breaking lines.
+ * @param string $charset Character set to use when breaking lines.
+ * @param boolean $quote Ignore lines that are wrapped with the '>'
+ * character (RFC 2646)? If true, we don't
+ * remove any padding whitespace at the end of
+ * the string.
+ *
+ * @return string String containing the wrapped text.
+ */
+ function wrap($text, $length = 80, $break_char = "\n", $charset = null,
+ $quote = false)
+ {
+ $paragraphs = array();
+
+ foreach (preg_split('/\r?\n/', $text) as $input) {
+ if ($quote && (strpos($input, '>') === 0)) {
+ $line = $input;
+ } else {
+ /* We need to handle the Usenet-style signature line
+ * separately; since the space after the two dashes is
+ * REQUIRED, we don't want to trim the line. */
+ if ($input != '-- ') {
+ $input = rtrim($input);
+ }
+ $line = String::wordwrap($input, $length, $break_char, false, $charset);
+ }
+
+ $paragraphs[] = $line;
+ }
+
+ return implode($break_char, $paragraphs);
+ }
+
+ /**
+ * Returns true if the every character in the parameter is an alphabetic
+ * character.
+ *
+ * @param $string The string to test.
+ * @param $charset The charset to use when testing the string.
+ *
+ * @return boolean True if the parameter was alphabetic only.
+ */
+ function isAlpha($string, $charset = null)
+ {
+ if (!String::extensionExists('mbstring')) {
+ return ctype_alpha($string);
+ }
+
+ $charset = String::_mbstringCharset($charset);
+ $old_charset = mb_regex_encoding();
+ $old_error = error_reporting(0);
+
+ if ($charset != $old_charset) {
+ mb_regex_encoding($charset);
+ }
+ $alpha = !mb_ereg_match('[^[:alpha:]]', $string);
+ if ($charset != $old_charset) {
+ mb_regex_encoding($old_charset);
+ }
+
+ error_reporting($old_error);
+
+ return $alpha;
+ }
+
+ /**
+ * Returns true if ever character in the parameter is a lowercase letter in
+ * the current locale.
+ *
+ * @param $string The string to test.
+ * @param $charset The charset to use when testing the string.
+ *
+ * @return boolean True if the parameter was lowercase.
+ */
+ function isLower($string, $charset = null)
+ {
+ return ((String::lower($string, true, $charset) === $string) &&
+ String::isAlpha($string, $charset));
+ }
+
+ /**
+ * Returns true if every character in the parameter is an uppercase letter
+ * in the current locale.
+ *
+ * @param string $string The string to test.
+ * @param string $charset The charset to use when testing the string.
+ *
+ * @return boolean True if the parameter was uppercase.
+ */
+ function isUpper($string, $charset = null)
+ {
+ return ((String::upper($string, true, $charset) === $string) &&
+ String::isAlpha($string, $charset));
+ }
+
+ /**
+ * Performs a multibyte safe regex match search on the text provided.
+ *
+ * @since Horde 3.1
+ *
+ * @param string $text The text to search.
+ * @param array $regex The regular expressions to use, without perl
+ * regex delimiters (e.g. '/' or '|').
+ * @param string $charset The character set of the text.
+ *
+ * @return array The matches array from the first regex that matches.
+ */
+ function regexMatch($text, $regex, $charset = null)
+ {
+ if (!empty($charset)) {
+ $regex = String::convertCharset($regex, $charset, 'utf-8');
+ $text = String::convertCharset($text, $charset, 'utf-8');
+ }
+
+ $matches = array();
+ foreach ($regex as $val) {
+ if (preg_match('/' . $val . '/u', $text, $matches)) {
+ break;
+ }
+ }
+
+ if (!empty($charset)) {
+ $matches = String::convertCharset($matches, 'utf-8', $charset);
+ }
+
+ return $matches;
+ }
+
+ /**
+ * Workaround charsets that don't work with mbstring functions.
+ *
+ * @access private
+ *
+ * @param string $charset The original charset.
+ *
+ * @return string The charset to use with mbstring functions.
+ */
+ function _mbstringCharset($charset)
+ {
+ /* mbstring functions do not handle the 'ks_c_5601-1987' &
+ * 'ks_c_5601-1989' charsets. However, these charsets are used, for
+ * example, by various versions of Outlook to send Korean characters.
+ * Use UHC (CP949) encoding instead. See, e.g.,
+ * http://lists.w3.org/Archives/Public/ietf-charsets/2001AprJun/0030.html */
+ if (in_array(String::lower($charset), array('ks_c_5601-1987', 'ks_c_5601-1989'))) {
+ $charset = 'UHC';
+ }
+
+ return $charset;
+ }
+
+}
+
+
+
+/**
+ * @package Horde_iCalendar
+ */
+
+/**
+ * String package
+ */
+
+
+
+/**
+ * Class representing iCalendar files.
+ *
+ * $Horde: framework/iCalendar/iCalendar.php,v 1.57.4.81 2010-11-10 14:34:25 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author Mike Cochrane <mike@graftonhall.co.nz>
+ * @since Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar {
+
+ /**
+ * The parent (containing) iCalendar object.
+ *
+ * @var Horde_iCalendar
+ */
+ var $_container = false;
+
+ /**
+ * The name/value pairs of attributes for this object (UID,
+ * DTSTART, etc.). Which are present depends on the object and on
+ * what kind of component it is.
+ *
+ * @var array
+ */
+ var $_attributes = array();
+
+ /**
+ * Any children (contained) iCalendar components of this object.
+ *
+ * @var array
+ */
+ var $_components = array();
+
+ /**
+ * According to RFC 2425, we should always use CRLF-terminated lines.
+ *
+ * @var string
+ */
+ var $_newline = "\r\n";
+
+ /**
+ * iCalendar format version (different behavior for 1.0 and 2.0
+ * especially with recurring events).
+ *
+ * @var string
+ */
+ var $_version;
+
+ function Horde_iCalendar($version = '2.0')
+ {
+ $this->_version = $version;
+ $this->setAttribute('VERSION', $version);
+ }
+
+ /**
+ * Return a reference to a new component.
+ *
+ * @param string $type The type of component to return
+ * @param Horde_iCalendar $container A container that this component
+ * will be associated with.
+ *
+ * @return object Reference to a Horde_iCalendar_* object as specified.
+ *
+ * @static
+ */
+ function &newComponent($type, &$container)
+ {
+ $type = String::lower($type);
+ $class = 'Horde_iCalendar_' . $type;
+ if (!class_exists($class)) {
+ include 'Horde/iCalendar/' . $type . '.php';
+ }
+ if (class_exists($class)) {
+ $component = new $class();
+ if ($container !== false) {
+ $component->_container = &$container;
+ // Use version of container, not default set by component
+ // constructor.
+ $component->_version = $container->_version;
+ }
+ } else {
+ // Should return an dummy x-unknown type class here.
+ $component = false;
+ }
+
+ return $component;
+ }
+
+ /**
+ * Sets the value of an attribute.
+ *
+ * @param string $name The name of the attribute.
+ * @param string $value The value of the attribute.
+ * @param array $params Array containing any addition parameters for
+ * this attribute.
+ * @param boolean $append True to append the attribute, False to replace
+ * the first matching attribute found.
+ * @param array $values Array representation of $value. For
+ * comma/semicolon seperated lists of values. If
+ * not set use $value as single array element.
+ */
+ function setAttribute($name, $value, $params = array(), $append = true,
+ $values = false)
+ {
+ // Make sure we update the internal format version if
+ // setAttribute('VERSION', ...) is called.
+ if ($name == 'VERSION') {
+ $this->_version = $value;
+ if ($this->_container !== false) {
+ $this->_container->_version = $value;
+ }
+ }
+
+ if (!$values) {
+ $values = array($value);
+ }
+ $found = false;
+ if (!$append) {
+ foreach (array_keys($this->_attributes) as $key) {
+ if ($this->_attributes[$key]['name'] == String::upper($name)) {
+ $this->_attributes[$key]['params'] = $params;
+ $this->_attributes[$key]['value'] = $value;
+ $this->_attributes[$key]['values'] = $values;
+ $found = true;
+ break;
+ }
+ }
+ }
+
+ if ($append || !$found) {
+ $this->_attributes[] = array(
+ 'name' => String::upper($name),
+ 'params' => $params,
+ 'value' => $value,
+ 'values' => $values
+ );
+ }
+ }
+
+ /**
+ * Sets parameter(s) for an (already existing) attribute. The
+ * parameter set is merged into the existing set.
+ *
+ * @param string $name The name of the attribute.
+ * @param array $params Array containing any additional parameters for
+ * this attribute.
+ * @return boolean True on success, false if no attribute $name exists.
+ */
+ function setParameter($name, $params = array())
+ {
+ $keys = array_keys($this->_attributes);
+ foreach ($keys as $key) {
+ if ($this->_attributes[$key]['name'] == $name) {
+ $this->_attributes[$key]['params'] =
+ array_merge($this->_attributes[$key]['params'], $params);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ /**
+ * Get the value of an attribute.
+ *
+ * @param string $name The name of the attribute.
+ * @param boolean $params Return the parameters for this attribute instead
+ * of its value.
+ *
+ * @return mixed (object) PEAR_Error if the attribute does not exist.
+ * (string) The value of the attribute.
+ * (array) The parameters for the attribute or
+ * multiple values for an attribute.
+ */
+ function getAttribute($name, $params = false)
+ {
+ $result = array();
+ foreach ($this->_attributes as $attribute) {
+ if ($attribute['name'] == $name) {
+ if ($params) {
+ $result[] = $attribute['params'];
+ } else {
+ $result[] = $attribute['value'];
+ }
+ }
+ }
+ if (!count($result)) {
+ require_once 'PEAR.php';
+ return PEAR::raiseError('Attribute "' . $name . '" Not Found');
+ } if (count($result) == 1 && !$params) {
+ return $result[0];
+ } else {
+ return $result;
+ }
+ }
+
+ /**
+ * Gets the values of an attribute as an array. Multiple values
+ * are possible due to:
+ *
+ * a) multiplce occurences of 'name'
+ * b) (unsecapd) comma seperated lists.
+ *
+ * So for a vcard like "KEY:a,b\nKEY:c" getAttributesValues('KEY')
+ * will return array('a', 'b', 'c').
+ *
+ * @param string $name The name of the attribute.
+ * @return mixed (object) PEAR_Error if the attribute does not exist.
+ * (array) Multiple values for an attribute.
+ */
+ function getAttributeValues($name)
+ {
+ $result = array();
+ foreach ($this->_attributes as $attribute) {
+ if ($attribute['name'] == $name) {
+ $result = array_merge($attribute['values'], $result);
+ }
+ }
+ if (!count($result)) {
+ return PEAR::raiseError('Attribute "' . $name . '" Not Found');
+ }
+ return $result;
+ }
+
+ /**
+ * Returns the value of an attribute, or a specified default value
+ * if the attribute does not exist.
+ *
+ * @param string $name The name of the attribute.
+ * @param mixed $default What to return if the attribute specified by
+ * $name does not exist.
+ *
+ * @return mixed (string) The value of $name.
+ * (mixed) $default if $name does not exist.
+ */
+ function getAttributeDefault($name, $default = '')
+ {
+ $value = $this->getAttribute($name);
+ return is_a($value, 'PEAR_Error') ? $default : $value;
+ }
+
+ /**
+ * Remove all occurences of an attribute.
+ *
+ * @param string $name The name of the attribute.
+ */
+ function removeAttribute($name)
+ {
+ $keys = array_keys($this->_attributes);
+ foreach ($keys as $key) {
+ if ($this->_attributes[$key]['name'] == $name) {
+ unset($this->_attributes[$key]);
+ }
+ }
+ }
+
+ /**
+ * Get attributes for all tags or for a given tag.
+ *
+ * @param string $tag Return attributes for this tag, or all attributes if
+ * not given.
+ *
+ * @return array An array containing all the attributes and their types.
+ */
+ function getAllAttributes($tag = false)
+ {
+ if ($tag === false) {
+ return $this->_attributes;
+ }
+ $result = array();
+ foreach ($this->_attributes as $attribute) {
+ if ($attribute['name'] == $tag) {
+ $result[] = $attribute;
+ }
+ }
+ return $result;
+ }
+
+ /**
+ * Add a vCalendar component (eg vEvent, vTimezone, etc.).
+ *
+ * @param Horde_iCalendar $component Component (subclass) to add.
+ */
+ function addComponent($component)
+ {
+ if (is_a($component, 'Horde_iCalendar')) {
+ $component->_container = &$this;
+ $this->_components[] = &$component;
+ }
+ }
+
+ /**
+ * Retrieve all the components.
+ *
+ * @return array Array of Horde_iCalendar objects.
+ */
+ function getComponents()
+ {
+ return $this->_components;
+ }
+
+ function getType()
+ {
+ return 'vcalendar';
+ }
+
+ /**
+ * Return the classes (entry types) we have.
+ *
+ * @return array Hash with class names Horde_iCalendar_xxx as keys
+ * and number of components of this class as value.
+ */
+ function getComponentClasses()
+ {
+ $r = array();
+ foreach ($this->_components as $c) {
+ $cn = strtolower(get_class($c));
+ if (empty($r[$cn])) {
+ $r[$cn] = 1;
+ } else {
+ $r[$cn]++;
+ }
+ }
+
+ return $r;
+ }
+
+ /**
+ * Number of components in this container.
+ *
+ * @return integer Number of components in this container.
+ */
+ function getComponentCount()
+ {
+ return count($this->_components);
+ }
+
+ /**
+ * Retrieve a specific component.
+ *
+ * @param integer $idx The index of the object to retrieve.
+ *
+ * @return mixed (boolean) False if the index does not exist.
+ * (Horde_iCalendar_*) The requested component.
+ */
+ function getComponent($idx)
+ {
+ if (isset($this->_components[$idx])) {
+ return $this->_components[$idx];
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Locates the first child component of the specified class, and returns a
+ * reference to it.
+ *
+ * @param string $type The type of component to find.
+ *
+ * @return boolean|Horde_iCalendar_* False if no subcomponent of the
+ * specified class exists or a reference
+ * to the requested component.
+ */
+ function &findComponent($childclass)
+ {
+ $childclass = 'Horde_iCalendar_' . String::lower($childclass);
+ $keys = array_keys($this->_components);
+ foreach ($keys as $key) {
+ if (is_a($this->_components[$key], $childclass)) {
+ return $this->_components[$key];
+ }
+ }
+
+ $component = false;
+ return $component;
+ }
+
+ /**
+ * Locates the first matching child component of the specified class, and
+ * returns a reference to it.
+ *
+ * @param string $childclass The type of component to find.
+ * @param string $attribute This attribute must be set in the component
+ * for it to match.
+ * @param string $value Optional value that $attribute must match.
+ *
+ * @return boolean|Horde_iCalendar_* False if no matching subcomponent of
+ * the specified class exists, or a
+ * reference to the requested component.
+ */
+ function &findComponentByAttribute($childclass, $attribute, $value = null)
+ {
+ $childclass = 'Horde_iCalendar_' . String::lower($childclass);
+ $keys = array_keys($this->_components);
+ foreach ($keys as $key) {
+ if (is_a($this->_components[$key], $childclass)) {
+ $attr = $this->_components[$key]->getAttribute($attribute);
+ if (is_a($attr, 'PEAR_Error')) {
+ continue;
+ }
+ if ($value !== null && $value != $attr) {
+ continue;
+ }
+ return $this->_components[$key];
+ }
+ }
+
+ $component = false;
+ return $component;
+ }
+
+ /**
+ * Clears the iCalendar object (resets the components and attributes
+ * arrays).
+ */
+ function clear()
+ {
+ $this->_components = array();
+ $this->_attributes = array();
+ }
+
+ /**
+ * Checks if entry is vcalendar 1.0, vcard 2.1 or vnote 1.1.
+ *
+ * These 'old' formats are defined by www.imc.org. The 'new' (non-old)
+ * formats icalendar 2.0 and vcard 3.0 are defined in rfc2426 and rfc2445
+ * respectively.
+ *
+ * @since Horde 3.1.2
+ */
+ function isOldFormat()
+ {
+ if ($this->getType() == 'vcard') {
+ return ($this->_version < 3);
+ }
+ if ($this->getType() == 'vNote') {
+ return ($this->_version < 2);
+ }
+ if ($this->_version >= 2) {
+ return false;
+ }
+ return true;
+ }
+
+ /**
+ * Export as vCalendar format.
+ */
+ function exportvCalendar()
+ {
+ // Default values.
+ $requiredAttributes['PRODID'] = '-//The Horde Project//Horde_iCalendar Library' . (defined('HORDE_VERSION') ? ', Horde ' . constant('HORDE_VERSION') : '') . '//EN';
+ $requiredAttributes['METHOD'] = 'PUBLISH';
+
+ foreach ($requiredAttributes as $name => $default_value) {
+ if (is_a($this->getattribute($name), 'PEAR_Error')) {
+ $this->setAttribute($name, $default_value);
+ }
+ }
+
+ return $this->_exportvData('VCALENDAR');
+ }
+
+ /**
+ * Export this entry as a hash array with tag names as keys.
+ *
+ * @param boolean $paramsInKeys
+ * If false, the operation can be quite lossy as the
+ * parameters are ignored when building the array keys.
+ * So if you export a vcard with
+ * LABEL;TYPE=WORK:foo
+ * LABEL;TYPE=HOME:bar
+ * the resulting hash contains only one label field!
+ * If set to true, array keys look like 'LABEL;TYPE=WORK'
+ * @return array A hash array with tag names as keys.
+ */
+ function toHash($paramsInKeys = false)
+ {
+ $hash = array();
+ foreach ($this->_attributes as $a) {
+ $k = $a['name'];
+ if ($paramsInKeys && is_array($a['params'])) {
+ foreach ($a['params'] as $p => $v) {
+ $k .= ";$p=$v";
+ }
+ }
+ $hash[$k] = $a['value'];
+ }
+
+ return $hash;
+ }
+
+ /**
+ * Parses a string containing vCalendar data.
+ *
+ * @todo This method doesn't work well at all, if $base is VCARD.
+ *
+ * @param string $text The data to parse.
+ * @param string $base The type of the base object.
+ * @param string $charset The encoding charset for $text. Defaults to
+ * utf-8 for new format, iso-8859-1 for old format.
+ * @param boolean $clear If true clears the iCal object before parsing.
+ *
+ * @return boolean True on successful import, false otherwise.
+ */
+ function parsevCalendar($text, $base = 'VCALENDAR', $charset = null,
+ $clear = true)
+ {
+ if ($clear) {
+ $this->clear();
+ }
+ if (preg_match('/^BEGIN:' . $base . '(.*)^END:' . $base . '/ism', $text, $matches)) {
+ $container = true;
+ $vCal = $matches[1];
+ } else {
+ // Text isn't enclosed in BEGIN:VCALENDAR
+ // .. END:VCALENDAR. We'll try to parse it anyway.
+ $container = false;
+ $vCal = $text;
+ }
+ $vCal = trim($vCal);
+
+ // Extract all subcomponents.
+ $matches = $components = null;
+ if (preg_match_all('/^BEGIN:(.*)(\r\n|\r|\n)(.*)^END:\1/Uims', $vCal, $components)) {
+ foreach ($components[0] as $key => $data) {
+ // Remove from the vCalendar data.
+ $vCal = str_replace($data, '', $vCal);
+ }
+ } elseif (!$container) {
+ return false;
+ }
+
+ // Unfold "quoted printable" folded lines like:
+ // BODY;ENCODING=QUOTED-PRINTABLE:=
+ // another=20line=
+ // last=20line
+ while (preg_match_all('/^([^:]+;\s*(ENCODING=)?QUOTED-PRINTABLE(.*=\r?\n)+(.*[^=])?\r?\n)/mU', $vCal, $matches)) {
+ foreach ($matches[1] as $s) {
+ $r = preg_replace('/=\r?\n/', '', $s);
+ $vCal = str_replace($s, $r, $vCal);
+ }
+ }
+
+ // Unfold any folded lines.
+ if ($this->isOldFormat()) {
+ $vCal = preg_replace('/[\r\n]+([ \t])/', '$1', $vCal);
+ } else {
+ $vCal = preg_replace('/[\r\n]+[ \t]/', '', $vCal);
+ }
+
+ // Parse the remaining attributes.
+ if (preg_match_all('/^((?:[^":]+|(?:"[^"]*")+)*):([^\r\n]*)\r?$/m', $vCal, $matches)) {
+ foreach ($matches[0] as $attribute) {
+ preg_match('/([^;^:]*)((;(?:[^":]+|(?:"[^"]*")+)*)?):([^\r\n]*)[\r\n]*/', $attribute, $parts);
+ $tag = trim(String::upper($parts[1]));
+ $value = $parts[4];
+ $params = array();
+
+ // Parse parameters.
+ if (!empty($parts[2])) {
+ preg_match_all('/;(([^;=]*)(=("[^"]*"|[^;]*))?)/', $parts[2], $param_parts);
+ foreach ($param_parts[2] as $key => $paramName) {
+ $paramName = String::upper($paramName);
+ $paramValue = $param_parts[4][$key];
+ if ($paramName == 'TYPE') {
+ $paramValue = preg_split('/(?<!\\\\),/', $paramValue);
+ if (count($paramValue) == 1) {
+ $paramValue = $paramValue[0];
+ }
+ }
+ if (is_string($paramValue)) {
+ if (preg_match('/"([^"]*)"/', $paramValue, $parts)) {
+ $paramValue = $parts[1];
+ }
+ } else {
+ foreach ($paramValue as $k => $tmp) {
+ if (preg_match('/"([^"]*)"/', $tmp, $parts)) {
+ $paramValue[$k] = $parts[1];
+ }
+ }
+ }
+ $params[$paramName] = $paramValue;
+ }
+ }
+
+ // Charset and encoding handling.
+ if ((isset($params['ENCODING']) &&
+ String::upper($params['ENCODING']) == 'QUOTED-PRINTABLE') ||
+ isset($params['QUOTED-PRINTABLE'])) {
+
+ $value = quoted_printable_decode($value);
+ if (isset($params['CHARSET'])) {
+ $value = String::convertCharset($value, $params['CHARSET']);
+ } else {
+ $value = String::convertCharset($value, empty($charset) ? ($this->isOldFormat() ? 'iso-8859-1' : 'utf-8') : $charset);
+ }
+ } elseif (isset($params['CHARSET'])) {
+ $value = String::convertCharset($value, $params['CHARSET']);
+ } else {
+ // As per RFC 2279, assume UTF8 if we don't have an
+ // explicit charset parameter.
+ $value = String::convertCharset($value, empty($charset) ? ($this->isOldFormat() ? 'iso-8859-1' : 'utf-8') : $charset);
+ }
+
+ // Get timezone info for date fields from $params.
+ $tzid = isset($params['TZID']) ? trim($params['TZID'], '\"') : false;
+
+ switch ($tag) {
+ // Date fields.
+ case 'COMPLETED':
+ case 'CREATED':
+ case 'LAST-MODIFIED':
+ case 'X-MOZ-LASTACK':
+ case 'X-MOZ-SNOOZE-TIME':
+ $this->setAttribute($tag, $this->_parseDateTime($value, $tzid), $params);
+ break;
+
+ case 'BDAY':
+ case 'X-SYNCJE-ANNIVERSARY':
+ case 'X-ANNIVERSARY':
+ $this->setAttribute($tag, $this->_parseDate($value), $params);
+ break;
+
+ case 'DTEND':
+ case 'DTSTART':
+ case 'DTSTAMP':
+ case 'DUE':
+ case 'AALARM':
+ case 'RECURRENCE-ID':
+ // types like AALARM may contain additional data after a ;
+ // ignore these.
+ $ts = explode(';', $value);
+ if (isset($params['VALUE']) && $params['VALUE'] == 'DATE') {
+ $this->setAttribute($tag, $this->_parseDate($ts[0]), $params);
+ } else {
+ $this->setAttribute($tag, $this->_parseDateTime($ts[0], $tzid), $params);
+ }
+ break;
+
+ case 'TRIGGER':
+ if (isset($params['VALUE']) &&
+ $params['VALUE'] == 'DATE-TIME') {
+ $this->setAttribute($tag, $this->_parseDateTime($value, $tzid), $params);
+ } else {
+ $this->setAttribute($tag, $this->_parseDuration($value), $params);
+ }
+ break;
+
+ // Comma seperated dates.
+ case 'EXDATE':
+ case 'RDATE':
+ if (!strlen($value)) {
+ break;
+ }
+ $dates = array();
+ $separator = $this->isOldFormat() ? ';' : ',';
+ preg_match_all('/' . $separator . '([^' . $separator . ']*)/', $separator . $value, $values);
+
+ foreach ($values[1] as $value) {
+ $dates[] = $this->_parseDate($value);
+ }
+ $this->setAttribute($tag, isset($dates[0]) ? $dates[0] : null, $params, true, $dates);
+ break;
+
+ // Duration fields.
+ case 'DURATION':
+ $this->setAttribute($tag, $this->_parseDuration($value), $params);
+ break;
+
+ // Period of time fields.
+ case 'FREEBUSY':
+ $periods = array();
+ preg_match_all('/,([^,]*)/', ',' . $value, $values);
+ foreach ($values[1] as $value) {
+ $periods[] = $this->_parsePeriod($value);
+ }
+
+ $this->setAttribute($tag, isset($periods[0]) ? $periods[0] : null, $params, true, $periods);
+ break;
+
+ // UTC offset fields.
+ case 'TZOFFSETFROM':
+ case 'TZOFFSETTO':
+ $this->setAttribute($tag, $this->_parseUtcOffset($value), $params);
+ break;
+
+ // Integer fields.
+ case 'PERCENT-COMPLETE':
+ case 'PRIORITY':
+ case 'REPEAT':
+ case 'SEQUENCE':
+ $this->setAttribute($tag, intval($value), $params);
+ break;
+
+ // Geo fields.
+ case 'GEO':
+ if ($this->isOldFormat()) {
+ $floats = explode(',', $value);
+ $value = array('latitude' => floatval($floats[1]),
+ 'longitude' => floatval($floats[0]));
+ } else {
+ $floats = explode(';', $value);
+ $value = array('latitude' => floatval($floats[0]),
+ 'longitude' => floatval($floats[1]));
+ }
+ $this->setAttribute($tag, $value, $params);
+ break;
+
+ // Recursion fields.
+ case 'EXRULE':
+ case 'RRULE':
+ $this->setAttribute($tag, trim($value), $params);
+ break;
+
+ // ADR, ORG and N are lists seperated by unescaped semicolons
+ // with a specific number of slots.
+ case 'ADR':
+ case 'N':
+ case 'ORG':
+ $value = trim($value);
+ // As of rfc 2426 2.4.2 semicolon, comma, and colon must
+ // be escaped (comma is unescaped after splitting below).
+ $value = str_replace(array('\\n', '\\N', '\\;', '\\:'),
+ array($this->_newline, $this->_newline, ';', ':'),
+ $value);
+
+ // Split by unescaped semicolons:
+ $values = preg_split('/(?<!\\\\);/', $value);
+ $value = str_replace('\\;', ';', $value);
+ $values = str_replace('\\;', ';', $values);
+ $this->setAttribute($tag, trim($value), $params, true, $values);
+ break;
+
+ // String fields.
+ default:
+ if ($this->isOldFormat()) {
+ // vCalendar 1.0 and vCard 2.1 only escape semicolons
+ // and use unescaped semicolons to create lists.
+ $value = trim($value);
+ // Split by unescaped semicolons:
+ $values = preg_split('/(?<!\\\\);/', $value);
+ $value = str_replace('\\;', ';', $value);
+ $values = str_replace('\\;', ';', $values);
+ $this->setAttribute($tag, trim($value), $params, true, $values);
+ } else {
+ $value = trim($value);
+ // As of rfc 2426 2.4.2 semicolon, comma, and colon
+ // must be escaped (comma is unescaped after splitting
+ // below).
+ $value = str_replace(array('\\n', '\\N', '\\;', '\\:', '\\\\'),
+ array($this->_newline, $this->_newline, ';', ':', '\\'),
+ $value);
+
+ // Split by unescaped commas.
+ $values = preg_split('/(?<!\\\\),/', $value);
+ $value = str_replace('\\,', ',', $value);
+ $values = str_replace('\\,', ',', $values);
+
+ $this->setAttribute($tag, trim($value), $params, true, $values);
+ }
+ break;
+ }
+ }
+ }
+
+ // Process all components.
+ if ($components) {
+ // vTimezone components are processed first. They are
+ // needed to process vEvents that may use a TZID.
+ foreach ($components[0] as $key => $data) {
+ $type = trim($components[1][$key]);
+ if ($type != 'VTIMEZONE') {
+ continue;
+ }
+ $component = &Horde_iCalendar::newComponent($type, $this);
+ if ($component === false) {
+ return PEAR::raiseError("Unable to create object for type $type");
+ }
+ $component->parsevCalendar($data, $type, $charset);
+
+ $this->addComponent($component);
+ }
+
+ // Now process the non-vTimezone components.
+ foreach ($components[0] as $key => $data) {
+ $type = trim($components[1][$key]);
+ if ($type == 'VTIMEZONE') {
+ continue;
+ }
+ $component = &Horde_iCalendar::newComponent($type, $this);
+ if ($component === false) {
+ return PEAR::raiseError("Unable to create object for type $type");
+ }
+ $component->parsevCalendar($data, $type, $charset);
+
+ $this->addComponent($component);
+ }
+ }
+
+ return true;
+ }
+
+ /**
+ * Export this component in vCal format.
+ *
+ * @param string $base The type of the base object.
+ *
+ * @return string vCal format data.
+ */
+ function _exportvData($base = 'VCALENDAR')
+ {
+ $result = 'BEGIN:' . String::upper($base) . $this->_newline;
+
+ // VERSION is not allowed for entries enclosed in VCALENDAR/ICALENDAR,
+ // as it is part of the enclosing VCALENDAR/ICALENDAR. See rfc2445
+ if ($base !== 'VEVENT' && $base !== 'VTODO' && $base !== 'VALARM' &&
+ $base !== 'VJOURNAL' && $base !== 'VFREEBUSY') {
+ // Ensure that version is the first attribute.
+ $result .= 'VERSION:' . $this->_version . $this->_newline;
+ }
+ foreach ($this->_attributes as $attribute) {
+ $name = $attribute['name'];
+ if ($name == 'VERSION') {
+ // Already done.
+ continue;
+ }
+
+ $params_str = '';
+ $params = $attribute['params'];
+ if ($params) {
+ foreach ($params as $param_name => $param_value) {
+ /* Skip CHARSET for iCalendar 2.0 data, not allowed. */
+ if ($param_name == 'CHARSET' && !$this->isOldFormat()) {
+ continue;
+ }
+ /* Skip VALUE=DATE for vCalendar 1.0 data, not allowed. */
+ if ($this->isOldFormat() &&
+ $param_name == 'VALUE' && $param_value == 'DATE') {
+ continue;
+ }
+
+ if ($param_value === null) {
+ $params_str .= ";$param_name";
+ } else {
+ $len = strlen($param_value);
+ $safe_value = '';
+ $quote = false;
+ for ($i = 0; $i < $len; ++$i) {
+ $ord = ord($param_value[$i]);
+ // Accept only valid characters.
+ if ($ord == 9 || $ord == 32 || $ord == 33 ||
+ ($ord >= 35 && $ord <= 126) ||
+ $ord >= 128) {
+ $safe_value .= $param_value[$i];
+ // Characters above 128 do not need to be
+ // quoted as per RFC2445 but Outlook requires
+ // this.
+ if ($ord == 44 || $ord == 58 || $ord == 59 ||
+ $ord >= 128) {
+ $quote = true;
+ }
+ }
+ }
+ if ($quote) {
+ $safe_value = '"' . $safe_value . '"';
+ }
+ $params_str .= ";$param_name=$safe_value";
+ }
+ }
+ }
+
+ $value = $attribute['value'];
+ switch ($name) {
+ // Date fields.
+ case 'COMPLETED':
+ case 'CREATED':
+ case 'DCREATED':
+ case 'LAST-MODIFIED':
+ case 'X-MOZ-LASTACK':
+ case 'X-MOZ-SNOOZE-TIME':
+ $value = $this->_exportDateTime($value);
+ break;
+
+ case 'DTEND':
+ case 'DTSTART':
+ case 'DTSTAMP':
+ case 'DUE':
+ case 'AALARM':
+ case 'RECURRENCE-ID':
+ if (isset($params['VALUE'])) {
+ if ($params['VALUE'] == 'DATE') {
+ // VCALENDAR 1.0 uses T000000 - T235959 for all day events:
+ if ($this->isOldFormat() && $name == 'DTEND') {
+ $d = new Horde_Date($value);
+ $value = new Horde_Date(array(
+ 'year' => $d->year,
+ 'month' => $d->month,
+ 'mday' => $d->mday - 1));
+ $value->correct();
+ $value = $this->_exportDate($value, '235959');
+ } else {
+ $value = $this->_exportDate($value, '000000');
+ }
+ } else {
+ $value = $this->_exportDateTime($value);
+ }
+ } else {
+ $value = $this->_exportDateTime($value);
+ }
+ break;
+
+ // Comma seperated dates.
+ case 'EXDATE':
+ case 'RDATE':
+ $dates = array();
+ foreach ($value as $date) {
+ if (isset($params['VALUE'])) {
+ if ($params['VALUE'] == 'DATE') {
+ $dates[] = $this->_exportDate($date, '000000');
+ } elseif ($params['VALUE'] == 'PERIOD') {
+ $dates[] = $this->_exportPeriod($date);
+ } else {
+ $dates[] = $this->_exportDateTime($date);
+ }
+ } else {
+ $dates[] = $this->_exportDateTime($date);
+ }
+ }
+ $value = implode($this->isOldFormat() ? ';' : ',', $dates);
+ break;
+
+ case 'TRIGGER':
+ if (isset($params['VALUE'])) {
+ if ($params['VALUE'] == 'DATE-TIME') {
+ $value = $this->_exportDateTime($value);
+ } elseif ($params['VALUE'] == 'DURATION') {
+ $value = $this->_exportDuration($value);
+ }
+ } else {
+ $value = $this->_exportDuration($value);
+ }
+ break;
+
+ // Duration fields.
+ case 'DURATION':
+ $value = $this->_exportDuration($value);
+ break;
+
+ // Period of time fields.
+ case 'FREEBUSY':
+ $value_str = '';
+ foreach ($value as $period) {
+ $value_str .= empty($value_str) ? '' : ',';
+ $value_str .= $this->_exportPeriod($period);
+ }
+ $value = $value_str;
+ break;
+
+ // UTC offset fields.
+ case 'TZOFFSETFROM':
+ case 'TZOFFSETTO':
+ $value = $this->_exportUtcOffset($value);
+ break;
+
+ // Integer fields.
+ case 'PERCENT-COMPLETE':
+ case 'PRIORITY':
+ case 'REPEAT':
+ case 'SEQUENCE':
+ $value = "$value";
+ break;
+
+ // Geo fields.
+ case 'GEO':
+ if ($this->isOldFormat()) {
+ $value = $value['longitude'] . ',' . $value['latitude'];
+ } else {
+ $value = $value['latitude'] . ';' . $value['longitude'];
+ }
+ break;
+
+ // Recurrence fields.
+ case 'EXRULE':
+ case 'RRULE':
+ break;
+
+ default:
+ if ($this->isOldFormat()) {
+ if (is_array($attribute['values']) &&
+ count($attribute['values']) > 1) {
+ $values = $attribute['values'];
+ if ($name == 'N' || $name == 'ADR' || $name == 'ORG') {
+ $glue = ';';
+ } else {
+ $glue = ',';
+ }
+ $values = str_replace(';', '\\;', $values);
+ $value = implode($glue, $values);
+ } else {
+ /* vcard 2.1 and vcalendar 1.0 escape only
+ * semicolons */
+ $value = str_replace(';', '\\;', $value);
+ }
+ // Text containing newlines or ASCII >= 127 must be BASE64
+ // or QUOTED-PRINTABLE encoded. Currently we use
+ // QUOTED-PRINTABLE as default.
+ if (preg_match("/[^\x20-\x7F]/", $value) &&
+ empty($params['ENCODING'])) {
+ $params['ENCODING'] = 'QUOTED-PRINTABLE';
+ $params_str .= ';ENCODING=QUOTED-PRINTABLE';
+ // Add CHARSET as well. At least the synthesis client
+ // gets confused otherwise
+ if (empty($params['CHARSET'])) {
+ $params['CHARSET'] = 'UTF-8';
+ $params_str .= ';CHARSET=' . $params['CHARSET'];
+ }
+ }
+ } else {
+ if (is_array($attribute['values']) &&
+ count($attribute['values'])) {
+ $values = $attribute['values'];
+ if ($name == 'N' || $name == 'ADR' || $name == 'ORG') {
+ $glue = ';';
+ } else {
+ $glue = ',';
+ }
+ // As of rfc 2426 2.5 semicolon and comma must be
+ // escaped.
+ $values = str_replace(array('\\', ';', ','),
+ array('\\\\', '\\;', '\\,'),
+ $values);
+ $value = implode($glue, $values);
+ } else {
+ // As of rfc 2426 2.5 semicolon and comma must be
+ // escaped.
+ $value = str_replace(array('\\', ';', ','),
+ array('\\\\', '\\;', '\\,'),
+ $value);
+ }
+ $value = preg_replace('/\r?\n/', '\n', $value);
+ }
+ break;
+ }
+
+ $value = str_replace("\r", '', $value);
+ if (!empty($params['ENCODING']) &&
+ $params['ENCODING'] == 'QUOTED-PRINTABLE' &&
+ strlen(trim($value))) {
+ $result .= $name . $params_str . ':'
+ . str_replace('=0A', '=0D=0A',
+ $this->_quotedPrintableEncode($value))
+ . $this->_newline;
+ } else {
+ $attr_string = $name . $params_str . ':' . $value;
+ if (!$this->isOldFormat()) {
+ $attr_string = String::wordwrap($attr_string, 75, $this->_newline . ' ',
+ true, 'utf-8', true);
+ }
+ $result .= $attr_string . $this->_newline;
+ }
+ }
+
+ foreach ($this->_components as $component) {
+ $result .= $component->exportvCalendar();
+ }
+
+ return $result . 'END:' . $base . $this->_newline;
+ }
+
+ /**
+ * Parse a UTC Offset field.
+ */
+ function _parseUtcOffset($text)
+ {
+ $offset = array();
+ if (preg_match('/(\+|-)([0-9]{2})([0-9]{2})([0-9]{2})?/', $text, $timeParts)) {
+ $offset['ahead'] = (bool)($timeParts[1] == '+');
+ $offset['hour'] = intval($timeParts[2]);
+ $offset['minute'] = intval($timeParts[3]);
+ if (isset($timeParts[4])) {
+ $offset['second'] = intval($timeParts[4]);
+ }
+ return $offset;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Export a UTC Offset field.
+ */
+ function _exportUtcOffset($value)
+ {
+ $offset = $value['ahead'] ? '+' : '-';
+ $offset .= sprintf('%02d%02d',
+ $value['hour'], $value['minute']);
+ if (isset($value['second'])) {
+ $offset .= sprintf('%02d', $value['second']);
+ }
+
+ return $offset;
+ }
+
+ /**
+ * Parse a Time Period field.
+ */
+ function _parsePeriod($text)
+ {
+ $periodParts = explode('/', $text);
+
+ $start = $this->_parseDateTime($periodParts[0]);
+
+ if ($duration = $this->_parseDuration($periodParts[1])) {
+ return array('start' => $start, 'duration' => $duration);
+ } elseif ($end = $this->_parseDateTime($periodParts[1])) {
+ return array('start' => $start, 'end' => $end);
+ }
+ }
+
+ /**
+ * Export a Time Period field.
+ */
+ function _exportPeriod($value)
+ {
+ $period = $this->_exportDateTime($value['start']);
+ $period .= '/';
+ if (isset($value['duration'])) {
+ $period .= $this->_exportDuration($value['duration']);
+ } else {
+ $period .= $this->_exportDateTime($value['end']);
+ }
+ return $period;
+ }
+
+ /**
+ * Grok the TZID and return an offset in seconds from UTC for this
+ * date and time.
+ */
+ function _parseTZID($date, $time, $tzid)
+ {
+ $vtimezone = $this->_container->findComponentByAttribute('vtimezone', 'TZID', $tzid);
+ if (!$vtimezone) {
+ return false;
+ }
+
+ $change_times = array();
+ foreach ($vtimezone->getComponents() as $o) {
+ $t = $vtimezone->parseChild($o, $date['year']);
+ if ($t !== false) {
+ $change_times[] = $t;
+ }
+ }
+
+ if (!$change_times) {
+ return false;
+ }
+
+ sort($change_times);
+
+ // Time is arbitrarily based on UTC for comparison.
+ $t = @gmmktime($time['hour'], $time['minute'], $time['second'],
+ $date['month'], $date['mday'], $date['year']);
+
+ if ($t < $change_times[0]['time']) {
+ return $change_times[0]['from'];
+ }
+
+ for ($i = 0, $n = count($change_times); $i < $n - 1; $i++) {
+ if (($t >= $change_times[$i]['time']) &&
+ ($t < $change_times[$i + 1]['time'])) {
+ return $change_times[$i]['to'];
+ }
+ }
+
+ if ($t >= $change_times[$n - 1]['time']) {
+ return $change_times[$n - 1]['to'];
+ }
+
+ return false;
+ }
+
+ /**
+ * Parses a DateTime field and returns a unix timestamp. If the
+ * field cannot be parsed then the original text is returned
+ * unmodified.
+ *
+ * @todo This function should be moved to Horde_Date and made public.
+ */
+ function _parseDateTime($text, $tzid = false)
+ {
+ $dateParts = explode('T', $text);
+ if (count($dateParts) != 2 && !empty($text)) {
+ // Not a datetime field but may be just a date field.
+ if (!preg_match('/^(\d{4})-?(\d{2})-?(\d{2})$/', $text, $match)) {
+ // Or not
+ return $text;
+ }
+ $newtext = $text.'T000000';
+ $dateParts = explode('T', $newtext);
+ }
+
+ if (!$date = Horde_iCalendar::_parseDate($dateParts[0])) {
+ return $text;
+ }
+ if (!$time = Horde_iCalendar::_parseTime($dateParts[1])) {
+ return $text;
+ }
+
+ // Get timezone info for date fields from $tzid and container.
+ $tzoffset = ($time['zone'] == 'Local' && $tzid && is_a($this->_container, 'Horde_iCalendar'))
+ ? $this->_parseTZID($date, $time, $tzid) : false;
+ if ($time['zone'] == 'UTC' || $tzoffset !== false) {
+ $result = @gmmktime($time['hour'], $time['minute'], $time['second'],
+ $date['month'], $date['mday'], $date['year']);
+ if ($tzoffset) {
+ $result -= $tzoffset;
+ }
+ } else {
+ // We don't know the timezone so assume local timezone.
+ // FIXME: shouldn't this be based on the user's timezone
+ // preference rather than the server's timezone?
+ $result = @mktime($time['hour'], $time['minute'], $time['second'],
+ $date['month'], $date['mday'], $date['year']);
+ }
+
+ return ($result !== false) ? $result : $text;
+ }
+
+ /**
+ * Export a DateTime field.
+ */
+ function _exportDateTime($value)
+ {
+ $temp = array();
+ if (!is_object($value) && !is_array($value)) {
+ $tz = date('O', $value);
+ $TZOffset = (3600 * substr($tz, 0, 3)) + (60 * substr($tz, 3, 2));
+ $value -= $TZOffset;
+
+ $temp['zone'] = 'UTC';
+ list($temp['year'], $temp['month'], $temp['mday'], $temp['hour'], $temp['minute'], $temp['second']) = explode('-', date('Y-n-j-G-i-s', $value));
+ } else {
+ $dateOb = new Horde_Date($value);
+ return Horde_iCalendar::_exportDateTime($dateOb->timestamp());
+ }
+
+ return Horde_iCalendar::_exportDate($temp) . 'T' . Horde_iCalendar::_exportTime($temp);
+ }
+
+ /**
+ * Parses a Time field.
+ *
+ * @static
+ */
+ function _parseTime($text)
+ {
+ if (preg_match('/([0-9]{2})([0-9]{2})([0-9]{2})(Z)?/', $text, $timeParts)) {
+ $time['hour'] = intval($timeParts[1]);
+ $time['minute'] = intval($timeParts[2]);
+ $time['second'] = intval($timeParts[3]);
+ if (isset($timeParts[4])) {
+ $time['zone'] = 'UTC';
+ } else {
+ $time['zone'] = 'Local';
+ }
+ return $time;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Exports a Time field.
+ */
+ function _exportTime($value)
+ {
+ $time = sprintf('%02d%02d%02d',
+ $value['hour'], $value['minute'], $value['second']);
+ if ($value['zone'] == 'UTC') {
+ $time .= 'Z';
+ }
+ return $time;
+ }
+
+ /**
+ * Parses a Date field.
+ *
+ * @static
+ */
+ function _parseDate($text)
+ {
+ $parts = explode('T', $text);
+ if (count($parts) == 2) {
+ $text = $parts[0];
+ }
+
+ if (!preg_match('/^(\d{4})-?(\d{2})-?(\d{2})$/', $text, $match)) {
+ return false;
+ }
+
+ return array('year' => $match[1],
+ 'month' => $match[2],
+ 'mday' => $match[3]);
+ }
+
+ /**
+ * Exports a date field.
+ *
+ * @param object|array $value Date object or hash.
+ * @param string $autoconvert If set, use this as time part to export the
+ * date as datetime when exporting to Vcalendar
+ * 1.0. Examples: '000000' or '235959'
+ */
+ function _exportDate($value, $autoconvert = false)
+ {
+ if (is_object($value)) {
+ $value = array('year' => $value->year, 'month' => $value->month, 'mday' => $value->mday);
+ }
+ if ($autoconvert !== false && $this->isOldFormat()) {
+ return sprintf('%04d%02d%02dT%s', $value['year'], $value['month'], $value['mday'], $autoconvert);
+ } else {
+ return sprintf('%04d%02d%02d', $value['year'], $value['month'], $value['mday']);
+ }
+ }
+
+ /**
+ * Parse a Duration Value field.
+ */
+ function _parseDuration($text)
+ {
+ if (preg_match('/([+]?|[-])P(([0-9]+W)|([0-9]+D)|)(T(([0-9]+H)|([0-9]+M)|([0-9]+S))+)?/', trim($text), $durvalue)) {
+ // Weeks.
+ $duration = 7 * 86400 * intval($durvalue[3]);
+
+ if (count($durvalue) > 4) {
+ // Days.
+ $duration += 86400 * intval($durvalue[4]);
+ }
+ if (count($durvalue) > 5) {
+ // Hours.
+ $duration += 3600 * intval($durvalue[7]);
+
+ // Mins.
+ if (isset($durvalue[8])) {
+ $duration += 60 * intval($durvalue[8]);
+ }
+
+ // Secs.
+ if (isset($durvalue[9])) {
+ $duration += intval($durvalue[9]);
+ }
+ }
+
+ // Sign.
+ if ($durvalue[1] == "-") {
+ $duration *= -1;
+ }
+
+ return $duration;
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Export a duration value.
+ */
+ function _exportDuration($value)
+ {
+ $duration = '';
+ if ($value < 0) {
+ $value *= -1;
+ $duration .= '-';
+ }
+ $duration .= 'P';
+
+ $weeks = floor($value / (7 * 86400));
+ $value = $value % (7 * 86400);
+ if ($weeks) {
+ $duration .= $weeks . 'W';
+ }
+
+ $days = floor($value / (86400));
+ $value = $value % (86400);
+ if ($days) {
+ $duration .= $days . 'D';
+ }
+
+ if ($value) {
+ $duration .= 'T';
+
+ $hours = floor($value / 3600);
+ $value = $value % 3600;
+ if ($hours) {
+ $duration .= $hours . 'H';
+ }
+
+ $mins = floor($value / 60);
+ $value = $value % 60;
+ if ($mins) {
+ $duration .= $mins . 'M';
+ }
+
+ if ($value) {
+ $duration .= $value . 'S';
+ }
+ }
+
+ return $duration;
+ }
+
+ /**
+ * Converts an 8bit string to a quoted-printable string according to RFC
+ * 2045, section 6.7.
+ *
+ * imap_8bit() does not apply all necessary rules.
+ *
+ * @param string $input The string to be encoded.
+ *
+ * @return string The quoted-printable encoded string.
+ */
+ function _quotedPrintableEncode($input = '')
+ {
+ $output = $line = '';
+ $len = strlen($input);
+
+ for ($i = 0; $i < $len; ++$i) {
+ $ord = ord($input[$i]);
+ // Encode non-printable characters (rule 2).
+ if ($ord == 9 ||
+ ($ord >= 32 && $ord <= 60) ||
+ ($ord >= 62 && $ord <= 126)) {
+ $chunk = $input[$i];
+ } else {
+ // Quoted printable encoding (rule 1).
+ $chunk = '=' . String::upper(sprintf('%02X', $ord));
+ }
+ $line .= $chunk;
+ // Wrap long lines (rule 5)
+ if (strlen($line) + 1 > 76) {
+ $line = String::wordwrap($line, 75, "=\r\n", true, 'us-ascii', true);
+ $newline = strrchr($line, "\r\n");
+ if ($newline !== false) {
+ $output .= substr($line, 0, -strlen($newline) + 2);
+ $line = substr($newline, 2);
+ } else {
+ $output .= $line;
+ }
+ continue;
+ }
+ // Wrap at line breaks for better readability (rule 4).
+ if (substr($line, -3) == '=0A') {
+ $output .= $line . "=\r\n";
+ $line = '';
+ }
+ }
+ $output .= $line;
+
+ // Trailing whitespace must be encoded (rule 3).
+ $lastpos = strlen($output) - 1;
+ if ($output[$lastpos] == chr(9) ||
+ $output[$lastpos] == chr(32)) {
+ $output[$lastpos] = '=';
+ $output .= String::upper(sprintf('%02X', ord($output[$lastpos])));
+ }
+
+ return $output;
+ }
+
+}
+
+
+
+/**
+ * Class representing vAlarms.
+ *
+ * $Horde: framework/iCalendar/iCalendar/valarm.php,v 1.8.10.9 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author Mike Cochrane <mike@graftonhall.co.nz>
+ * @since Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_valarm extends Horde_iCalendar {
+
+ function getType()
+ {
+ return 'vAlarm';
+ }
+
+ function exportvCalendar()
+ {
+ return parent::_exportvData('VALARM');
+ }
+
+}
+
+/**
+ * Class representing vEvents.
+ *
+ * $Horde: framework/iCalendar/iCalendar/vevent.php,v 1.31.10.16 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author Mike Cochrane <mike@graftonhall.co.nz>
+ * @since Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_vevent extends Horde_iCalendar {
+
+ function getType()
+ {
+ return 'vEvent';
+ }
+
+ function exportvCalendar()
+ {
+ // Default values.
+ $requiredAttributes = array();
+ $requiredAttributes['DTSTAMP'] = time();
+ $requiredAttributes['UID'] = $this->_exportDateTime(time())
+ . substr(str_pad(base_convert(microtime(), 10, 36), 16, uniqid(mt_rand()), STR_PAD_LEFT), -16)
+ . '@' . (isset($_SERVER['SERVER_NAME']) ? $_SERVER['SERVER_NAME'] : 'localhost');
+
+ $method = !empty($this->_container) ?
+ $this->_container->getAttribute('METHOD') : 'PUBLISH';
+
+ switch ($method) {
+ case 'PUBLISH':
+ $requiredAttributes['DTSTART'] = time();
+ $requiredAttributes['SUMMARY'] = '';
+ break;
+
+ case 'REQUEST':
+ $requiredAttributes['ATTENDEE'] = '';
+ $requiredAttributes['DTSTART'] = time();
+ $requiredAttributes['SUMMARY'] = '';
+ break;
+
+ case 'REPLY':
+ $requiredAttributes['ATTENDEE'] = '';
+ break;
+
+ case 'ADD':
+ $requiredAttributes['DTSTART'] = time();
+ $requiredAttributes['SEQUENCE'] = 1;
+ $requiredAttributes['SUMMARY'] = '';
+ break;
+
+ case 'CANCEL':
+ $requiredAttributes['ATTENDEE'] = '';
+ $requiredAttributes['SEQUENCE'] = 1;
+ break;
+
+ case 'REFRESH':
+ $requiredAttributes['ATTENDEE'] = '';
+ break;
+ }
+
+ foreach ($requiredAttributes as $name => $default_value) {
+ if (is_a($this->getAttribute($name), 'PEAR_Error')) {
+ $this->setAttribute($name, $default_value);
+ }
+ }
+
+ return parent::_exportvData('VEVENT');
+ }
+
+ /**
+ * Update the status of an attendee of an event.
+ *
+ * @param $email The email address of the attendee.
+ * @param $status The participant status to set.
+ * @param $fullname The full name of the participant to set.
+ */
+ function updateAttendee($email, $status, $fullname = '')
+ {
+ foreach ($this->_attributes as $key => $attribute) {
+ if ($attribute['name'] == 'ATTENDEE' &&
+ $attribute['value'] == 'mailto:' . $email) {
+ $this->_attributes[$key]['params']['PARTSTAT'] = $status;
+ if (!empty($fullname)) {
+ $this->_attributes[$key]['params']['CN'] = $fullname;
+ }
+ unset($this->_attributes[$key]['params']['RSVP']);
+ return;
+ }
+ }
+ $params = array('PARTSTAT' => $status);
+ if (!empty($fullname)) {
+ $params['CN'] = $fullname;
+ }
+ $this->setAttribute('ATTENDEE', 'mailto:' . $email, $params);
+ }
+
+ /**
+ * Return the organizer display name or email.
+ *
+ * @return string The organizer name to display for this event.
+ */
+ function organizerName()
+ {
+ $organizer = $this->getAttribute('ORGANIZER', true);
+ if (is_a($organizer, 'PEAR_Error')) {
+ return _("An unknown person");
+ }
+
+ if (isset($organizer[0]['CN'])) {
+ return $organizer[0]['CN'];
+ }
+
+ $organizer = parse_url($this->getAttribute('ORGANIZER'));
+
+ return $organizer['path'];
+ }
+
+ /**
+ * Update this event with details from another event.
+ *
+ * @param Horde_iCalendar_vEvent $vevent The vEvent with latest details.
+ */
+ function updateFromvEvent($vevent)
+ {
+ $newAttributes = $vevent->getAllAttributes();
+ foreach ($newAttributes as $newAttribute) {
+ $currentValue = $this->getAttribute($newAttribute['name']);
+ if (is_a($currentValue, 'PEAR_error')) {
+ // Already exists so just add it.
+ $this->setAttribute($newAttribute['name'],
+ $newAttribute['value'],
+ $newAttribute['params']);
+ } else {
+ // Already exists so locate and modify.
+ $found = false;
+
+ // Try matching the attribte name and value incase
+ // only the params changed (eg attendee updating
+ // status).
+ foreach ($this->_attributes as $id => $attr) {
+ if ($attr['name'] == $newAttribute['name'] &&
+ $attr['value'] == $newAttribute['value']) {
+ // merge the params
+ foreach ($newAttribute['params'] as $param_id => $param_name) {
+ $this->_attributes[$id]['params'][$param_id] = $param_name;
+ }
+ $found = true;
+ break;
+ }
+ }
+ if (!$found) {
+ // Else match the first attribute with the same
+ // name (eg changing start time).
+ foreach ($this->_attributes as $id => $attr) {
+ if ($attr['name'] == $newAttribute['name']) {
+ $this->_attributes[$id]['value'] = $newAttribute['value'];
+ // Merge the params.
+ foreach ($newAttribute['params'] as $param_id => $param_name) {
+ $this->_attributes[$id]['params'][$param_id] = $param_name;
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ /**
+ * Update just the attendess of event with details from another
+ * event.
+ *
+ * @param Horde_iCalendar_vEvent $vevent The vEvent with latest details
+ */
+ function updateAttendeesFromvEvent($vevent)
+ {
+ $newAttributes = $vevent->getAllAttributes();
+ foreach ($newAttributes as $newAttribute) {
+ if ($newAttribute['name'] != 'ATTENDEE') {
+ continue;
+ }
+ $currentValue = $this->getAttribute($newAttribute['name']);
+ if (is_a($currentValue, 'PEAR_error')) {
+ // Already exists so just add it.
+ $this->setAttribute($newAttribute['name'],
+ $newAttribute['value'],
+ $newAttribute['params']);
+ } else {
+ // Already exists so locate and modify.
+ $found = false;
+ // Try matching the attribte name and value incase
+ // only the params changed (eg attendee updating
+ // status).
+ foreach ($this->_attributes as $id => $attr) {
+ if ($attr['name'] == $newAttribute['name'] &&
+ $attr['value'] == $newAttribute['value']) {
+ // Merge the params.
+ foreach ($newAttribute['params'] as $param_id => $param_name) {
+ $this->_attributes[$id]['params'][$param_id] = $param_name;
+ }
+ $found = true;
+ break;
+ }
+ }
+
+ if (!$found) {
+ // Else match the first attribute with the same
+ // name (eg changing start time).
+ foreach ($this->_attributes as $id => $attr) {
+ if ($attr['name'] == $newAttribute['name']) {
+ $this->_attributes[$id]['value'] = $newAttribute['value'];
+ // Merge the params.
+ foreach ($newAttribute['params'] as $param_id => $param_name) {
+ $this->_attributes[$id]['params'][$param_id] = $param_name;
+ }
+ break;
+ }
+ }
+ }
+ }
+ }
+ }
+
+}
+
+/**
+ * Class representing vFreebusy components.
+ *
+ * $Horde: framework/iCalendar/iCalendar/vfreebusy.php,v 1.16.10.18 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @todo Don't use timestamps
+ *
+ * @author Mike Cochrane <mike@graftonhall.co.nz>
+ * @since Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_vfreebusy extends Horde_iCalendar {
+
+ var $_busyPeriods = array();
+ var $_extraParams = array();
+
+ /**
+ * Returns the type of this calendar component.
+ *
+ * @return string The type of this component.
+ */
+ function getType()
+ {
+ return 'vFreebusy';
+ }
+
+ /**
+ * Parses a string containing vFreebusy data.
+ *
+ * @param string $data The data to parse.
+ */
+ function parsevCalendar($data, $type = null, $charset = null)
+ {
+ parent::parsevCalendar($data, 'VFREEBUSY', $charset);
+
+ // Do something with all the busy periods.
+ foreach ($this->_attributes as $key => $attribute) {
+ if ($attribute['name'] != 'FREEBUSY') {
+ continue;
+ }
+ foreach ($attribute['values'] as $value) {
+ $params = isset($attribute['params'])
+ ? $attribute['params']
+ : array();
+ if (isset($value['duration'])) {
+ $this->addBusyPeriod('BUSY', $value['start'], null,
+ $value['duration'], $params);
+ } else {
+ $this->addBusyPeriod('BUSY', $value['start'],
+ $value['end'], null, $params);
+ }
+ }
+ unset($this->_attributes[$key]);
+ }
+ }
+
+ /**
+ * Returns the component exported as string.
+ *
+ * @return string The exported vFreeBusy information according to the
+ * iCalender format specification.
+ */
+ function exportvCalendar()
+ {
+ foreach ($this->_busyPeriods as $start => $end) {
+ $periods = array(array('start' => $start, 'end' => $end));
+ $this->setAttribute('FREEBUSY', $periods,
+ isset($this->_extraParams[$start])
+ ? $this->_extraParams[$start] : array());
+ }
+
+ $res = parent::_exportvData('VFREEBUSY');
+
+ foreach ($this->_attributes as $key => $attribute) {
+ if ($attribute['name'] == 'FREEBUSY') {
+ unset($this->_attributes[$key]);
+ }
+ }
+
+ return $res;
+ }
+
+ /**
+ * Returns a display name for this object.
+ *
+ * @return string A clear text name for displaying this object.
+ */
+ function getName()
+ {
+ $name = '';
+ $method = !empty($this->_container) ?
+ $this->_container->getAttribute('METHOD') : 'PUBLISH';
+
+ if (is_a($method, 'PEAR_Error') || $method == 'PUBLISH') {
+ $attr = 'ORGANIZER';
+ } elseif ($method == 'REPLY') {
+ $attr = 'ATTENDEE';
+ }
+
+ $name = $this->getAttribute($attr, true);
+ if (!is_a($name, 'PEAR_Error') && isset($name[0]['CN'])) {
+ return $name[0]['CN'];
+ }
+
+ $name = $this->getAttribute($attr);
+ if (is_a($name, 'PEAR_Error')) {
+ return '';
+ } else {
+ $name = parse_url($name);
+ return $name['path'];
+ }
+ }
+
+ /**
+ * Returns the email address for this object.
+ *
+ * @return string The email address of this object's owner.
+ */
+ function getEmail()
+ {
+ $name = '';
+ $method = !empty($this->_container)
+ ? $this->_container->getAttribute('METHOD') : 'PUBLISH';
+
+ if (is_a($method, 'PEAR_Error') || $method == 'PUBLISH') {
+ $attr = 'ORGANIZER';
+ } elseif ($method == 'REPLY') {
+ $attr = 'ATTENDEE';
+ }
+
+ $name = $this->getAttribute($attr);
+ if (is_a($name, 'PEAR_Error')) {
+ return '';
+ } else {
+ $name = parse_url($name);
+ return $name['path'];
+ }
+ }
+
+ /**
+ * Returns the busy periods.
+ *
+ * @return array All busy periods.
+ */
+ function getBusyPeriods()
+ {
+ return $this->_busyPeriods;
+ }
+
+ /**
+ * Returns any additional freebusy parameters.
+ *
+ * @return array Additional parameters of the freebusy periods.
+ */
+ function getExtraParams()
+ {
+ return $this->_extraParams;
+ }
+
+ /**
+ * Returns all the free periods of time in a given period.
+ *
+ * @param integer $startStamp The start timestamp.
+ * @param integer $endStamp The end timestamp.
+ *
+ * @return array A hash with free time periods, the start times as the
+ * keys and the end times as the values.
+ */
+ function getFreePeriods($startStamp, $endStamp)
+ {
+ $this->simplify();
+ $periods = array();
+
+ // Check that we have data for some part of this period.
+ if ($this->getEnd() < $startStamp || $this->getStart() > $endStamp) {
+ return $periods;
+ }
+
+ // Locate the first time in the requested period we have data for.
+ $nextstart = max($startStamp, $this->getStart());
+
+ // Check each busy period and add free periods in between.
+ foreach ($this->_busyPeriods as $start => $end) {
+ if ($start <= $endStamp && $end >= $nextstart) {
+ if ($nextstart <= $start) {
+ $periods[$nextstart] = min($start, $endStamp);
+ }
+ $nextstart = min($end, $endStamp);
+ }
+ }
+
+ // If we didn't read the end of the requested period but still have
+ // data then mark as free to the end of the period or available data.
+ if ($nextstart < $endStamp && $nextstart < $this->getEnd()) {
+ $periods[$nextstart] = min($this->getEnd(), $endStamp);
+ }
+
+ return $periods;
+ }
+
+ /**
+ * Adds a busy period to the info.
+ *
+ * This function may throw away data in case you add a period with a start
+ * date that already exists. The longer of the two periods will be chosen
+ * (and all information associated with the shorter one will be removed).
+ *
+ * @param string $type The type of the period. Either 'FREE' or
+ * 'BUSY'; only 'BUSY' supported at the moment.
+ * @param integer $start The start timestamp of the period.
+ * @param integer $end The end timestamp of the period.
+ * @param integer $duration The duration of the period. If specified, the
+ * $end parameter will be ignored.
+ * @param array $extra Additional parameters for this busy period.
+ */
+ function addBusyPeriod($type, $start, $end = null, $duration = null,
+ $extra = array())
+ {
+ if ($type == 'FREE') {
+ // Make sure this period is not marked as busy.
+ return false;
+ }
+
+ // Calculate the end time if duration was specified.
+ $tempEnd = is_null($duration) ? $end : $start + $duration;
+
+ // Make sure the period length is always positive.
+ $end = max($start, $tempEnd);
+ $start = min($start, $tempEnd);
+
+ if (isset($this->_busyPeriods[$start])) {
+ // Already a period starting at this time. Change the current
+ // period only if the new one is longer. This might be a problem
+ // if the callee assumes that there is no simplification going
+ // on. But since the periods are stored using the start time of
+ // the busy periods we have to throw away data here.
+ if ($end > $this->_busyPeriods[$start]) {
+ $this->_busyPeriods[$start] = $end;
+ $this->_extraParams[$start] = $extra;
+ }
+ } else {
+ // Add a new busy period.
+ $this->_busyPeriods[$start] = $end;
+ $this->_extraParams[$start] = $extra;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns the timestamp of the start of the time period this free busy
+ * information covers.
+ *
+ * @return integer A timestamp.
+ */
+ function getStart()
+ {
+ if (!is_a($this->getAttribute('DTSTART'), 'PEAR_Error')) {
+ return $this->getAttribute('DTSTART');
+ } elseif (count($this->_busyPeriods)) {
+ return min(array_keys($this->_busyPeriods));
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Returns the timestamp of the end of the time period this free busy
+ * information covers.
+ *
+ * @return integer A timestamp.
+ */
+ function getEnd()
+ {
+ if (!is_a($this->getAttribute('DTEND'), 'PEAR_Error')) {
+ return $this->getAttribute('DTEND');
+ } elseif (count($this->_busyPeriods)) {
+ return max(array_values($this->_busyPeriods));
+ } else {
+ return false;
+ }
+ }
+
+ /**
+ * Merges the busy periods of another Horde_iCalendar_vfreebusy object
+ * into this one.
+ *
+ * This might lead to simplification no matter what you specify for the
+ * "simplify" flag since periods with the same start date will lead to the
+ * shorter period being removed (see addBusyPeriod).
+ *
+ * @param Horde_iCalendar_vfreebusy $freebusy A freebusy object.
+ * @param boolean $simplify If true, simplify() will
+ * called after the merge.
+ */
+ function merge($freebusy, $simplify = true)
+ {
+ if (!is_a($freebusy, 'Horde_iCalendar_vfreebusy')) {
+ return false;
+ }
+
+ $extra = $freebusy->getExtraParams();
+ foreach ($freebusy->getBusyPeriods() as $start => $end) {
+ // This might simplify the busy periods without taking the
+ // "simplify" flag into account.
+ $this->addBusyPeriod('BUSY', $start, $end, null,
+ isset($extra[$start])
+ ? $extra[$start] : array());
+ }
+
+ $thisattr = $this->getAttribute('DTSTART');
+ $thatattr = $freebusy->getAttribute('DTSTART');
+ if (is_a($thisattr, 'PEAR_Error') && !is_a($thatattr, 'PEAR_Error')) {
+ $this->setAttribute('DTSTART', $thatattr, array(), false);
+ } elseif (!is_a($thatattr, 'PEAR_Error')) {
+ if ($thatattr < $thisattr) {
+ $this->setAttribute('DTSTART', $thatattr, array(), false);
+ }
+ }
+
+ $thisattr = $this->getAttribute('DTEND');
+ $thatattr = $freebusy->getAttribute('DTEND');
+ if (is_a($thisattr, 'PEAR_Error') && !is_a($thatattr, 'PEAR_Error')) {
+ $this->setAttribute('DTEND', $thatattr, array(), false);
+ } elseif (!is_a($thatattr, 'PEAR_Error')) {
+ if ($thatattr > $thisattr) {
+ $this->setAttribute('DTEND', $thatattr, array(), false);
+ }
+ }
+
+ if ($simplify) {
+ $this->simplify();
+ }
+
+ return true;
+ }
+
+ /**
+ * Removes all overlaps and simplifies the busy periods array as much as
+ * possible.
+ */
+ function simplify()
+ {
+ $clean = false;
+ $busy = array($this->_busyPeriods, $this->_extraParams);
+ while (!$clean) {
+ $result = $this->_simplify($busy[0], $busy[1]);
+ $clean = $result === $busy;
+ $busy = $result;
+ }
+
+ ksort($result[1], SORT_NUMERIC);
+ $this->_extraParams = $result[1];
+
+ ksort($result[0], SORT_NUMERIC);
+ $this->_busyPeriods = $result[0];
+ }
+
+ function _simplify($busyPeriods, $extraParams = array())
+ {
+ $checked = array();
+ $checkedExtra = array();
+ $checkedEmpty = true;
+
+ foreach ($busyPeriods as $start => $end) {
+ if ($checkedEmpty) {
+ $checked[$start] = $end;
+ $checkedExtra[$start] = isset($extraParams[$start])
+ ? $extraParams[$start] : array();
+ $checkedEmpty = false;
+ } else {
+ $added = false;
+ foreach ($checked as $testStart => $testEnd) {
+ // Replace old period if the new period lies around the
+ // old period.
+ if ($start <= $testStart && $end >= $testEnd) {
+ // Remove old period entry.
+ unset($checked[$testStart]);
+ unset($checkedExtra[$testStart]);
+ // Add replacing entry.
+ $checked[$start] = $end;
+ $checkedExtra[$start] = isset($extraParams[$start])
+ ? $extraParams[$start] : array();
+ $added = true;
+ } elseif ($start >= $testStart && $end <= $testEnd) {
+ // The new period lies fully within the old
+ // period. Just forget about it.
+ $added = true;
+ } elseif (($end <= $testEnd && $end >= $testStart) ||
+ ($start >= $testStart && $start <= $testEnd)) {
+ // Now we are in trouble: Overlapping time periods. If
+ // we allow for additional parameters we cannot simply
+ // choose one of the two parameter sets. It's better
+ // to leave two separated time periods.
+ $extra = isset($extraParams[$start])
+ ? $extraParams[$start] : array();
+ $testExtra = isset($checkedExtra[$testStart])
+ ? $checkedExtra[$testStart] : array();
+ // Remove old period entry.
+ unset($checked[$testStart]);
+ unset($checkedExtra[$testStart]);
+ // We have two periods overlapping. Are their
+ // additional parameters the same or different?
+ $newStart = min($start, $testStart);
+ $newEnd = max($end, $testEnd);
+ if ($extra === $testExtra) {
+ // Both periods have the same information. So we
+ // can just merge.
+ $checked[$newStart] = $newEnd;
+ $checkedExtra[$newStart] = $extra;
+ } else {
+ // Extra parameters are different. Create one
+ // period at the beginning with the params of the
+ // first period and create a trailing period with
+ // the params of the second period. The break
+ // point will be the end of the first period.
+ $break = min($end, $testEnd);
+ $checked[$newStart] = $break;
+ $checkedExtra[$newStart] =
+ isset($extraParams[$newStart])
+ ? $extraParams[$newStart] : array();
+ $checked[$break] = $newEnd;
+ $highStart = max($start, $testStart);
+ $checkedExtra[$break] =
+ isset($extraParams[$highStart])
+ ? $extraParams[$highStart] : array();
+
+ // Ensure we also have the extra data in the
+ // extraParams.
+ $extraParams[$break] =
+ isset($extraParams[$highStart])
+ ? $extraParams[$highStart] : array();
+ }
+ $added = true;
+ }
+
+ if ($added) {
+ break;
+ }
+ }
+
+ if (!$added) {
+ $checked[$start] = $end;
+ $checkedExtra[$start] = isset($extraParams[$start])
+ ? $extraParams[$start] : array();
+ }
+ }
+ }
+
+ return array($checked, $checkedExtra);
+ }
+
+}
+
+/**
+ * Class representing vJournals.
+ *
+ * $Horde: framework/iCalendar/iCalendar/vjournal.php,v 1.8.10.9 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author Mike Cochrane <mike@graftonhall.co.nz>
+ * @since Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_vjournal extends Horde_iCalendar {
+
+ function getType()
+ {
+ return 'vJournal';
+ }
+
+ function exportvCalendar()
+ {
+ return parent::_exportvData('VJOURNAL');
+ }
+
+}
+
+
+
+
+/**
+ * Class representing vNotes.
+ *
+ * $Horde: framework/iCalendar/iCalendar/vnote.php,v 1.3.10.10 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author Mike Cochrane <mike@graftonhall.co.nz>
+ * @author Karsten Fourmont <fourmont@gmx.de>
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_vnote extends Horde_iCalendar {
+
+ function Horde_iCalendar_vnote($version = '1.1')
+ {
+ return parent::Horde_iCalendar($version);
+ }
+
+ function getType()
+ {
+ return 'vNote';
+ }
+
+ /**
+ * Unlike vevent and vtodo, a vnote is normally not enclosed in an
+ * iCalendar container. (BEGIN..END)
+ */
+ function exportvCalendar()
+ {
+ $requiredAttributes['BODY'] = '';
+ $requiredAttributes['VERSION'] = '1.1';
+
+ foreach ($requiredAttributes as $name => $default_value) {
+ if (is_a($this->getattribute($name), 'PEAR_Error')) {
+ $this->setAttribute($name, $default_value);
+ }
+ }
+
+ return $this->_exportvData('VNOTE');
+ }
+
+}
+
+/**
+ * Class representing vTimezones.
+ *
+ * $Horde: framework/iCalendar/iCalendar/vtimezone.php,v 1.8.10.10 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author Mike Cochrane <mike@graftonhall.co.nz>
+ * @since Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_vtimezone extends Horde_iCalendar {
+
+ function getType()
+ {
+ return 'vTimeZone';
+ }
+
+ function exportvCalendar()
+ {
+ return parent::_exportvData('VTIMEZONE');
+ }
+
+ /**
+ * Parse child components of the vTimezone component. Returns an
+ * array with the exact time of the time change as well as the
+ * 'from' and 'to' offsets around the change. Time is arbitrarily
+ * based on UTC for comparison.
+ */
+ function parseChild(&$child, $year)
+ {
+ // Make sure 'time' key is first for sort().
+ $result['time'] = 0;
+
+ $t = $child->getAttribute('TZOFFSETFROM');
+ if (is_a($t, 'PEAR_Error')) {
+ return false;
+ }
+ $result['from'] = ($t['hour'] * 60 * 60 + $t['minute'] * 60) * ($t['ahead'] ? 1 : -1);
+
+ $t = $child->getAttribute('TZOFFSETTO');
+ if (is_a($t, 'PEAR_Error')) {
+ return false;
+ }
+ $result['to'] = ($t['hour'] * 60 * 60 + $t['minute'] * 60) * ($t['ahead'] ? 1 : -1);
+
+ $switch_time = $child->getAttribute('DTSTART');
+ if (is_a($switch_time, 'PEAR_Error')) {
+ return false;
+ }
+
+ $rrules = $child->getAttribute('RRULE');
+ if (is_a($rrules, 'PEAR_Error')) {
+ if (!is_int($switch_time)) {
+ return false;
+ }
+ // Convert this timestamp from local time to UTC for
+ // comparison (All dates are compared as if they are UTC).
+ $t = getdate($switch_time);
+ $result['time'] = @gmmktime($t['hours'], $t['minutes'], $t['seconds'],
+ $t['mon'], $t['mday'], $t['year']);
+ return $result;
+ }
+
+ $rrules = explode(';', $rrules);
+ foreach ($rrules as $rrule) {
+ $t = explode('=', $rrule);
+ switch ($t[0]) {
+ case 'FREQ':
+ if ($t[1] != 'YEARLY') {
+ return false;
+ }
+ break;
+
+ case 'INTERVAL':
+ if ($t[1] != '1') {
+ return false;
+ }
+ break;
+
+ case 'BYMONTH':
+ $month = intval($t[1]);
+ break;
+
+ case 'BYDAY':
+ $len = strspn($t[1], '1234567890-+');
+ if ($len == 0) {
+ return false;
+ }
+ $weekday = substr($t[1], $len);
+ $weekdays = array(
+ 'SU' => 0,
+ 'MO' => 1,
+ 'TU' => 2,
+ 'WE' => 3,
+ 'TH' => 4,
+ 'FR' => 5,
+ 'SA' => 6
+ );
+ $weekday = $weekdays[$weekday];
+ $which = intval(substr($t[1], 0, $len));
+ break;
+
+ case 'UNTIL':
+ if (intval($year) > intval(substr($t[1], 0, 4))) {
+ return false;
+ }
+ break;
+ }
+ }
+
+ if (empty($month) || !isset($weekday)) {
+ return false;
+ }
+
+ if (is_int($switch_time)) {
+ // Was stored as localtime.
+ $switch_time = strftime('%H:%M:%S', $switch_time);
+ $switch_time = explode(':', $switch_time);
+ } else {
+ $switch_time = explode('T', $switch_time);
+ if (count($switch_time) != 2) {
+ return false;
+ }
+ $switch_time[0] = substr($switch_time[1], 0, 2);
+ $switch_time[2] = substr($switch_time[1], 4, 2);
+ $switch_time[1] = substr($switch_time[1], 2, 2);
+ }
+
+ // Get the timestamp for the first day of $month.
+ $when = gmmktime($switch_time[0], $switch_time[1], $switch_time[2],
+ $month, 1, $year);
+ // Get the day of the week for the first day of $month.
+ $first_of_month_weekday = intval(gmstrftime('%w', $when));
+
+ // Go to the first $weekday before first day of $month.
+ if ($weekday >= $first_of_month_weekday) {
+ $weekday -= 7;
+ }
+ $when -= ($first_of_month_weekday - $weekday) * 60 * 60 * 24;
+
+ // If going backwards go to the first $weekday after last day
+ // of $month.
+ if ($which < 0) {
+ do {
+ $when += 60*60*24*7;
+ } while (intval(gmstrftime('%m', $when)) == $month);
+ }
+
+ // Calculate $weekday number $which.
+ $when += $which * 60 * 60 * 24 * 7;
+
+ $result['time'] = $when;
+
+ return $result;
+ }
+
+}
+
+/**
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_standard extends Horde_iCalendar {
+
+ function getType()
+ {
+ return 'standard';
+ }
+
+ function parsevCalendar($data)
+ {
+ parent::parsevCalendar($data, 'STANDARD');
+ }
+
+ function exportvCalendar()
+ {
+ return parent::_exportvData('STANDARD');
+ }
+
+}
+
+/**
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_daylight extends Horde_iCalendar {
+
+ function getType()
+ {
+ return 'daylight';
+ }
+
+ function parsevCalendar($data)
+ {
+ parent::parsevCalendar($data, 'DAYLIGHT');
+ }
+
+ function exportvCalendar()
+ {
+ return parent::_exportvData('DAYLIGHT');
+ }
+
+}
+
+/**
+ * Class representing vTodos.
+ *
+ * $Horde: framework/iCalendar/iCalendar/vtodo.php,v 1.13.10.9 2009-01-06 15:23:53 jan Exp $
+ *
+ * Copyright 2003-2009 The Horde Project (http://www.horde.org/)
+ *
+ * See the enclosed file COPYING for license information (LGPL). If you
+ * did not receive this file, see http://www.fsf.org/copyleft/lgpl.html.
+ *
+ * @author Mike Cochrane <mike@graftonhall.co.nz>
+ * @since Horde 3.0
+ * @package Horde_iCalendar
+ */
+class Horde_iCalendar_vtodo extends Horde_iCalendar {
+
+ function getType()
+ {
+ return 'vTodo';
+ }
+
+ function exportvCalendar()
+ {
+ return parent::_exportvData('VTODO');
+ }
+
+ /**
+ * Convert this todo to an array of attributes.
+ *
+ * @return array Array containing the details of the todo in a hash
+ * as used by Horde applications.
+ */
+ function toArray()
+ {
+ $todo = array();
+
+ $name = $this->getAttribute('SUMMARY');
+ if (!is_array($name) && !is_a($name, 'PEAR_Error')) {
+ $todo['name'] = $name;
+ }
+ $desc = $this->getAttribute('DESCRIPTION');
+ if (!is_array($desc) && !is_a($desc, 'PEAR_Error')) {
+ $todo['desc'] = $desc;
+ }
+
+ $priority = $this->getAttribute('PRIORITY');
+ if (!is_array($priority) && !is_a($priority, 'PEAR_Error')) {
+ $todo['priority'] = $priority;
+ }
+
+ $due = $this->getAttribute('DTSTAMP');
+ if (!is_array($due) && !is_a($due, 'PEAR_Error')) {
+ $todo['due'] = $due;
+ }
+
+ return $todo;
+ }
+
+ /**
+ * Set the attributes for this todo item from an array.
+ *
+ * @param array $todo Array containing the details of the todo in
+ * the same format that toArray() exports.
+ */
+ function fromArray($todo)
+ {
+ if (isset($todo['name'])) {
+ $this->setAttribute('SUMMARY', $todo['name']);
+ }
+ if (isset($todo['desc'])) {
+ $this->setAttribute('DESCRIPTION', $todo['desc']);
+ }
+
+ if (isset($todo['priority'])) {
+ $this->setAttribute('PRIORITY', $todo['priority']);
+ }
+
+ if (isset($todo['due'])) {
+ $this->setAttribute('DTSTAMP', $todo['due']);
+ }
+ }
+
+}
diff --git a/plugins/calendar/lib/calendar_ical.php b/plugins/calendar/lib/calendar_ical.php
index c99fab35..5aa51953 100644
--- a/plugins/calendar/lib/calendar_ical.php
+++ b/plugins/calendar/lib/calendar_ical.php
@@ -1,461 +1,464 @@
<?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;
public $method;
public $events = array();
function __construct($cal)
{
$this->cal = $cal;
$this->rc = $cal->rc;
}
/**
* 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';
+ @include_once('Horde/iCalendar.php');
+
+ if (!class_exists('Horde_iCalendar'))
+ require_once($this->cal->home . '/lib/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
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 6507b513..8008aaef 100644
--- a/plugins/calendar/lib/calendar_itip.php
+++ b/plugins/calendar/lib/calendar_itip.php
@@ -1,324 +1,325 @@
<?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_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
+ // delete old entry
+ $this->rc->db->query("DELETE FROM itipinvitations WHERE token=?", $base);
$query = $this->rc->db->query(
- "REPLACE INTO itipinvitations
+ "INSERT 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 35a5c9ce..ba8cc83d 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -1,820 +1,813 @@
<?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(
'command' => 'calendar',
'class' => 'button-calendar',
'classsel' => 'button-calendar button-selected',
'innerclass' => 'button-inner',
'label' => 'calendar.calendar',
), '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('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'));
+ $checkbox = new html_checkbox(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('label', html::label(null, $radio->show('BYMONTHDAY', array('value' => 'BYMONTHDAY')) . ' ' . $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'));
+ $checkbox = new html_checkbox(array('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')
+ $this->cal->gettext('fourth'),
+ $this->cal->gettext('last')
),
- 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);
+ array(1, 2, 3, 4, -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'))) . ']');
+ $table->add('header', Q($this->cal->attachment['name']));
+ $table->add('download-link', 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'])));
+ $table->add('header', 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/get_horde_icalendar.sh b/plugins/calendar/lib/get_horde_icalendar.sh
new file mode 100755
index 00000000..1992bf27
--- /dev/null
+++ b/plugins/calendar/lib/get_horde_icalendar.sh
@@ -0,0 +1,31 @@
+#!/bin/sh
+
+# Copy Horde_iCalendar classes and dependencies to stdout.
+# This will create a standalone copy of the classes requried for iCal parsing.
+
+SRCDIR=$1
+
+if [ ! -d "$SRCDIR" ]; then
+ echo "Usage: get_horde_icalendar.sh SRCDIR"
+ echo "Please enter a valid source directory of the Horde lib"
+ exit 1
+fi
+
+echo "<?php
+
+/**
+ * This is a concatenated copy of the following files:
+ * Horde/String.php, Horde/iCalendar.php, Horde/iCalendar/*.php
+ * Pull the latest version of these file from the PEAR channel of the Horde project at http://pear.horde.org
+ */
+
+require_once(dirname(__FILE__) . '/Horde_Date.php');"
+
+sed 's/<?php//; s/?>//' $SRCDIR/String.php
+echo "\n"
+sed 's/<?php//; s/?>//' $SRCDIR/iCalendar.php | sed -E "s/include_once.+//; s/NLS::getCharset\(\)/'UTF-8'/"
+echo "\n"
+
+for fn in `ls $SRCDIR/iCalendar/*.php | grep -v 'vcard.php'`; do
+ sed 's/<?php//; s/?>//' $fn | sed -E "s/(include|require)_once.+//"
+done;
diff --git a/plugins/calendar/package.xml b/plugins/calendar/package.xml
index ed42622a..12844305 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://git.kolab.org/roundcube-plugins-kolab/</uri>
+ <uri>http://git.kolab.org/roundcubemail-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/Horde_Date.php" role="php"></file>
+ <file name="lib/Horde_Date_Recurrence.php" role="php"></file>
+ <file name="lib/Horde_iCalendar.php" role="php"></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="README" 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/larry/calendar.css b/plugins/calendar/skins/larry/calendar.css
index 488f37ca..cba1149c 100644
--- a/plugins/calendar/skins/larry/calendar.css
+++ b/plugins/calendar/skins/larry/calendar.css
@@ -1,1332 +1,1336 @@
/**
* Roundcube Calendar plugin styles for skin "Larry"
*
* Copyright (c) 2012, The Roundcube Dev Team
* Screendesign by FLINT / Büro für Gestaltung, bueroflint.com
*
* The contents are subject to the Creative Commons Attribution-ShareAlike
* License. It is allowed to copy, distribute, transmit and to adapt the work
* by keeping credits to the original autors in the README file.
* See http://creativecommons.org/licenses/by-sa/3.0/ for details.
*
* $Id$
*/
body.calendarmain {
overflow: hidden;
}
body.calendarmain #mainscreen {
left: 0;
}
#calendarsidebar {
position: absolute;
top: 0;
left: 10px;
bottom: 0;
width: 240px;
}
#datepicker {
margin-top: 12px;
width: 100%;
min-height: 190px;
}
#datepicker .ui-datepicker {
width: 100% !important;
box-shadow: none;
-moz-box-shadow: none;
-webkit-box-shadow: none;
}
#datepicker .ui-datepicker td a {
padding: 5px 4px;
font-size: 12px;
}
#datepicker td.ui-datepicker-activerange {
border-color: #69a2b6;
}
#datepicker .ui-datepicker-activerange a {
color: #185d7a;
background: #d9f1fb;
background: -moz-linear-gradient(top, #d9f1fb 0%, #c5e3ee 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#d9f1fb), color-stop(100%,#c5e3ee));
background: -o-linear-gradient(top, #d9f1fb 0%, #c5e3ee 100%);
background: -ms-linear-gradient(top, #d9f1fb 0%, #c5e3ee 100%);
background: linear-gradient(top, #d9f1fb 0%, #c5e3ee 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#d9f1fb', endColorstr='#c5e3ee', GradientType=0);
}
#datepicker .ui-datepicker-activerange a.ui-state-active {
color: #fff;
background: #00acd4;
background: -moz-linear-gradient(top, #00acd4 0%, #008fc7 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#00acd4), color-stop(100%,#008fc7));
background: -o-linear-gradient(top, #00acd4 0%, #008fc7 100%);
background: -ms-linear-gradient(top, #00acd4 0%, #008fc7 100%);
background: linear-gradient(top, #00acd4 0%, #008fc7 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#00acd4', endColorstr='#008fc7', GradientType=0);
}
#datepicker td.ui-datepicker-week-col {
cursor: pointer;
}
#datepicker .ui-datepicker-title {
margin: 2px 2.3em 3px 2.3em;
}
#datepicker .ui-datepicker .ui-datepicker-prev,
#datepicker .ui-datepicker .ui-datepicker-next {
top: 4px;
}
#calendarsidebartoggle {
position: absolute;
left: 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;
}
#calendarsidebartoggle:hover {
background-color: #ddd;
}
#calendar {
position: absolute;
top: 0;
left: 266px;
right: 0;
bottom: 0;
padding-bottom: 28px;
}
.calendarmain #message.statusbar {
border: 1px solid #c3c3c3;
border-bottom-color: #ababab;
}
#print {
width: 680px;
}
pre {
font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
}
#calendars {
position: absolute;
top: 276px;
left: 0;
bottom: 0;
right: 0;
}
#calendarslist li {
margin: 0;
height: 20px;
padding: 6px 8px 2px;
display: block;
position: relative;
white-space: nowrap;
}
#calendarslist li label {
display: block;
}
#calendarslist li span.calname {
cursor: default;
background: url(images/calendars.png) 0 -2px no-repeat;
padding-left: 22px;
padding-bottom: 2px;
color: #004458;
}
#calendarslist li span.handle {
display: inline-block;
padding: 0;
border-radius: 7px;
margin-right: 6px;
width: 10px;
height: 10px;
font-size: 0.8em;
border: 1px solid rgba(0, 0, 0, 0.5);
-webkit-box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
-moz-box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
}
#calendarslist li input {
position: absolute;
top: 3px;
right: 5px;
}
#calendarslist li.selected {
background-color: #c7e3ef;
}
#calendarslist li.selected span.calname {
font-weight: bold;
}
#calendarslist li.readonly span.calname {
background-position: 0 -20px;
}
#calendarslist li.other span.calname {
background-position: 0 -38px;
}
#calendarslist li.other.readonly span.calname {
background-position: 0 -56px;
}
#calendarslist li.shared span.calname {
background-position: 0 -74px;
}
#calendarslist li.shared.readonly span.calname {
background-position: 0 -92px;
}
#calfeedurl {
width: 98%;
background: #fbfbfb;
padding: 4px;
margin-bottom: 1em;
resize: none;
}
#agendalist {
width: 100%;
margin: 0 auto;
margin-top: 60px;
border: 1px solid #C1DAD7;
display: none;
}
#agendalist table {
width: 100%;
}
#agendalist td,
#agendalist th {
border-right: 1px solid #C1DAD7;
border-bottom: 1px solid #C1DAD7;
background: #fff;
padding: 6px 6px 6px 12px;
}
#agendalist tr {
vertical-align: top;
}
#agendalist th {
font-weight: bold;
}
#calendartoolbar {
position: absolute;
top: -6px;
right: 0;
height: 40px;
z-index: 200;
}
#calendartoolbar a {
padding-right: 10px;
}
#quicksearchbar {
right: 4px;
}
body.calendarmain #searchmenulink {
width: 15px;
}
div.uidialog {
display: none;
}
#user {
position: absolute;
top: 10px;
right: 100px;
left: 100px;
text-align: center;
}
a.morelink {
font-size: 90%;
color: #0069a6;
text-decoration: none;
}
a.morelink:hover {
text-decoration: underline;
}
a.miniColors-trigger {
margin-top: -3px;
}
#attachmentcontainer {
position: absolute;
- top: 80px;
- left: 20px;
- right: 20px;
- bottom: 20px;
+ top: 60px;
+ left: 0px;
+ right: 0px;
+ bottom: 0px;
}
#attachmentframe {
width: 100%;
height: 100%;
- border: 1px solid #999999;
- background-color: #F9F9F9;
+ border: 0;
+ background-color: #fff;
+ border-radius: 4px;
}
#partheader {
- position: absolute;
- top: 20px;
- left: 220px;
- right: 20px;
- height: 40px;
+ position: relative;
+ padding: 3px 0;
+ background: #f9f9f9;
+ background: -moz-linear-gradient(top, #fff 0%, #e9e9e9 100%);
+ background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#fff), color-stop(100%,#e9e9e9));
+ background: -o-linear-gradient(top, #fff 0%, #e9e9e9 100%);
+ background: -ms-linear-gradient(top, #fff 0%, #e9e9e9 100%);
+ background: linear-gradient(top, #fff 0%, #e9e9e9 100%);
}
#partheader table td {
- padding-left: 2px;
- padding-right: 4px;
- vertical-align: middle;
- font-size: 11px;
+ color: #666;
+ padding: 2px 8px;
}
-#partheader table td.title {
- color: #666;
+#partheader table td.header {
font-weight: bold;
}
+#partheader table td.title a {
+ color: #666;
+ text-decoration: none;
+}
+
#edit-attachments {
margin-top: 0.6em;
}
#edit-attachments ul li {
display: block;
color: #333;
font-weight: bold;
padding: 8px 4px 3px 30px;
text-shadow: 0px 1px 1px #fff;
text-decoration: none;
white-space: nowrap;
}
#edit-attachments ul li a.file {
padding: 0;
}
#edit-attachments-form {
padding-top: 1.2em;
}
#edit-attachments-form .buttons {
margin: 0.5em 0;
}
#event-attachments .attachmentslist li {
float: left;
margin-right: 1em;
}
#event-attachments .attachmentslist li a {
outline: none;
}
#event-attendees span.attendee {
padding-right: 18px;
margin-right: 0.5em;
background: url(images/attendee-status.gif) right 0 no-repeat;
}
#event-attendees span.attendee a.mailtolink {
text-decoration: none;
white-space: nowrap;
}
#event-attendees span.attendee a.mailtolink:hover {
text-decoration: underline;
}
#event-attendees span.accepted {
background-position: right -20px;
}
#event-attendees span.declined {
background-position: right -40px;
}
#event-attendees span.tentative {
background-position: right -60px;
}
#event-attendees span.organizer {
background-position: right -80px;
}
/* jQuery UI overrides */
#eventshow h1 {
font-size: 18px;
margin: -0.3em 0 0.4em 0;
}
#eventshow label,
#eventshow h5.label {
font-weight: normal;
font-size: 1em;
color: #999;
margin: 0 0 0.2em 0;
}
#eventshow {
margin: 0 -0.2em;
}
#eventshow.sensitivity-private {
background: url(images/badge_private.png) top right no-repeat;
}
#eventshow.sensitivity-confidential {
background: url(images/badge_confidential.png) top right no-repeat;
}
.sensitivity-private #event-title {
margin-right: 50px;
}
.sensitivity-confidential #event-title {
margin-right: 60px;
}
#eventshow div.event-line {
margin-top: 0.1em;
margin-bottom: 0.3em;
}
#eventedit {
position: relative;
top: -1.5em;
padding: 0.5em 0.1em;
margin: 0 -0.2em;
}
#eventedit input.text,
#eventedit textarea {
width: 97%;
}
#eventtabs {
position: relative;
padding: 0;
border: 0;
border-radius: 0;
}
div.form-section,
#eventshow div.event-section,
#eventtabs div.event-section {
margin-top: 0.2em;
margin-bottom: 0.8em;
}
#eventtabs .border-after {
padding-bottom: 0.8em;
margin-bottom: 0.8em;
border-bottom: 2px solid #fafafa;
}
#eventshow label,
#eventedit label,
.form-section label {
display: inline-block;
min-width: 7em;
padding-right: 0.5em;
}
#eventedit .formtable td.label {
min-width: 6em;
}
td.topalign {
vertical-align: top;
}
#eventedit label.weekday,
#eventedit label.monthday {
min-width: 3em;
}
#eventedit label.month {
min-width: 5em;
}
#edit-recurrence-yearly-bymonthblock {
margin-left: 7.5em;
}
#eventedit .recurrence-form {
display: none;
}
#eventedit .formtable td {
padding: 0.2em 0;
}
.ui-dialog .event-update-confirm {
padding: 0 0.5em 0.5em 0.5em;
}
.event-dialog-message,
.event-update-confirm .message {
margin-top: 0.5em;
padding: 0.8em;
- background-color: #F7FDCB;
- border: 1px solid #C2D071;
+ border: 1px solid #ffdf0e;
+ background-color: #fef893;
}
.event-dialog-message .message,
.event-update-confirm .message {
margin-bottom: 0.5em;
}
.edit-recurring-warning .savemode {
padding-left: 20px;
}
.event-update-confirm .savemode {
padding-left: 30px;
}
.event-dialog-message span.ui-icon,
.event-update-confirm span.ui-icon {
float: left;
margin: 0 7px 20px 0;
}
.event-dialog-message label,
.event-update-confirm label {
min-width: 3em;
padding-right: 1em;
}
.event-update-confirm a.button {
margin: 0 0.5em 0 0.2em;
min-width: 5em;
}
#event-rsvp,
#edit-attendees-notify {
margin: 0.3em 0;
padding: 0.5em;
- border: 1px solid #ffdf0e;
- background-color: #fef893;
}
#edit-attendees-table {
width: 100%;
margin-top: 0.5em;
}
#edit-attendees-table td.role {
width: 9em;
}
#edit-attendees-table td.availability,
#edit-attendees-table td.confirmstate {
width: 4em;
}
#edit-attendees-table td.options {
width: 3em;
text-align: right;
padding-right: 4px;
}
#edit-attendees-table td.name {
width: auto;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
#edit-attendees-form {
position: relative;
margin-top: 1em;
}
#edit-attendees-form #edit-attendee-schedule {
position: absolute;
top: 0;
right: 0;
}
#edit-attendees-table select.edit-attendee-role {
border: 0;
padding: 2px;
background: white;
}
.availability img.availabilityicon {
margin: 1px;
width: 14px;
height: 14px;
border-radius: 4px;
-moz-border-radius: 4px;
}
.availability img.availabilityicon.loading {
background: url(images/loading_blue.gif) center no-repeat;
}
#schedule-freebusy-times td.unknown,
.availability img.availabilityicon.unknown {
background: #ddd;
}
#schedule-freebusy-times td.free,
.availability img.availabilityicon.free {
background: #0c0;
}
#schedule-freebusy-times td.busy,
.availability img.availabilityicon.busy {
background: #c00;
}
#schedule-freebusy-times td.tentative,
.availability img.availabilityicon.tentative {
background: #66d;
}
#schedule-freebusy-times td.out-of-office,
.availability img.availabilityicon.out-of-office {
background: #f0b400;
}
#schedule-freebusy-times td.all-busy,
#schedule-freebusy-times td.all-tentative,
#schedule-freebusy-times td.all-out-of-office {
background-image: url(images/freebusy-colors.png);
background-position: top right;
background-repeat: no-repeat;
}
#schedule-freebusy-times td.all-tentative {
background-position: right -40px;
}
#schedule-freebusy-times td.all-out-of-office {
background-position: right -80px;
}
#edit-attendees-legend {
margin-top: 3em;
margin-bottom: 0.5em;
}
#edit-attendees-legend .legend {
margin-right: 2em;
white-space: nowrap;
}
#edit-attendees-legend img.availabilityicon {
vertical-align: middle;
}
#edit-attendees-table tbody td.confirmstate {
overflow: hidden;
white-space: nowrap;
text-indent: -2000%;
}
#edit-attendees-table td.confirmstate span {
display: block;
width: 20px;
background: url(images/attendee-status.gif) 5px 0 no-repeat;
}
#edit-attendees-table td.confirmstate span.needs-action {
}
#edit-attendees-table td.confirmstate span.accepted {
background-position: 5px -20px;
}
#edit-attendees-table td.confirmstate span.declined {
background-position: 5px -40px;
}
#edit-attendees-table td.confirmstate span.tentative {
background-position: 5px -60px;
}
#attendees-freebusy-table {
width: 100%;
table-layout: fixed;
border-collapse: collapse;
margin: 0.5em 0;
}
#attendees-freebusy-table td.attendees {
width: 18em;
border: 1px solid #ccc;
vertical-align: top;
overflow: hidden;
}
#attendees-freebusy-table td.times {
width: auto;
vertical-align: top;
border: 1px solid #ccc;
}
#attendees-freebusy-table div.scroll {
position: relative;
overflow: auto;
}
#attendees-freebusy-table h3.boxtitle {
margin: 0;
height: auto !important;
border-color: #ccc;
}
.attendees-list .attendee {
padding: 3px 4px 3px 1px;
background: url(images/attendee-status.gif) 2px -97px no-repeat;
white-space: nowrap;
}
.attendees-list a.attendee-role-toggle {
display: inline-block;
width: 16px;
margin-right: 3px;
cursor: pointer;
}
.attendees-list div.attendee {
border-top: 1px solid #ccc;
}
.attendees-list span.attendee {
padding-left: 20px;
margin-right: 2em;
}
.attendees-list .organizer {
background-position: 3px -77px;
}
.attendees-list .opt-participant {
background-position: 2px -117px;
}
.attendees-list .chair {
background-position: 2px -137px;
}
.attendees-list .loading {
background: url(images/loading_blue.gif) 1px 50% no-repeat;
}
.attendees-list .total {
background: none;
padding-left: 4px;
font-weight: bold;
}
.attendees-list .spacer,
#schedule-freebusy-times tr.spacer td {
background: 0;
font-size: 50%;
}
#schedule-freebusy-times {
border-collapse: collapse;
width: 100%;
}
#schedule-freebusy-times td {
padding: 3px;
border: 1px solid #ccc;
}
#schedule-freebusy-times tr.dates th {
border-color: #aaa;
border-style: solid;
border-width: 0 1px 0 1px;
white-space: nowrap;
}
#attendees-freebusy-table div.timesheader,
#schedule-freebusy-times tr.times td {
min-width: 30px;
font-size: 9px;
padding: 5px 2px 6px 2px;
text-align: center;
}
#schedule-freebusy-times tr.times td.allday {
min-width: 60px;
}
#schedule-freebusy-times tr.times td {
cursor: pointer;
}
#schedule-event-time {
position: absolute;
border: 2px solid #333;
background: #777;
background: rgba(60, 60, 60, 0.6);
opacity: 0.5;
border-radius: 4px;
cursor: move;
filter: alpha(opacity=40); /* IE8 */
}
#eventfreebusy .schedule-options {
position: relative;
margin-bottom: 1.5em;
}
#eventfreebusy .schedule-buttons {
position: absolute;
top: 0;
right: 0;
}
#eventfreebusy .schedule-find-buttons {
padding-bottom:0.5em;
}
#eventfreebusy .schedule-find-buttons button {
min-width: 9em;
text-align: center;
}
span.edit-alarm-set {
white-space: nowrap;
}
a.dropdown-link {
color: #CC0000;
font-size: 12px;
text-decoration: none;
}
a.dropdown-link:after {
content: ' ▼';
font-size: 11px;
color: #666;
}
#eventedit .ui-tabs-panel {
min-height: 20em;
}
.alarm-item {
margin: 0.4em 0 1em 0;
}
.alarm-item .event-title {
font-size: 14px;
margin: 0.1em 0 0.3em 0;
}
.alarm-item div.event-section {
margin-top: 0.1em;
margin-bottom: 0.3em;
}
.alarm-item .alarm-actions {
margin-top: 0.4em;
}
.alarm-item div.alarm-actions a {
color: #CC0000;
margin-right: 0.8em;
text-decoration: none;
}
a.alarm-action-snooze:after {
content: ' ▼';
font-size: 10px;
color: #666;
}
#alarm-snooze-dropdown {
z-index: 5000;
}
.ui-dialog-buttonset a.dropdown-link {
margin-right: 1em;
}
/*
.ui-datepicker-calendar .ui-datepicker-today .ui-state-default {
border-color: #cccccc;
background: #ffffcc;
color: #000;
}
*/
.ui-datepicker-calendar .ui-datepicker-week-col {
border: 0;
color: #999;
font-size: 90%;
text-align: right;
padding-right: 6px;
}
/*
.ui-datepicker th {
padding: 0.3em 0;
font-size: 10px;
}
.ui-datepicker td span,
.ui-datepicker td a {
padding-left: 0.1em;
}
*/
.ui-autocomplete {
max-height: 160px;
overflow-y: auto;
overflow-x: hidden;
}
.ui-autocomplete .ui-menu-item {
white-space: nowrap;
}
* html .ui-autocomplete {
height: 160px;
}
span.spacer {
padding-left: 3em;
}
#agendaoptions {
position: absolute;
bottom: 28px;
left: 0;
right: 0;
height: auto;
z-index: 200;
background: #d6eaf3;
border: 1px solid #c3c3c3;
border-top-color: #ddd;
padding: 4px 5px;
}
#agendaoptions label {
color: #69939e;
text-shadow: 1px 1px #f2f2f2;
padding-right: 0.5em;
}
#calendar-kolabform {
position: relative;
margin: 0 -8px;
min-width: 660px;
min-height: 400px;
}
#calendar-kolabform table td.title {
font-weight: bold;
white-space: nowrap;
color: #666;
padding-right: 10px;
}
.propform fieldset.tab {
background: #efefef;
display: block;
margin-top: 0.5em;
padding: 0.5em 1em;
}
/* fullcalendar style overrides */
.rcube-fc-content {
overflow: hidden;
border: 0;
border-radius: 4px 4px 0 0;
box-shadow: 0 0 2px #999;
-o-box-shadow: 0 0 2px #999;
-webkit-box-shadow: 0 0 2px #999;
-moz-box-shadow: 0 0 2px #999;
}
.fc-content {
position: absolute !important;
top: 38px;
left: 0;
right: 0;
bottom: 28px;
background: #fff;
}
#fish-eye-view .fc-content {
top: 2px;
bottom: 2px;
}
.calendarmain .fc-button,
.calendarmain .fc-button.fc-state-hover,
.calendarmain .fc-button.fc-state-down {
border: 0;
background: none;
}
.calendarmain .fc-state-default .fc-button-inner,
.calendarmain .fc-state-hover .fc-button-inner {
margin: 0 0 0 0;
color: #555;
text-shadow: 0px 1px 1px #fff;
border: 1px solid #a2a2a2;
background: #f7f7f7;
background: -moz-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#f9f9f9), color-stop(100%,#e6e6e6));
background: -o-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
background: -ms-linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
background: linear-gradient(top, #f9f9f9 0%, #e6e6e6 100%);
box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3);
-o-box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3);
-webkit-box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3);
-moz-box-shadow: 0 1px 1px 0 rgba(140, 140, 140, 0.3);
text-decoration: none;
}
.calendarmain .fc-state-disabled .fc-button-inner {
color: #bbb;
}
.calendarmain .fc-header .fc-button {
margin-left: -1px;
margin-right: 0;
}
.calendarmain .fc-state-down .fc-button-inner {
margin: 0;
border: 1px solid #a2a2a2;
background: #e6e6e6;
background: -moz-linear-gradient(top, #e6e6e6 0%, #f9f9f9 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#e6e6e6), color-stop(100%,#f9f9f9));
background: -o-linear-gradient(top, #e6e6e6 0%, #f9f9f9 100%);
background: -ms-linear-gradient(top, #e6e6e6 0%, #f9f9f9 100%);
background: linear-gradient(top, #e6e6e6 0%, #f9f9f9 100%);
}
.calendarmain .fc-state-active .fc-button-inner {
color: #333;
background: #bababa;
background: -moz-linear-gradient(top, #bababa 0%, #d8d8d8 100%);
background: -webkit-gradient(linear, left top, left bottom, color-stop(0%,#bababa), color-stop(100%,#d8d8d8));
background: -o-linear-gradient(top, #bababa 0%, #d8d8d8 100%);
background: -ms-linear-gradient(top, #bababa 0%, #d8d8d8 100%);
background: linear-gradient(top, #bababa 0%, #d8d8d8 100%);
}
.calendarmain .fc-corner-left .fc-button-inner,
.calendarmain .fc-corner-left .fc-button-content {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
.calendarmain .fc-corner-right .fc-button-inner,
.calendarmain .fc-corner-right .fc-button-content {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
.calendarmain .fc-state-default .fc-button-effect {
display: none;
}
.calendarmain .fc-button-content {
height: 2.2em;
line-height: 2.2em;
}
#calendar .fc-header-right {
padding-right: 250px;
}
.fc-event {
font-size: 1em !important;
}
.fc-event-title {
font-weight: bold;
}
.fc-event-hori .fc-event-title {
font-weight: normal;
white-space: nowrap;
}
.fc-event-hori .fc-event-time {
white-space: nowrap;
font-weight: normal !important;
font-size: 10px;
padding-right: 0.6em;
}
.fc-grid .fc-event-time {
font-weight: normal !important;
padding-right: 0.3em;
}
.fc-event-cateories {
font-style:italic;
}
div.fc-event-location {
font-size: 90%;
}
.fc-more-link {
color: #999;
padding-top: 1px;
cursor: pointer;
}
.fc-agenda-slots td div {
height: 22px;
}
.fc-sat, .fc-sun {
background-color: #fdfdfd;
}
.fc-widget-header {
background-color: #d6eaf3;
color: #004458;
text-shadow: 0px 1px 1px #fff;
}
.fc-view thead th.fc-widget-header {
padding: 8px 0;
color: #69939e;
}
.fc-day-number {
color: #578da5;
}
.fc-icon-alarms,
.fc-icon-sensitive,
.fc-icon-recurring {
display: inline-block;
width: 11px;
height: 11px;
background: url(images/eventicons.gif) 0 0 no-repeat;
margin-left: 3px;
line-height: 10px;
}
.fc-icon-alarms {
background-position: 0 -13px;
}
.fc-icon-sensitive {
background-position: 0 -25px;
}
.fc-list-section .fc-event {
cursor: pointer;
}
/*.calendarmain .fc-view-list div.fc-list-header,*/
.calendarmain .fc-view-table td.fc-list-header,
#edit-attendees-table thead td {
color: #69939e;
font-size: 11px;
font-weight: bold;
background: #d6eaf3;
background: -moz-linear-gradient(left, #e3f2f6 0, #d6eaf3 14px, #d6eaf3 100%);
background: -webkit-gradient(linear, left top, right top, color-stop(0,#e3f2f6), color-stop(8%,#d6eaf3), color-stop(100%,#d6eaf3));
background: -o-linear-gradient(left, #e3f2f6 0, #d6eaf3 14px, #d6eaf3 100%);
background: -ms-linear-gradient(left, #e3f2f6 0, #d6eaf3 14px ,#d6eaf3 100%);
background: linear-gradient(left, #e3f2f6 0, #d6eaf3 14px, #d6eaf3 100%);
border: 0;
padding: 7px;
}
.calendarmain .fc-view-table tr.fc-event td {
border-color: #ddd;
padding: 4px 7px;
}
.calendarmain .fc-view-table col.fc-event-location {
width: 20%;
}
.calendarmain .fc-view-table tr.fc-event td.fc-event-handle {
padding: 5px 10px 2px 7px;
width: 12px;
}
.calendarmain .fc-view-table .fc-event-handle .fc-event-skin {
margin: 0;
padding: 0;
display: inline-block;
width: 10px;
height: 10px;
font-size: 6px;
border-radius: 8px;
}
.calendarmain .fc-view-table .fc-event-handle .fc-event-inner {
display: inline-block;
width: 10px;
height: 10px;
padding: 0;
font-size: 10px;
border-radius: 8px;
border: 1px solid rgba(0, 0, 0, 0.4);
-webkit-box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
-moz-box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
box-shadow: inset 0px 0 1px 1px rgba(0, 0, 0, 0.3);
}
.fc-listappend {
text-align: center;
margin: 1em 0;
}
.fc-listappend .message {
padding: 0.5em;
margin-bottom: 0.5em;
font-size: 150%;
color: #999;
}
.fc-listappend .formlinks a {
font-size: 12px;
padding: 0 0.3em;
}
.fc-event-temp {
opacity: 0.4;
filter: alpha(opacity=40); /* IE8 */
}
/* Settings section */
fieldset #calendarcategories div {
margin-bottom: 0.3em;
}
/* Invitation UI in mail */
#messagemenu li a.calendarlink span.calendar {
background-position: 0px -1948px;
}
div.calendar-invitebox {
min-height: 20px;
margin: 5px 8px;
padding: 3px 6px 6px 34px;
border: 1px solid #ffdf0e;
background: url(images/calendar.png) 6px 5px no-repeat #fef893;
}
div.calendar-invitebox td.ititle {
font-weight: bold;
padding-right: 0.5em;
}
div.calendar-invitebox td.label {
color: #666;
padding-right: 1em;
}
#event-rsvp .rsvp-buttons,
div.calendar-invitebox .rsvp-status,
div.calendar-invitebox .rsvp-buttons {
margin-top: 0.5em;
}
#event-rsvp input.button,
div.calendar-invitebox input.button {
font-weight: bold;
margin-right: 0.5em;
}
div.calendar-invitebox .calendar-select {
font-weight: 10px;
margin-left: 1em;
}
div.calendar-invitebox .rsvp-status.loading {
color: #666;
padding: 1px 0 2px 24px;
background: url(images/loading_blue.gif) top left no-repeat;
}
div.calendar-invitebox .rsvp-status.declined,
div.calendar-invitebox .rsvp-status.tentative,
div.calendar-invitebox .rsvp-status.accepted {
padding: 0 0 1px 22px;
background: url(images/attendee-status.gif) 2px -20px no-repeat;
}
div.calendar-invitebox .rsvp-status.declined {
background-position: 2px -40px;
}
div.calendar-invitebox .rsvp-status.tentative {
background-position: 2px -60px;
}
/* iTIP attend reply page */
.calendaritipattend .centerbox {
width: 40em;
margin: 80px auto;
padding: 10px 10px 10px 90px;
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
index 439afd40..4d4789da 100644
--- a/plugins/calendar/skins/larry/templates/attachment.html
+++ b/plugins/calendar/skins/larry/templates/attachment.html
@@ -1,36 +1,36 @@
<roundcube:object name="doctype" value="html5" />
<html>
<head>
<title><roundcube:object name="pagetitle" /></title>
<roundcube:include file="/includes/links.html" />
</head>
<body class="extwin">
<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%" />
+ <roundcube:object name="plugin.attachmentframe" id="attachmentframe" class="header-table" style="width:100%" />
</div>
</div>
</body>
</html>
diff --git a/plugins/kolab_addressbook/kolab_addressbook.php b/plugins/kolab_addressbook/kolab_addressbook.php
index c03c4263..65b4bf1b 100644
--- a/plugins/kolab_addressbook/kolab_addressbook.php
+++ b/plugins/kolab_addressbook/kolab_addressbook.php
@@ -1,590 +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');
+ $this->require_plugin('libkolab');
// 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
'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');
+ $this->folders = kolab_storage::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');
asort($names, SORT_LOCALE_STRING);
foreach ($names as $utf7name => $name) {
// create instance of rcube_contacts
- $abook_id = rcube_kolab::folder_id($utf7name);
+ $abook_id = kolab_storage::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);
+ $p['form']['personal']['content']['pgppublickey'] = array('size' => 70);
+ $p['form']['personal']['content']['pkcs7publickey'] = array('size' => 70);
// 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),
+ 'pgppublickey' => array('size' => 70, 'visible' => true),
+ 'pkcs7publickey' => array('size' => 70, 'visible' => false),
)
);
*/
}
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 = $storage->get_hierarchy_delimiter();
if (strlen($oldfolder)) {
$options = $storage->folder_info($oldfolder);
}
if (!empty($options) && ($options['norename'] || $options['protected'])) {
}
// sanity checks (from steps/settings/save_folder.inc)
else if (!strlen($folder)) {
$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)
$folder = $storage->mod_folder($folder, 'in');
}
// Check access rights to the parent folder
if (strlen($path) && (!strlen($oldfolder) || $oldfolder != $folder)) {
$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);
+ $result = kolab_storage::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);
+ $result = kolab_storage::folder_create($folder, 'contact');
}
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),
+ 'id' => kolab_storage::folder_id($folder),
'name' => $name,
'readonly' => false,
'editable' => true,
'groups' => true,
'realname' => rcube_charset::convert($folder, 'UTF7-IMAP'), // IMAP folder name
'class_name' => $kolab_folder->get_namespace(),
'kolab' => true,
- ), rcube_kolab::folder_id($oldfolder));
+ ), kolab_storage::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)) {
+ if (kolab_storage::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));
+ $this->rc->output->command('book_delete_done', kolab_storage::folder_id($folder));
}
else {
$this->rc->output->show_message('kolab_addressbook.bookdeleteerror', 'error');
}
$this->rc->output->send();
}
}
diff --git a/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php b/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php
index d154a0a7..980df059 100644
--- a/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php
+++ b/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php
@@ -1,281 +1,281 @@
<?php
/**
* Kolab address book UI
*
* @author Aleksander Machniak <machniak@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_addressbook_ui
{
private $plugin;
private $rc;
/**
* Class constructor
*
* @param kolab_addressbook $plugin Plugin object
*/
public function __construct($plugin)
{
$this->rc = rcmail::get_instance();
$this->plugin = $plugin;
$this->init_ui();
}
/**
* Adds folders management functionality to Addressbook UI
*/
private function init_ui()
{
if (!empty($this->rc->action) && !preg_match('/^plugin\.book/', $this->rc->action)) {
return;
}
// Include script
$this->plugin->include_script('kolab_addressbook.js');
if (empty($this->rc->action)) {
// Include stylesheet (for directorylist)
$this->plugin->include_stylesheet($this->plugin->local_skin_path().'/kolab_addressbook.css');
// Add actions on address books
$options = array('book-create', 'book-edit', 'book-delete');
$idx = 0;
foreach ($options as $command) {
$content = html::tag('li', $idx ? null : array('class' => 'separator_above'),
$this->plugin->api->output->button(array(
'label' => 'kolab_addressbook.'.str_replace('-', '', $command),
'domain' => $this->ID,
'classact' => 'active',
'command' => $command
)));
$this->plugin->api->add_content($content, 'groupoptions');
$idx++;
}
// Link to Settings/Folders
$content = html::tag('li', array('class' => 'separator_above'),
$this->plugin->api->output->button(array(
'label' => 'managefolders',
'type' => 'link',
'classact' => 'active',
'command' => 'folders',
'task' => 'settings',
)));
$this->plugin->api->add_content($content, 'groupoptions');
$this->rc->output->add_label('kolab_addressbook.bookdeleteconfirm',
'kolab_addressbook.bookdeleting');
}
// book create/edit form
else {
$this->rc->output->add_label('kolab_addressbook.nobooknamewarning',
'kolab_addressbook.booksaving');
}
}
/**
* Handler for address book create/edit action
*/
public function book_edit()
{
$this->rc->output->add_handler('bookdetails', array($this, 'book_form'));
$this->rc->output->send('kolab_addressbook.bookedit');
}
/**
* Handler for 'bookdetails' object returning form content for book create/edit
*
* @param array $attr Object attributes
*
* @return string HTML output
*/
public function book_form($attrib)
{
$action = trim(get_input_value('_act', RCUBE_INPUT_GPC));
$folder = trim(get_input_value('_source', RCUBE_INPUT_GPC, true)); // UTF8
$hidden_fields[] = array('name' => '_source', 'value' => $folder);
$folder = rcube_charset_convert($folder, RCMAIL_CHARSET, 'UTF7-IMAP');
$delim = $_SESSION['imap_delimiter'];
if ($this->rc->action == 'plugin.book-save') {
// save error
$name = trim(get_input_value('_name', RCUBE_INPUT_GPC, true)); // UTF8
$old = trim(get_input_value('_oldname', RCUBE_INPUT_GPC, true)); // UTF7-IMAP
$path_imap = trim(get_input_value('_parent', RCUBE_INPUT_GPC, true)); // UTF7-IMAP
$hidden_fields[] = array('name' => '_oldname', 'value' => $old);
$folder = $old;
}
else if ($action == 'edit') {
$path_imap = explode($delim, $folder);
$name = rcube_charset_convert(array_pop($path_imap), 'UTF7-IMAP');
$path_imap = implode($path_imap, $delim);
}
else { // create
$path_imap = $folder;
$name = '';
$folder = '';
}
// Store old name, get folder options
if (strlen($folder)) {
$hidden_fields[] = array('name' => '_oldname', 'value' => $folder);
- $this->rc->imap_connect();
- $options = $this->rc->imap->mailbox_info($folder);
+ $this->rc->storage_connect();
+ $options = $this->rc->get_storage()->mailbox_info($folder);
}
$form = array();
// General tab
$form['props'] = array(
'name' => $this->rc->gettext('properties'),
);
if (!empty($options) && ($options['norename'] || $options['protected'])) {
- $foldername = Q(str_replace($delimiter, ' &raquo; ', rcube_kolab::object_name($folder)));
+ $foldername = Q(str_replace($delimiter, ' &raquo; ', kolab_storage::object_name($folder)));
}
else {
$foldername = new html_inputfield(array('name' => '_name', 'id' => '_name', 'size' => 30));
$foldername = $foldername->show($name);
}
$form['props']['fieldsets']['location'] = array(
'name' => $this->rc->gettext('location'),
'content' => array(
'name' => array(
'label' => $this->plugin->gettext('bookname'),
'value' => $foldername,
),
),
);
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('contact', array('name' => '_parent'), $folder);
+ $select = kolab_storage::folder_selector('contact', array('name' => '_parent'), $folder);
$form['props']['fieldsets']['location']['content']['path'] = array(
'label' => $this->plugin->gettext('parentbook'),
'value' => $select->show(strlen($folder) ? $path_imap : ''),
);
}
// Allow plugins to modify address book form content (e.g. with ACL form)
$plugin = $this->rc->plugins->exec_hook('addressbook_form',
array('form' => $form, 'options' => $options, 'name' => $folder));
$form = $plugin['form'];
// Set form tags and hidden fields
list($form_start, $form_end) = $this->get_form_tags($attrib, 'plugin.book-save', null, $hidden_fields);
unset($attrib['form']);
// return the complete edit form as table
$out = "$form_start\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) {
$out .= html::tag('fieldset', null, html::tag('legend', null, Q($tab['name'])) . $content) ."\n";
}
}
$out .= "\n$form_end";
return $out;
}
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;
}
private function get_form_tags($attrib, $action, $id = null, $hidden = null)
{
$form_start = $form_end = '';
$request_key = $action . (isset($id) ? '.'.$id : '');
$form_start = $this->rc->output->request_form(array(
'name' => 'form',
'method' => 'post',
'task' => $this->rc->task,
'action' => $action,
'request' => $request_key,
'noclose' => true,
) + $attrib);
if (is_array($hidden)) {
foreach ($hidden as $field) {
$hiddenfield = new html_hiddenfield($field);
$form_start .= $hiddenfield->show();
}
}
$form_end = !strlen($attrib['form']) ? '</form>' : '';
$EDIT_FORM = !empty($attrib['form']) ? $attrib['form'] : 'form';
$this->rc->output->add_gui_object('editform', $EDIT_FORM);
return array($form_start, $form_end);
}
}
diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
index f028f522..5702a0c9 100644
--- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
+++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
@@ -1,1232 +1,1154 @@
<?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
+ * rcube_addressbook interface with the kolab_storage wrapper from libkolab
*
* @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),
+ 'address' => array('subtypes' => array('home','work','office')),
+ 'website' => array('subtypes' => array('homepage','blog')),
+ 'im' => array('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),
+ 'manager' => array('limit' => null),
+ 'assistant' => array('limit' => null),
'spouse' => array('limit' => 1),
- 'children' => array('type' => 'text', 'size' => 40, 'maxlength' => 80, 'limit' => 1,
+ 'children' => array('type' => 'text', 'size' => 40, 'maxlength' => 80, 'limit' => null,
'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'),
+ 'pgppublickey' => array('type' => 'textarea', 'size' => 70, 'rows' => 10, 'limit' => 1,
+ 'label' => 'kolab_addressbook.pgppublickey'),
+ 'pkcs7publickey' => array('type' => 'textarea', 'size' => 70, 'rows' => 10, 'limit' => 1,
+ 'label' => 'kolab_addressbook.pkcs7publickey'),
'notes' => array(),
'photo' => array(),
- // TODO: define more Kolab-specific fields such as: language, latitude, longitude
+ // TODO: define more Kolab-specific fields such as: language, latitude, longitude, crypto settings
);
/**
* 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',
- );
+ private $action;
public function __construct($imap_folder = null)
{
if ($imap_folder) {
$this->imap_folder = $imap_folder;
}
// extend coltypes configuration
- $format = rcube_kolab::get_format('contact');
- $this->coltypes['phone']['subtypes'] = $format->_phone_types;
- $this->coltypes['address']['subtypes'] = $format->_address_types;
+ $format = kolab_format::factory('contact');
+ $this->coltypes['phone']['subtypes'] = array_keys($format->phonetypes);
+ $this->coltypes['address']['subtypes'] = array_keys($format->addresstypes);
// 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);
+ $this->storagefolder = kolab_storage::get_folder($this->imap_folder);
+ $this->ready = $this->storagefolder && !PEAR::isError($this->storagefolder);
// Set readonly and editable flags according to folder permissions
if ($this->ready) {
- if ($this->get_owner() == $_SESSION['username']) {
+ if ($this->storagefolder->get_owner() == $_SESSION['username']) {
$this->editable = true;
$this->readonly = false;
}
else {
- $rights = $this->storagefolder->getMyRights();
+ $rights = $this->storagefolder->get_myrights();
if (!PEAR::isError($rights)) {
if (strpos($rights, 'i') !== false)
$this->readonly = false;
if (strpos($rights, 'a') !== false || strpos($rights, 'x') !== false)
$this->editable = true;
}
}
}
+
+ $this->action = rcmail::get_instance()->action;
}
/**
* 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);
+ $folder = kolab_storage::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);
+ if ($this->namespace === null && $this->ready) {
+ $this->namespace = $this->storagefolder->get_namespace();
}
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']);
+ if (!$search || strstr(strtolower($group['name']), strtolower($search)))
+ $groups[$group['name']] = array('ID' => $group['ID'], 'name' => $group['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();
+ $this->result = new rcube_result_set(0, ($this->list_page-1) * $this->page_size);;
+
// list member of the selected group
if ($this->gid) {
+ $this->_fetch_groups();
$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']]++)
+ if ($member['uid'] && ($contact = $this->storagefolder->get_object($member['uid'])) && !$seen[$member['ID']]++) {
+ $this->contacts[$member['ID']] = $this->_to_rcube_contact($contact);
+ $this->result->count++;
+ }
+ else if ($member['email'] && !$seen[$member['ID']]++) {
+ $this->contacts[$member['ID']] = $member;
$this->result->count++;
+ }
}
$ids = array_keys($seen);
}
- else
- $ids = is_array($this->filter['ids']) ? $this->filter['ids'] : array_keys($this->contacts);
+ else if (is_array($this->filter['ids'])) {
+ $ids = $this->filter['ids'];
+ if ($this->result->count = count($ids))
+ $this->_fetch_contacts(array(array('uid', '=', $ids)));
+ }
+ else {
+ $this->_fetch_contacts();
+ $ids = array_keys($this->contacts);
+ $this->result->count = count($ids);
+ }
// 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, '|') . ')(?:.*)$/';
+ // pass query to storage if only indexed cols are involved
+ // NOTE: this is only some rough pre-filtering but probably includes false positives
+ $squery = array();
+ if (count(array_intersect(kolab_format_contact::$fulltext_cols, $fields)) == $scount) {
+ switch ($mode) {
+ case 1: $prefix = ' '; $suffix = ' '; break; // strict
+ case 2: $prefix = ' '; $suffix = ''; break; // prefix
+ default: $prefix = ''; $suffix = ''; break; // substring
+ }
+
+ $search_string = is_array($value) ? join(' ', $value) : $value;
+ foreach (rcube_utils::normalize_string($search_string, true) as $word) {
+ $squery[] = array('words', 'LIKE', '%' . $prefix . $word . $suffix . '%');
+ }
+ }
+
+ // get all/matching records
+ $this->_fetch_contacts($squery);
+
// 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;
+ foreach ((array)$val as $str) {
+ $str = mb_strtolower($str);
+ switch ($mode) {
+ case 1:
+ $got = ($str == $search);
+ break;
+ case 2:
+ $got = ($search == substr($str, 0, strlen($search)));
+ break;
+ default:
+ $got = (strpos($str, $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));
+ if ($this->gid) {
+ $this->_fetch_groups();
+ $count = count($this->distlists[$this->gid]['member']);
+ }
+ else if (is_array($this->filter['ids'])) {
+ $count = count($this->filter['ids']);
+ }
+ else {
+ $count = $this->storagefolder->count();
+ }
+
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]) {
+ $rec = null;
+ $uid = $this->_id2uid($id);
+ if (strpos($uid, 'mailto:') === 0) {
+ $this->_fetch_groups(true);
+ $rec = $this->contacts[$id];
+ $this->readonly = true; // set source to read-only
+ }
+ else if ($object = $this->storagefolder->get_object($uid)) {
+ $rec = $this->_to_rcube_contact($object);
+ }
+
+ if ($rec) {
$this->result = new rcube_result_set(1);
- $this->result->add($this->contacts[$id]);
- return $assoc ? $this->contacts[$id] : $this->result;
+ $this->result->add($rec);
+ return $assoc ? $rec : $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'];
+ $out[$gid] = $group['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);
+ $saved = $this->storagefolder->save($object, 'contact');
- if (PEAR::isError($saved)) {
+ if (!$saved) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Error saving contact object to Kolab server:" . $saved->getMessage()),
+ 'message' => "Error saving contact object to Kolab server"),
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));
+ if ($old = $this->storagefolder->get_object($this->_id2uid($id))) {
+ $object = $this->_from_rcube_contact($save_data, $old);
- $saved = $this->contactstorage->save($object, $uid);
- if (PEAR::isError($saved)) {
+ if (!$this->storagefolder->save($object, 'contact', $old['uid'])) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Error saving contact object to Kolab server:" . $saved->getMessage()),
+ 'message' => "Error saving contact object to Kolab server"),
true, false);
}
else {
$this->contacts[$id] = $this->_to_rcube_contact($object);
$updated = true;
+
+ // TODO: update data in groups this contact is member of
}
}
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 ($uid = $this->_id2uid($id)) {
+ $is_mailto = strpos($uid, 'mailto:') === 0;
+ $deleted = $is_mailto || $this->storagefolder->delete($uid, $force);
- if (PEAR::isError($deleted)) {
+ if (!$deleted) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Error deleting a contact object from the Kolab server:" . $deleted->getMessage()),
+ 'message' => "Error deleting a contact object $uid from the Kolab server"),
true, false);
}
else {
// remove from distribution lists
- foreach ((array)$this->groupmembers[$id] as $gid)
- $this->remove_from_group($gid, $id);
+ foreach ((array)$this->groupmembers[$id] as $gid) {
+ if (!$is_mailto || $gid == $this->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]);
+ unset($this->contacts[$id], $this->groupmembers[$id]);
$count++;
}
}
}
- // store IMAP uids for undelete()
- if (!$force) {
- $_SESSION['kolab_delete_uids'] = $imap_uids;
- }
-
return $count;
}
/**
* Undelete one or more contact records.
* Only possible just after delete (see 2nd argument of delete() method).
*
* @param array Record identifiers
*
* @return int Number of records restored
*/
public function undelete($ids)
{
if (!is_array($ids))
$ids = explode(',', $ids);
- $count = 0;
- $uids = array();
- $imap_uids = $_SESSION['kolab_delete_uids'];
-
- // convert contact IDs into IMAP UIDs
- foreach ($ids as $id)
- if ($uid = $imap_uids[$id])
- $uids[] = $uid;
-
- if (!empty($uids)) {
- $session = &Horde_Kolab_Session::singleton();
- $imap = &$session->getImap();
-
- if (is_object($imap) && is_a($imap, 'PEAR_Error')) {
- $error = $imap;
+ $count = 0;
+ foreach ($ids as $id) {
+ $uid = $this->_id2uid($id);
+ if ($this->storagefolder->undelete($uid)) {
+ $count++;
}
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()),
+ 'message' => "Error undeleting a contact object $uid from the Kolab server"),
true, false);
}
-
- $rcmail = rcmail::get_instance();
- $rcmail->session->remove('kolab_delete_uids');
}
- return count($uids);
+ return $count;
}
/**
* Remove all records from the database
*/
public function delete_all()
{
- $this->_connect();
-
- if (!PEAR::isError($this->contactstorage->deleteAll())) {
+ if ($this->storagefolder->delete_all()) {
$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,
+ 'uid' => kolab_format::generate_uid(),
+ 'name' => $name,
'member' => array(),
);
- $saved = $this->liststorage->save($list);
+ $saved = $this->storagefolder->save($list, 'distribution-list');
- if (PEAR::isError($saved)) {
+ if (!$saved) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Error saving distribution-list object to Kolab server:" . $saved->getMessage()),
+ 'message' => "Error saving distribution-list object to Kolab server"),
true, false);
return false;
}
else {
- $id = md5($list['uid']);
+ $id = $this->_uid2id($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']);
+ $deleted = $this->storagefolder->delete($list['uid']);
- if (PEAR::isError($deleted)) {
+ if (!$deleted) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Error deleting distribution-list object from the Kolab server:" . $deleted->getMessage()),
+ 'message' => "Error deleting distribution-list object from the Kolab server"),
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 ($newname != $list['name']) {
+ $list['name'] = $newname;
+ $saved = $this->storagefolder->save($list, 'distribution-list', $list['uid']);
}
- if (PEAR::isError($saved)) {
+ if (!$saved) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Error saving distribution-list object to Kolab server:" . $saved->getMessage()),
+ 'message' => "Error saving distribution-list object to Kolab server"),
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();
+ $this->_fetch_groups(true);
$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,
- );
- }
+ $uid = $this->_id2uid($contact_id);
+ if ($contact = $this->storagefolder->get_object($uid)) {
+ foreach ($this->get_col_values('email', $contact, true) as $email)
+ break;
+
+ $list['member'][] = array(
+ 'uid' => $uid,
+ 'email' => $email,
+ 'name' => $contact['name'],
+ );
+ $this->groupmembers[$contact_id][] = $gid;
+ $added++;
+ }
+ else if (strpos($uid, 'mailto:') === 0 && ($contact = $this->contacts[$contact_id])) {
+ $list['member'][] = array(
+ 'email' => $contact['email'],
+ 'name' => $contact['name'],
+ );
$this->groupmembers[$contact_id][] = $gid;
$added++;
}
}
if ($added)
- $saved = $this->liststorage->save($list, $list['uid']);
+ $saved = $this->storagefolder->save($list, 'distribution-list', $list['uid']);
- if (PEAR::isError($saved)) {
+ if (!$saved) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Error saving distribution-list to Kolab server:" . $saved->getMessage()),
+ 'message' => "Error saving distribution-list to Kolab server"),
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']);
+ $saved = $this->storagefolder->save($list, 'distribution-list', $list['uid']);
- if (PEAR::isError($saved)) {
+ if (!$saved) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Error saving distribution-list object to Kolab server:" . $saved->getMessage()),
+ 'message' => "Error saving distribution-list object to Kolab server"),
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
+ * Query storage layer and store records in private member var
*/
- 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()
+ private function _fetch_contacts($query = array())
{
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;
-
+ $this->contacts = array();
+ foreach ((array)$this->storagefolder->select($query) as $record) {
$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_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;
}
$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_value == $b_value)
return 0;
$arr = array($a_value, $b_value);
sort($arr, SORT_LOCALE_STRING);
return $a_value == $arr[0] ? -1 : 1;
}
/**
* Read distribution-lists AKA groups from server
*/
- private function _fetch_groups()
+ private function _fetch_groups($with_contacts = false)
{
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)$this->storagefolder->get_objects('distribution-list') as $record) {
+ $record['ID'] = $this->_uid2id($record['uid']);
foreach ((array)$record['member'] as $i => $member) {
- $mid = md5($member['uid']);
+ $mid = $this->_uid2id($member['uid'] ? $member['uid'] : 'mailto:' . $member['email']);
$record['member'][$i]['ID'] = $mid;
+ $record['member'][$i]['readonly'] = empty($member['uid']);
$this->groupmembers[$mid][] = $record['ID'];
+
+ if ($with_contacts && empty($member['uid']))
+ $this->contacts[$mid] = $record['member'][$i];
}
$this->distlists[$record['ID']] = $record;
}
}
}
+ /**
+ * Encode object UID into a safe identifier
+ */
+ private function _uid2id($uid)
+ {
+ return rtrim(strtr(base64_encode($uid), '+/', '-_'), '=');
+ }
+
+ /**
+ * Convert Roundcube object identifier back into the original UID
+ */
+ private function _id2uid($id)
+ {
+ return base64_decode(str_pad(strtr($id, '-_', '+/'), strlen($id) % 4, '=', STR_PAD_RIGHT));
+ }
+
/**
* 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];
+ $record['ID'] = $this->_uid2id($record['uid']);
+
+ if (is_array($record['phone'])) {
+ $phones = $record['phone'];
+ unset($record['phone']);
+ foreach ((array)$phones as $i => $phone) {
+ $key = 'phone' . ($phone['type'] ? ':' . $phone['type'] : '');
+ $record[$key][] = $phone['number'];
+ }
}
- 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['website'])) {
+ $urls = $record['website'];
+ unset($record['website']);
+ foreach ((array)$urls as $i => $url) {
+ $key = 'website' . ($url['type'] ? ':' . $url['type'] : '');
+ $record[$key][] = $url['url'];
+ }
+ }
if (is_array($record['address'])) {
- foreach ($record['address'] as $i => $adr) {
- $key = 'address:' . $adr['type'];
- $out[$key][] = array(
- 'street' => $adr['street'],
+ $addresses = $record['address'];
+ unset($record['address']);
+ foreach ($addresses as $i => $adr) {
+ $key = 'address' . ($adr['type'] ? ':' . $adr['type'] : '');
+ $record[$key][] = array(
+ 'street' => $adr['street'],
'locality' => $adr['locality'],
- 'zipcode' => $adr['postal-code'],
- 'region' => $adr['region'],
- 'country' => $adr['country'],
+ 'zipcode' => $adr['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']);
+ if ($record['photo'] && strlen($record['photo']) < 255 && ($att = $record['_attachments'][$record['photo']])) {
+ // only fetch photo content if requested
+ if ($this->action == 'photo')
+ $record['photo'] = $att['content'] ? $att['content'] : $this->storagefolder->get_attachment($record['uid'], $att['id']);
}
+ // truncate publickey value for display
+ if ($record['pgppublickey'] && $this->action == 'show')
+ $record['pgppublickey'] = substr($record['pgppublickey'], 0, 140) . '...';
+
// remove empty fields
- return array_filter($out);
+ return array_filter($record);
}
/**
- * Map fields from Roundcube format to internal Kolab_Format
+ * Map fields from Roundcube format to internal kolab_format_contact properties
*/
- private function _from_rcube_contact($contact)
+ private function _from_rcube_contact($contact, $old = array())
{
- $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;
+ if (!$contact['uid'] && $contact['ID'])
+ $contact['uid'] = $this->_id2uid($contact['ID']);
+ else if (!$contact['uid'] && $old['uid'])
+ $contact['uid'] = $old['uid'];
+
+ $contact['email'] = array_filter($this->get_col_values('email', $contact, true));
+ $contact['im'] = array_filter($this->get_col_values('im', $contact, true));
+
+ foreach ($this->get_col_values('website', $contact) as $type => $values) {
+ foreach ((array)$values as $url) {
+ if (!empty($url)) {
+ $contact['website'][] = array('url' => $url, 'type' => $type);
+ }
+ }
+ unset($contact['website:'.$type]);
}
- // 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);
+ $contact['phone'][] = array('number' => $phone, 'type' => $type);
}
}
+ unset($contact['phone:'.$type]);
}
- $object['address'] = array();
-
+ $addresses = 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'],
- );
- }
+ $addresses[] = array(
+ 'type' => $type,
+ 'street' => $adr['street'],
+ 'locality' => $adr['locality'],
+ 'code' => $adr['zipcode'],
+ 'region' => $adr['region'],
+ 'country' => $adr['country'],
+ );
}
+
+ unset($contact['address:'.$type]);
}
+ $contact['address'] = $addresses;
- // 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;
+ // copy meta data (starting with _) from old object
+ foreach ((array)$old as $key => $val) {
+ if (!isset($contact[$key]) && $key[0] == '_')
+ $contact[$key] = $val;
}
- return $object;
+ // add empty values for some fields which can be removed in the UI
+ return array_filter($contact) + array('nickname' => '', 'birthday' => '', 'anniversary' => '', 'freebusyurl' => '');
}
}
diff --git a/plugins/kolab_addressbook/localization/de_CH.inc b/plugins/kolab_addressbook/localization/de_CH.inc
index f91a24ba..34394482 100644
--- a/plugins/kolab_addressbook/localization/de_CH.inc
+++ b/plugins/kolab_addressbook/localization/de_CH.inc
@@ -1,46 +1,47 @@
<?php
$labels = array();
$labels['initials'] = 'Initialen';
$labels['profession'] = 'Berufsbezeichnung';
$labels['officelocation'] = 'Büro Adresse';
$labels['children'] = 'Kinder';
-$labels['pgppublickey'] = 'Öffentlicher PGP-Schlüssel';
+$labels['pgppublickey'] = 'PGP-Schlüssel';
+$labels['pkcs7publickey'] = 'S/MIME-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
index 5fd86b76..2c2a5d20 100644
--- a/plugins/kolab_addressbook/localization/de_DE.inc
+++ b/plugins/kolab_addressbook/localization/de_DE.inc
@@ -1,46 +1,47 @@
<?php
$labels = array();
$labels['initials'] = 'Initialen';
$labels['profession'] = 'Berufsbezeichnung';
$labels['officelocation'] = 'Büro Adresse';
$labels['children'] = 'Kinder';
-$labels['pgppublickey'] = 'Öffentlicher PGP-Schlüssel';
+$labels['pgppublickey'] = 'PGP-Schlüssel';
+$labels['pkcs7publickey'] = 'S/MIME-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/localization/en_US.inc b/plugins/kolab_addressbook/localization/en_US.inc
index 36f2139f..a66426f4 100644
--- a/plugins/kolab_addressbook/localization/en_US.inc
+++ b/plugins/kolab_addressbook/localization/en_US.inc
@@ -1,46 +1,47 @@
<?php
$labels = array();
$labels['initials'] = 'Initials';
$labels['profession'] = 'Profession';
$labels['officelocation'] = 'Office location';
$labels['children'] = 'Children';
-$labels['pgppublickey'] = 'PGP publickey';
+$labels['pgppublickey'] = 'PGP public key';
+$labels['pkcs7publickey'] = 'S/MIME public key';
$labels['freebusyurl'] = 'Free-busy URL';
$labels['typebusiness'] = 'Business';
$labels['typebusinessfax'] = 'Business Fax';
$labels['typecompany'] = 'Company';
$labels['typeprimary'] = 'Primary';
$labels['typetelex'] = 'Telex';
$labels['typeradio'] = 'Radio';
$labels['typeisdn'] = 'ISDN';
$labels['typettytdd'] = 'TTY/TDD';
$labels['typecallback'] = 'Callback';
$labels['settings'] = 'Settings';
$labels['bookcreate'] = 'Create address book';
$labels['bookedit'] = 'Edit address book';
$labels['bookdelete'] = 'Delete address book';
$labels['bookproperties'] = 'Address book properties';
$labels['bookname'] = 'Book name';
$labels['parentbook'] = 'Superior book';
$labels['addressbookprio'] = 'Address book(s) selection/behaviour';
$labels['personalfirst'] = 'Personal address book(s) first';
$labels['globalfirst'] = 'Global address book(s) first';
$labels['personalonly'] = 'Personal address book(s) only';
$labels['globalonly'] = 'Global address book(s) only';
$messages['bookdeleteconfirm'] = 'Do you really want to delete the selected address book and all contacts in it?';
$messages['bookdeleting'] = 'Deleting address book...';
$messages['booksaving'] = 'Saving address book...';
$messages['bookdeleted'] = 'Address book deleted successfully.';
$messages['bookupdated'] = 'Address book updated successfully.';
$messages['bookcreated'] = 'Address book created successfully.';
$messages['bookdeleteerror'] = 'An error occured while deleting address book.';
$messages['bookupdateerror'] = 'An error occured while updating address book.';
$messages['bookcreateerror'] = 'An error occured while creating address book.';
$messages['nobooknamewarning'] = 'Please, enter address book name.';
$messages['noemailnamewarning'] = 'Please, enter email address or contact name.';
?>
diff --git a/plugins/kolab_addressbook/package.xml b/plugins/kolab_addressbook/package.xml
index e077dcb5..9d43c65c 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://git.kolab.org/roundcube-plugins-kolab/</uri>
+ <uri>http://git.kolab.org/roundcubemail-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_addressbook/skins/larry/kolab_addressbook.css b/plugins/kolab_addressbook/skins/larry/kolab_addressbook.css
new file mode 100644
index 00000000..f6963b47
--- /dev/null
+++ b/plugins/kolab_addressbook/skins/larry/kolab_addressbook.css
@@ -0,0 +1,28 @@
+
+#directorylist li.addressbook.readonly,
+#directorylist li.addressbook.shared,
+#directorylist li.addressbook.other {
+/* background-image: url(kolab_folders.png); */
+ background-position: 5px -1000px;
+ background-repeat: no-repeat;
+}
+
+#directorylist li.addressbook.readonly {
+ background-position: 5px 0px;
+}
+
+#directorylist li.addressbook.shared {
+ background-position: 5px -54px;
+}
+
+#directorylist li.addressbook.shared.readonly {
+ background-position: 5px -72px;
+}
+
+#directorylist li.addressbook.other {
+ background-position: 5px -18px;
+}
+
+#directorylist li.addressbook.other.readonly {
+ background-position: 5px -36px;
+}
diff --git a/plugins/kolab_addressbook/skins/larry/templates/bookedit.html b/plugins/kolab_addressbook/skins/larry/templates/bookedit.html
new file mode 100644
index 00000000..007d512e
--- /dev/null
+++ b/plugins/kolab_addressbook/skins/larry/templates/bookedit.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" />
+</head>
+<body class="iframe">
+
+<h1 class="boxtitle"><roundcube:label name="kolab_addressbook.bookproperties" /></h1>
+
+<div class="boxcontent">
+ <roundcube:object name="bookdetails" class="propform" />
+</div>
+
+<div id="formfooter">
+<div class="footerleft formbuttons">
+ <roundcube:button command="book-save" type="input" class="button mainaction" label="save" />
+</div>
+</div>
+
+<roundcube:include file="/includes/footer.html" />
+
+</body>
+</html>
diff --git a/plugins/kolab_auth/package.xml b/plugins/kolab_auth/package.xml
index 937798d9..52131031 100644
--- a/plugins/kolab_auth/package.xml
+++ b/plugins/kolab_auth/package.xml
@@ -1,59 +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://git.kolab.org/roundcube-plugins-kolab/</uri>
+ <uri>http://git.kolab.org/roundcubemail-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>2012-02-29</date>
<version>
<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/kolab_config.php b/plugins/kolab_config/kolab_config.php
index 24d569e3..b785306b 100644
--- a/plugins/kolab_config/kolab_config.php
+++ b/plugins/kolab_config/kolab_config.php
@@ -1,145 +1,138 @@
<?php
/**
* Kolab configuration storage.
*
* Plugin to use Kolab server as a configuration storage. Provides an API to handle
* configuration according to http://wiki.kolab.org/KEP:9.
*
* @version @package_version@
* @author Machniak Aleksander <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_config extends rcube_plugin
{
public $task = 'utils';
private $config;
private $enabled;
/**
* Required startup method of a Roundcube plugin
*/
public function init()
{
$rcmail = rcmail::get_instance();
// Register spellchecker dictionary handlers
if (strtolower($rcmail->config->get('spellcheck_dictionary')) != 'shared') {
$this->add_hook('spell_dictionary_save', array($this, 'dictionary_save'));
$this->add_hook('spell_dictionary_get', array($this, 'dictionary_get'));
}
/*
// Register addressbook saved searches handlers
$this->add_hook('saved_search_create', array($this, 'saved_search_create'));
$this->add_hook('saved_search_delete', array($this, 'saved_search_delete'));
$this->add_hook('saved_search_list', array($this, 'saved_search_list'));
$this->add_hook('saved_search_get', array($this, 'saved_search_get'));
*/
}
/**
* Initializes config object and dependencies
*/
private function load()
{
if ($this->config)
return;
- $this->require_plugin('kolab_folders');
+ return; // CURRENTLY DISABLED until libkolabxml has support for config objects
- // load dependencies
- require_once 'Horde/Util.php';
- require_once 'Horde/Kolab/Format.php';
- require_once 'Horde/Kolab/Format/XML.php';
- require_once $this->home . '/lib/configuration.php';
- require_once $this->home . '/lib/kolab_configuration.php';
-
- String::setDefaultCharset('UTF-8');
+ $this->require_plugin('libkolab');
$this->config = new kolab_configuration();
// check if configuration folder exist
if (strlen($this->config->dir)) {
$this->enabled = true;
}
}
/**
* Saves spellcheck dictionary.
*
* @param array $args Hook arguments
*
* @return array Hook arguments
*/
public function dictionary_save($args)
{
$this->load();
if (!$this->enabled) {
return $args;
}
$lang = $args['language'];
$dict = $this->dict;
$dict['type'] = 'dictionary';
$dict['language'] = $args['language'];
$dict['e'] = $args['dictionary'];
if (empty($dict['e'])) {
// Delete the object
$this->config->del($dict);
}
else {
// Update the object
$this->config->set($dict);
}
$args['abort'] = true;
return $args;
}
/**
* Returns spellcheck dictionary.
*
* @param array $args Hook arguments
*
* @return array Hook arguments
*/
public function dictionary_get($args)
{
$this->load();
if (!$this->enabled) {
return $args;
}
$lang = $args['language'];
$this->dict = $this->config->get('dictionary.'.$lang);
if (!empty($this->dict)) {
$args['dictionary'] = $this->dict['e'];
}
$args['abort'] = true;
return $args;
}
}
diff --git a/plugins/kolab_config/package.xml b/plugins/kolab_config/package.xml
index 85c7faa4..a0a29796 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://git.kolab.org/roundcube-plugins-kolab/</uri>
+ <uri>http://git.kolab.org/roundcubemail-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 fa407546..034e1b16 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://git.kolab.org/roundcube-plugins-kolab/</uri>
+ <uri>http://git.kolab.org/roundcubemail-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_folders/kolab_folders.php b/plugins/kolab_folders/kolab_folders.php
index e1b2e634..3e010611 100644
--- a/plugins/kolab_folders/kolab_folders.php
+++ b/plugins/kolab_folders/kolab_folders.php
@@ -1,668 +1,517 @@
<?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();
+ // load required plugin
+ $this->require_plugin('libkolab');
+
// Folder listing hooks
$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()) {
+ // infinite loop prevention
+ if ($this->is_processing) {
return $args;
}
- $filter = $args['filter'];
-
- // all-folders request, use core method
- if (!$filter) {
+ if (!$this->metadata_support()) {
return $args;
}
- // get folders types
- $folderdata = $this->get_folder_type_list($args['root'].$args['name'], true);
+ $this->is_processing = true;
- if (!is_array($folderdata)) {
- return $args;
- }
+ // get folders
+ $folders = kolab_storage::list_folders($args['root'], $args['name'], $args['filter'], $args['mode'] == 'LSUB');
- $regexp = '/^' . preg_quote($filter, '/') . '(\..+)?$/';
+ $this->is_processing = false;
- // 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);
+ if (!is_array($folders)) {
return $args;
}
- $storage = $this->rc->get_storage();
-
- // Get folders list
- if ($args['mode'] == 'LIST') {
- 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']);
+ // Create default folders
+ if ($args['root'] == '' && $args['name'] = '*') {
+ $this->create_default_folders($folders, $args['filter']);
}
- // 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]);
- }
- }
+ $args['folders'] = $folders;
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'];
+ $table = $args['table'];
+ $storage = $this->rc->get_storage();
// get folders types
- $folderdata = $this->get_folder_type_list('*');
+ $folderdata = $storage->get_metadata('*', kolab_storage::CTYPE_KEY);
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];
+ $type = !empty($folderdata[$folder]) ? $folderdata[$folder][kolab_storage::CTYPE_KEY] : null;
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 = $storage->folder_info($mbox);
if ($options['namespace'] != 'personal' && !in_array('a', $options['rights'])) {
if (in_array($ctype, $this->types)) {
$value = $this->gettext('foldertype'.$ctype);
}
else {
$value = $ctype;
}
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 = $storage->create_folder($mbox, $subscribe);
// Set folder type
if ($result) {
$this->set_folder_type($mbox, $ctype);
}
}
// Rename folder
else {
if ($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
+ * @return boolean
*/
function metadata_support()
{
$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)
{
$storage = $this->rc->get_storage();
- $folderdata = $storage->get_metadata($folder, array(kolab_folders::CTYPE_KEY));
+ $folderdata = $storage->get_metadata($folder, kolab_storage::CTYPE_KEY);
- return explode('.', $folderdata[$folder][kolab_folders::CTYPE_KEY]);
+ return explode('.', $folderdata[$folder][kolab_storage::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')
{
$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='*')
- {
- $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 = $storage->conn->listMailboxes($root, $name,
- NULL, array('SUBSCRIBED'));
-
- // remove non-existent folders
- if (is_array($a_folders) && $name = '*' && !empty($storage->conn->data['LIST'])) {
- foreach ($a_folders as $idx => $folder) {
- if (($opts = $storage->conn->data['LIST'][$folder])
- && in_array('\\NonExistent', $opts)
- ) {
- $storage->conn->unsubscribe($folder);
- unset($a_folders[$idx]);
- }
- }
- }
- }
- // retrieve list of folders from IMAP server using LSUB
- else {
- $a_folders = $storage->conn->listSubscribed($root, $name);
-
- // unsubscribe non-existent folders, remove from the list
- if (is_array($a_folders) && $name == '*' && !empty($storage->conn->data['LIST'])) {
- foreach ($a_folders as $idx => $folder) {
- if (!isset($storage->conn->data['LIST'][$folder])
- || in_array('\\Noselect', $storage->conn->data['LIST'][$folder])
- ) {
- // Some servers returns \Noselect for existing folders
- 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 = $storage->get_cache($cache_key);
-
- if (!is_array($metadata)) {
- $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) {
- $storage->update_cache($cache_key, $metadata);
- }
-
- return $metadata;
+ return $storage->set_metadata($folder, array(kolab_storage::CTYPE_KEY => $type));
}
/**
* 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('*');
+ $folderdata = $storage->get_metadata('*', kolab_storage::CTYPE_KEY);
if (!is_array($folderdata)) {
return null;
}
$type .= '.default';
$namespace = $storage->get_namespace();
// get all folders of specified type
- $folderdata = array_intersect($folderdata, array($type));
+ $folderdata = array_map('implode', $folderdata);
+ $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)
+ private function create_default_folders(&$folders, $filter)
{
$storage = $this->rc->get_storage();
$namespace = $storage->get_namespace();
+ $folderdata = $storage->get_metadata('*', kolab_storage::CTYPE_KEY);
$defaults = array();
$need_update = false;
+ if (!is_array($folderdata)) {
+ return;
+ }
+
+ // "Flattenize" metadata array to become a name->type hash
+ $folderdata = array_map('implode', $folderdata);
+
// 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');
// 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));
+ $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' || !$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;
+ if ($result && (!$filter || $filter == $type1)) {
+ $folders[] = $foldername;
}
}
-
- // update cache
- if ($need_update && $cache_key) {
- $storage->update_cache($cache_key, $folderdata);
- }
}
}
diff --git a/plugins/kolab_folders/package.xml b/plugins/kolab_folders/package.xml
index b1c364a7..875d6140 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://git.kolab.org/roundcube-plugins-kolab/</uri>
+ <uri>http://git.kolab.org/roundcubemail-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>
+ <date>2012-05-14</date>
<version>
- <release>1.0</release>
- <api>1.0</api>
+ <release>2.0</release>
+ <api>2.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 f19fe3ad..b65f39ca 100644
--- a/plugins/kolab_zpush/kolab_zpush.php
+++ b/plugins/kolab_zpush/kolab_zpush.php
@@ -1,346 +1,349 @@
<?php
/**
* Z-Push configuration utility for Kolab accounts
*
* @version 0.2
* @author Thomas Bruederli <bruederli@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');
+
+ if ($this->rc->action == 'plugin.zpushconfig') {
+ $this->require_plugin('libkolab');
+ }
}
/**
* Establish IMAP connection
*/
public function init_imap()
{
$storage = $this->rc->get_storage();
+ // @TODO: Metadata is already cached by rcube storage, get rid of cache here
+
$this->cache = $this->rc->get_cache('zpush', 'db', 900);
$this->cache->expunge();
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()
{
$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();
$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':
$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 = !$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', $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 = $storage->get_metadata($folder, array(self::ACTIVESYNC_KEY));
if ($asyncdata = $folderdata[$folder][self::ACTIVESYNC_KEY])
$metadata = $this->unserialize_metadata($asyncdata);
$metadata['FOLDER'] = $meta;
$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 = $storage->set_metadata(self::ROOT_MAILBOX, array(self::ACTIVESYNC_KEY => $this->serialize_metadata($this->root_meta)))) {
$this->cache->remove('devicemeta');
$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 = $storage->get_metadata($folder, array(self::ACTIVESYNC_KEY));
if ($asyncdata = $folderdata[$folder][self::ACTIVESYNC_KEY])
$metadata = $this->unserialize_metadata($asyncdata);
$metadata['FOLDER'] = $meta;
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';
-
+
$storage = $this->rc->get_storage();
-
+
// checks if IMAP server supports any of METADATA, ANNOTATEMORE, 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->init_imap();
$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 {
$storage = $this->rc->get_storage();
$this->folders = $storage->list_folders();
foreach ($this->folders as $folder) {
$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/kolab_zpush_ui.php b/plugins/kolab_zpush/kolab_zpush_ui.php
index e651e982..4c99cf78 100644
--- a/plugins/kolab_zpush/kolab_zpush_ui.php
+++ b/plugins/kolab_zpush/kolab_zpush_ui.php
@@ -1,171 +1,171 @@
<?php
/**
* Z-Push configuration user interface builder
*
* @version 0.2
* @author Thomas Bruederli <bruederli@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_zpush_ui
{
private $rc;
private $config;
public function __construct($config)
{
$this->config = $config;
$this->rc = rcmail::get_instance();
$skin = $this->rc->config->get('skin');
$this->config->include_stylesheet('skins/' . $skin . '/config.css');
$this->rc->output->include_script('list.js');
$this->skin_path = $this->config->urlbase . 'skins/' . $skin . '/';
}
public function device_list($attrib = array())
{
$attrib += array('id' => 'devices-list');
$devices = $this->config->list_devices();
$table = new html_table();
foreach ($devices as $id => $device) {
$name = $device['ALIAS'] ? $device['ALIAS'] : $id;
$table->add_row(array('id' => 'rcmrow' . $id));
$table->add(null, html::span('devicealias', Q($name)) . html::span('devicetype', Q($device['TYPE'])));
}
$this->rc->output->add_gui_object('devicelist', $attrib['id']);
$this->rc->output->set_env('devices', $devices);
return $table->show($attrib);
}
public function device_config_form($attrib = array())
{
$table = new html_table(array('cols' => 2));
$field_id = 'config-device-alias';
$input = new html_inputfield(array('name' => 'devicealias', 'id' => $field_id, 'size' => 40));
$table->add('title', html::label($field_id, $this->config->gettext('devicealias')));
$table->add(null, $input->show());
-
+
$field_id = 'config-device-mode';
$select = new html_select(array('name' => 'syncmode', 'id' => $field_id));
$select->add(array($this->config->gettext('modeauto'), $this->config->gettext('modeflat'), $this->config->gettext('modefolder')), array('-1', '0', '1'));
$table->add('title', html::label($field_id, $this->config->gettext('syncmode')));
$table->add(null, $select->show('-1'));
-
+
$field_id = 'config-device-laxpic';
$checkbox = new html_checkbox(array('name' => 'laxpic', 'value' => '1', 'id' => $field_id));
$table->add('title', $this->config->gettext('imageformat'));
$table->add(null, html::label($field_id, $checkbox->show() . ' ' . $this->config->gettext('laxpiclabel')));
-
+
if ($attrib['form'])
$this->rc->output->add_gui_object('editform', $attrib['form']);
return $table->show($attrib);
}
public function folder_subscriptions($attrib = array())
{
if (!$attrib['id'])
$attrib['id'] = 'foldersubscriptions';
-
+
// group folders by type (show only known types)
$folder_groups = array('mail' => array(), 'contact' => array(), 'event' => array(), 'task' => array());
$folder_meta = $this->config->folders_meta();
foreach ($this->config->list_folders() as $folder) {
$type = $folder_meta[$folder]['TYPE'] ? $folder_meta[$folder]['TYPE'] : 'mail';
if (is_array($folder_groups[$type]))
$folder_groups[$type][] = $folder;
}
-
+
// build block for every folder type
foreach ($folder_groups as $type => $group) {
if (empty($group))
continue;
$attrib['type'] = $type;
$html .= html::div('subscriptionblock',
html::tag('h3', $type, $this->config->gettext($type)) .
$this->folder_subscriptions_block($group, $attrib));
}
$this->rc->output->add_gui_object('subscriptionslist', $attrib['id']);
-
+
return html::div($attrib, $html);
}
public function folder_subscriptions_block($a_folders, $attrib)
{
$alarms = ($attrib['type'] == 'event' || $attrib['type'] == 'task');
-
+
$table = new html_table(array('cellspacing' => 0));
$table->add_header('subscription', $attrib['syncicon'] ? html::img(array('src' => $this->skin_path . $attrib['syncicon'], 'title' => $this->config->gettext('synchronize'))) : '');
$table->add_header('alarm', $alarms && $attrib['alarmicon'] ? html::img(array('src' => $this->skin_path . $attrib['alarmicon'], 'title' => $this->config->gettext('withalarms'))) : '');
$table->add_header('foldername', $this->config->gettext('folder'));
$checkbox_sync = new html_checkbox(array('name' => 'subscribed[]', 'class' => 'subscription'));
$checkbox_alarm = new html_checkbox(array('name' => 'alarm[]', 'class' => 'alarm'));
$names = array();
foreach ($a_folders as $folder) {
- $foldername = $origname = preg_replace('/^INBOX &raquo;\s+/', '', rcube_kolab::object_name($folder));
+ $foldername = $origname = preg_replace('/^INBOX &raquo;\s+/', '', kolab_storage::object_name($folder));
// find folder prefix to truncate (the same code as in kolab_addressbook plugin)
for ($i = count($names)-1; $i >= 0; $i--) {
if (strpos($foldername, $names[$i].' &raquo; ') === 0) {
$length = strlen($names[$i].' &raquo; ');
$prefix = substr($foldername, 0, $length);
$count = count(explode(' &raquo; ', $prefix));
$foldername = str_repeat('&nbsp;&nbsp;', $count-1) . '&raquo; ' . substr($foldername, $length);
break;
}
}
$names[] = $origname;
$classes = array('mailbox');
if ($folder_class = rcmail_folder_classname($folder)) {
$foldername = rcube_label($folder_class);
$classes[] = $folder_class;
}
$folder_id = 'rcmf' . html_identifier($folder);
$padding = str_repeat('&nbsp;&nbsp;&nbsp;&nbsp;', $level);
$table->add_row(array('class' => (($level+1) * $idx++) % 2 == 0 ? 'even' : 'odd'));
$table->add('subscription', $checkbox_sync->show('', array('value' => $folder, 'id' => $folder_id)));
if ($alarms)
$table->add('alarm', $checkbox_alarm->show('', array('value' => $folder, 'id' => $folder_id.'_alarm')));
else
$table->add('alarm', '');
-
+
$table->add(join(' ', $classes), html::label($folder_id, $padding . Q($foldername)));
}
return $table->show();
}
}
diff --git a/plugins/kolab_zpush/package.xml b/plugins/kolab_zpush/package.xml
index 250d1a66..ef6dfd72 100644
--- a/plugins/kolab_zpush/package.xml
+++ b/plugins/kolab_zpush/package.xml
@@ -1,71 +1,72 @@
<?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://git.kolab.org/roundcube-plugins-kolab/</uri>
+ <uri>http://git.kolab.org/roundcubemail-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>
+ <date>2012-05-14</date>
<version>
- <release>0.3</release>
+ <release>1.0</release>
+ <api>1.0</api>
</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/de_DE.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/libkolab/README b/plugins/libkolab/README
new file mode 100644
index 00000000..0a3c0ce3
--- /dev/null
+++ b/plugins/libkolab/README
@@ -0,0 +1,43 @@
+libkolab plugin to access to Kolab groupware data
+=================================================
+
+The contained library classes establish a connection to the Kolab server
+and manage the access to the Kolab groupware objects stored in various
+IMAP folders. For reading and writing these objects, the PHP bindings of
+the libkolabxml library are used.
+
+
+REQUIREMENTS
+------------
+* libkolabxml PHP bindings
+ - kolabformat.so loaded into PHP
+ - kolabformat.php placed somewhere in the include_path
+* PEAR: HTTP/Request2
+* PEAR: Net/URL2
+
+* Optional for old format support:
+ Horde Kolab_Format package and all of its dependencies
+ which are at least Horde_(Browser,DOM,NLS,String,Utils)
+
+
+INSTALLATION
+------------
+To use local cache you need to create a dedicated table in Roundcube's database.
+To do so, execute the SQL commands in SQL/<yourdatabase>.sql
+
+
+CONFIGURATION
+-------------
+The following options can be configured in Roundcube's main config file
+or a local config file (config.inc.php) located in the plugin folder.
+
+// Enable caching of Kolab objects in local database
+$rcmail_config['kolab_cache'] = true;
+
+// Optional override of the URL to read and trigger Free/Busy information of Kolab users
+// Defaults to https://<imap-server->/freebusy
+$rcmail_config['kolab_freebusy_server'] = 'https://<some-host>/<freebusy-path>';
+
+// Set this option to disable SSL certificate checks when triggering Free/Busy (enabled by default)
+$rcmail_config['kolab_ssl_verify_peer'] = false;
+
diff --git a/plugins/libkolab/SQL/mysql.sql b/plugins/libkolab/SQL/mysql.sql
new file mode 100644
index 00000000..55d0dbc9
--- /dev/null
+++ b/plugins/libkolab/SQL/mysql.sql
@@ -0,0 +1,22 @@
+/**
+ * libkolab database schema
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli
+ * @licence GNU AGPL
+ **/
+
+CREATE TABLE IF NOT EXISTS `kolab_cache` (
+ `resource` VARCHAR(255) CHARACTER SET ascii NOT NULL,
+ `type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
+ `msguid` BIGINT UNSIGNED NOT NULL,
+ `uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
+ `created` DATETIME DEFAULT NULL,
+ `data` TEXT NOT NULL,
+ `xml` TEXT NOT NULL,
+ `dtstart` DATETIME,
+ `dtend` DATETIME,
+ `tags` VARCHAR(255) NOT NULL,
+ `words` TEXT NOT NULL,
+ PRIMARY KEY(`resource`,`type`,`msguid`)
+) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
diff --git a/plugins/libkolab/lib/kolab_format.php b/plugins/libkolab/lib/kolab_format.php
new file mode 100644
index 00000000..a7b1e487
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_format.php
@@ -0,0 +1,289 @@
+<?php
+
+/**
+ * Kolab format model class wrapping libkolabxml bindings
+ *
+ * Abstract base class for different Kolab groupware objects read from/written
+ * to the new Kolab 3 format using the PHP bindings of libkolabxml.
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli@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/>.
+ */
+
+abstract class kolab_format
+{
+ public static $timezone;
+
+ protected $obj;
+ protected $data;
+ protected $xmldata;
+ protected $loaded = false;
+
+ /**
+ * Factory method to instantiate a kolab_format object of the given type
+ *
+ * @param string Object type to instantiate
+ * @param string Cached xml data to initialize with
+ * @return object kolab_format
+ */
+ public static function factory($type, $xmldata = null)
+ {
+ if (!isset(self::$timezone))
+ self::$timezone = new DateTimeZone('UTC');
+
+ $suffix = preg_replace('/[^a-z]+/', '', $type);
+ $classname = 'kolab_format_' . $suffix;
+ if (class_exists($classname))
+ return new $classname($xmldata);
+
+ return PEAR::raiseError(sprintf("Failed to load Kolab Format wrapper for type %s", $type));
+ }
+
+ /**
+ * Convert the given date/time value into a cDateTime object
+ *
+ * @param mixed Date/Time value either as unix timestamp, date string or PHP DateTime object
+ * @param DateTimeZone The timezone the date/time is in. Use global default if empty
+ * @param boolean True of the given date has no time component
+ * @return object The libkolabxml date/time object
+ */
+ public static function get_datetime($datetime, $tz = null, $dateonly = false)
+ {
+ if (!$tz) $tz = self::$timezone;
+ $result = new cDateTime();
+
+ // got a unix timestamp (in UTC)
+ if (is_numeric($datetime)) {
+ $datetime = new DateTime('@'.$datetime, new DateTimeZone('UTC'));
+ if ($tz) $datetime->setTimezone($tz);
+ }
+ else if (is_string($datetime) && strlen($datetime))
+ $datetime = new DateTime($datetime, $tz);
+
+ if (is_a($datetime, 'DateTime')) {
+ $result->setDate($datetime->format('Y'), $datetime->format('n'), $datetime->format('j'));
+
+ if (!$dateonly)
+ $result->setTime($datetime->format('G'), $datetime->format('i'), $datetime->format('s'));
+
+ if ($tz && $tz->getName() == 'UTC')
+ $result->setUTC(true);
+ else if ($tz)
+ $result->setTimezone($tz->getName());
+ }
+
+ return $result;
+ }
+
+ /**
+ * Convert the given cDateTime into a PHP DateTime object
+ *
+ * @param object cDateTime The libkolabxml datetime object
+ * @return object DateTime PHP datetime instance
+ */
+ public static function php_datetime($cdt)
+ {
+ if (!is_object($cdt) || !$cdt->isValid())
+ return null;
+
+ $d = new DateTime;
+ $d->setTimezone(self::$timezone);
+
+ try {
+ if ($tzs = $cdt->timezone()) {
+ $tz = new DateTimeZone($tzs);
+ $d->setTimezone($tz);
+ }
+ else if ($cdt->isUTC()) {
+ $d->setTimezone(new DateTimeZone('UTC'));
+ }
+ }
+ catch (Exception $e) { }
+
+ $d->setDate($cdt->year(), $cdt->month(), $cdt->day());
+
+ if ($cdt->isDateOnly()) {
+ $d->_dateonly = true;
+ $d->setTime(12, 0, 0); // set time to noon to avoid timezone troubles
+ }
+ else {
+ $d->setTime($cdt->hour(), $cdt->minute(), $cdt->second());
+ }
+
+ return $d;
+ }
+
+ /**
+ * Convert a libkolabxml vector to a PHP array
+ *
+ * @param object vector Object
+ * @return array Indexed array contaning vector elements
+ */
+ public static function vector2array($vec, $max = PHP_INT_MAX)
+ {
+ $arr = array();
+ for ($i=0; $i < $vec->size() && $i < $max; $i++)
+ $arr[] = $vec->get($i);
+ return $arr;
+ }
+
+ /**
+ * Build a libkolabxml vector (string) from a PHP array
+ *
+ * @param array Array with vector elements
+ * @return object vectors
+ */
+ public static function array2vector($arr)
+ {
+ $vec = new vectors;
+ foreach ((array)$arr as $val) {
+ if (strlen($val))
+ $vec->push($val);
+ }
+ return $vec;
+ }
+
+ /**
+ * Check for format errors after calling kolabformat::write*()
+ *
+ * @return boolean True if there were errors, False if OK
+ */
+ protected function format_errors()
+ {
+ $ret = $log = false;
+ switch (kolabformat::error()) {
+ case kolabformat.NoError:
+ $ret = false;
+ break;
+ case kolabformat.Warning:
+ $ret = false;
+ $log = "Warning";
+ break;
+ default:
+ $ret = true;
+ $log = "Error";
+ }
+
+ if ($log) {
+ raise_error(array(
+ 'code' => 660,
+ 'type' => 'php',
+ 'file' => __FILE__,
+ 'line' => __LINE__,
+ 'message' => "kolabformat write $log: " . kolabformat::errorMessage(),
+ ), true);
+ }
+
+ return $ret;
+ }
+
+ /**
+ * Save the last generated UID to the object properties.
+ * Should be called after kolabformat::writeXXXX();
+ */
+ protected function update_uid()
+ {
+ // get generated UID
+ if (!$this->data['uid']) {
+ $this->data['uid'] = kolabformat::getSerializedUID();
+ $this->obj->setUid($this->data['uid']);
+ }
+ }
+
+ /**
+ * Initialize libkolabxml object with cached xml data
+ */
+ protected function init()
+ {
+ if (!$this->loaded) {
+ if ($this->xmldata) {
+ $this->load($this->xmldata);
+ $this->xmldata = null;
+ }
+ $this->loaded = true;
+ }
+ }
+
+ /**
+ * Direct getter for object properties
+ */
+ public function __get($var)
+ {
+ return $this->data[$var];
+ }
+
+ /**
+ * Load Kolab object data from the given XML block
+ *
+ * @param string XML data
+ */
+ abstract public function load($xml);
+
+ /**
+ * Set properties to the kolabformat object
+ *
+ * @param array Object data as hash array
+ */
+ abstract public function set(&$object);
+
+ /**
+ *
+ */
+ abstract public function is_valid();
+
+ /**
+ * Write object data to XML format
+ *
+ * @return string XML data
+ */
+ abstract public function write();
+
+ /**
+ * Convert the Kolab object into a hash array data structure
+ *
+ * @return array Kolab object data as hash array
+ */
+ abstract public function to_array();
+
+ /**
+ * Load object data from Kolab2 format
+ *
+ * @param array Hash array with object properties (produced by Horde Kolab_Format classes)
+ */
+ abstract public function fromkolab2($object);
+
+ /**
+ * Callback for kolab_storage_cache to get object specific tags to cache
+ *
+ * @return array List of tags to save in cache
+ */
+ public function get_tags()
+ {
+ return array();
+ }
+
+ /**
+ * Callback for kolab_storage_cache to get words to index for fulltext search
+ *
+ * @return array List of words to save in cache
+ */
+ public function get_words()
+ {
+ return array();
+ }
+}
diff --git a/plugins/libkolab/lib/kolab_format_contact.php b/plugins/libkolab/lib/kolab_format_contact.php
new file mode 100644
index 00000000..d6da2352
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_format_contact.php
@@ -0,0 +1,532 @@
+<?php
+
+/**
+ * Kolab Contact model class
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli@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_format_contact extends kolab_format
+{
+ public $CTYPE = 'application/vcard+xml';
+
+ public static $fulltext_cols = array('name', 'firstname', 'surname', 'middlename', 'email');
+
+ public $phonetypes = array(
+ 'home' => Telephone::Home,
+ 'work' => Telephone::Work,
+ 'text' => Telephone::Text,
+ 'main' => Telephone::Voice,
+ 'homefax' => Telephone::Fax,
+ 'workfax' => Telephone::Fax,
+ 'mobile' => Telephone::Cell,
+ 'video' => Telephone::Video,
+ 'pager' => Telephone::Pager,
+ 'car' => Telephone::Car,
+ 'other' => Telephone::Textphone,
+ );
+
+ public $addresstypes = array(
+ 'home' => Address::Home,
+ 'work' => Address::Work,
+ 'office' => 0,
+ );
+
+ private $gendermap = array(
+ 'female' => Contact::Female,
+ 'male' => Contact::Male,
+ );
+
+ private $relatedmap = array(
+ 'manager' => Related::Manager,
+ 'assistant' => Related::Assistant,
+ 'spouse' => Related::Spouse,
+ 'children' => Related::Child,
+ );
+
+ // old Kolab 2 format field map
+ private $kolab2_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',
+ 'birthday' => 'birthday',
+ 'anniversary' => 'anniversary',
+ 'phone' => 'phone',
+ 'im-address' => 'im',
+ 'web-page' => 'website',
+ 'profession' => 'profession',
+ 'manager-name' => 'manager',
+ 'assistant' => 'assistant',
+ 'spouse-name' => 'spouse',
+ 'children' => 'children',
+ 'body' => 'notes',
+ 'pgp-publickey' => 'pgppublickey',
+ 'free-busy-url' => 'freebusyurl',
+ 'picture' => 'photo',
+ );
+ private $kolab2_phonetypes = array(
+ 'home1' => 'home',
+ 'business1' => 'work',
+ 'business2' => 'work',
+ 'businessfax' => 'workfax',
+ );
+ private $kolab2_addresstypes = array(
+ 'business' => 'work'
+ );
+ private $kolab2_gender = array(0 => 'male', 1 => 'female');
+
+
+ /**
+ * Default constructor
+ */
+ function __construct($xmldata = null)
+ {
+ $this->obj = new Contact;
+ $this->xmldata = $xmldata;
+
+ // complete phone types
+ $this->phonetypes['homefax'] |= Telephone::Home;
+ $this->phonetypes['workfax'] |= Telephone::Work;
+ }
+
+ /**
+ * Load Contact object data from the given XML block
+ *
+ * @param string XML data
+ */
+ public function load($xml)
+ {
+ $this->obj = kolabformat::readContact($xml, false);
+ $this->loaded = true;
+ }
+
+ /**
+ * Write Contact object data to XML format
+ *
+ * @return string XML data
+ */
+ public function write()
+ {
+ $this->init();
+ $this->xmldata = kolabformat::writeContact($this->obj);
+
+ if (!parent::format_errors())
+ parent::update_uid();
+ else
+ $this->xmldata = null;
+
+ return $this->xmldata;
+ }
+
+ /**
+ * Set contact properties to the kolabformat object
+ *
+ * @param array Contact data as hash array
+ */
+ public function set(&$object)
+ {
+ $this->init();
+
+ // set some automatic values if missing
+ if (false && !$this->obj->created()) {
+ if (!empty($object['created']))
+ $object['created'] = new DateTime('now', self::$timezone);
+ $this->obj->setCreated(self::get_datetime($object['created']));
+ }
+
+ if (!empty($object['uid']))
+ $this->obj->setUid($object['uid']);
+
+ // do the hard work of setting object values
+ $nc = new NameComponents;
+ $nc->setSurnames(self::array2vector($object['surname']));
+ $nc->setGiven(self::array2vector($object['firstname']));
+ $nc->setAdditional(self::array2vector($object['middlename']));
+ $nc->setPrefixes(self::array2vector($object['prefix']));
+ $nc->setSuffixes(self::array2vector($object['suffix']));
+ $this->obj->setNameComponents($nc);
+ $this->obj->setName($object['name']);
+
+ if (isset($object['nickname']))
+ $this->obj->setNickNames(self::array2vector($object['nickname']));
+ if (isset($object['profession']))
+ $this->obj->setTitles(self::array2vector($object['profession']));
+
+ // organisation related properties (affiliation)
+ $org = new Affiliation;
+ $offices = new vectoraddress;
+ if ($object['organization'])
+ $org->setOrganisation($object['organization']);
+ if ($object['department'])
+ $org->setOrganisationalUnits(self::array2vector($object['department']));
+ if ($object['jobtitle'])
+ $org->setRoles(self::array2vector($object['jobtitle']));
+
+ $rels = new vectorrelated;
+ if ($object['manager']) {
+ foreach ((array)$object['manager'] as $manager)
+ $rels->push(new Related(Related::Text, $manager, Related::Manager));
+ }
+ if ($object['assistant']) {
+ foreach ((array)$object['assistant'] as $assistant)
+ $rels->push(new Related(Related::Text, $assistant, Related::Assistant));
+ }
+ $org->setRelateds($rels);
+
+ // email, im, url
+ $this->obj->setEmailAddresses(self::array2vector($object['email']));
+ $this->obj->setIMaddresses(self::array2vector($object['im']));
+
+ $vurls = new vectorurl;
+ foreach ((array)$object['website'] as $url) {
+ $type = $url['type'] == 'blog' ? Url::Blog : Url::None;
+ $vurls->push(new Url($url['url'], $type));
+ }
+ $this->obj->setUrls($vurls);
+
+ // addresses
+ $adrs = new vectoraddress;
+ foreach ((array)$object['address'] as $address) {
+ $adr = new Address;
+ $type = $this->addresstypes[$address['type']];
+ if (isset($type))
+ $adr->setTypes($type);
+ else if ($address['type'])
+ $adr->setLabel($address['type']);
+ if ($address['street'])
+ $adr->setStreet($address['street']);
+ if ($address['locality'])
+ $adr->setLocality($address['locality']);
+ if ($address['code'])
+ $adr->setCode($address['code']);
+ if ($address['region'])
+ $adr->setRegion($address['region']);
+ if ($address['country'])
+ $adr->setCountry($address['country']);
+
+ if ($address['type'] == 'office')
+ $offices->push($adr);
+ else
+ $adrs->push($adr);
+ }
+ $this->obj->setAddresses($adrs);
+ $org->setAddresses($offices);
+
+ // add org affiliation after addresses are set
+ $orgs = new vectoraffiliation;
+ $orgs->push($org);
+ $this->obj->setAffiliations($orgs);
+
+ // telephones
+ $tels = new vectortelephone;
+ foreach ((array)$object['phone'] as $phone) {
+ $tel = new Telephone;
+ if (isset($this->phonetypes[$phone['type']]))
+ $tel->setTypes($this->phonetypes[$phone['type']]);
+ $tel->setNumber($phone['number']);
+ $tels->push($tel);
+ }
+ $this->obj->setTelephones($tels);
+
+ if (isset($object['gender']))
+ $this->obj->setGender($this->gendermap[$object['gender']] ? $this->gendermap[$object['gender']] : Contact::NotSet);
+ if (isset($object['notes']))
+ $this->obj->setNote($object['notes']);
+ if (isset($object['freebusyurl']))
+ $this->obj->setFreeBusyUrl($object['freebusyurl']);
+ if (isset($object['birthday']))
+ $this->obj->setBDay(self::get_datetime($object['birthday'], null, true));
+ if (isset($object['anniversary']))
+ $this->obj->setAnniversary(self::get_datetime($object['anniversary'], null, true));
+
+ if (!empty($object['photo'])) {
+ if ($type = rc_image_content_type($object['photo']))
+ $this->obj->setPhoto($object['photo'], $type);
+ }
+ else if (isset($object['photo']))
+ $this->obj->setPhoto('','');
+ else if ($this->obj->photoMimetype()) // load saved photo for caching
+ $object['photo'] = $this->obj->photo();
+
+ // spouse and children are relateds
+ $rels = new vectorrelated;
+ if ($object['spouse']) {
+ $rels->push(new Related(Related::Text, $object['spouse'], Related::Spouse));
+ }
+ if ($object['children']) {
+ foreach ((array)$object['children'] as $child)
+ $rels->push(new Related(Related::Text, $child, Related::Child));
+ }
+ $this->obj->setRelateds($rels);
+
+ // insert/replace crypto keys
+ $pgp_index = $pkcs7_index = -1;
+ $keys = $this->obj->keys();
+ for ($i=0; $i < $keys->size(); $i++) {
+ $key = $keys->get($i);
+ if ($pgp_index < 0 && $key->type() == Key::PGP)
+ $pgp_index = $i;
+ else if ($pkcs7_index < 0 && $key->type() == Key::PKCS7_MIME)
+ $pkcs7_index = $i;
+ }
+
+ $pgpkey = $object['pgppublickey'] ? new Key($object['pgppublickey'], Key::PGP) : new Key();
+ $pkcs7key = $object['pkcs7publickey'] ? new Key($object['pkcs7publickey'], Key::PKCS7_MIME) : new Key();
+
+ if ($pgp_index >= 0)
+ $keys->set($pgp_index, $pgpkey);
+ else if (!empty($object['pgppublickey']))
+ $keys->push($pgpkey);
+ if ($pkcs7_index >= 0)
+ $keys->set($pkcs7_index, $pkcs7key);
+ else if (!empty($object['pkcs7publickey']))
+ $keys->push($pkcs7key);
+
+ $this->obj->setKeys($keys);
+
+ // TODO: handle language, gpslocation, etc.
+
+
+ // cache this data
+ $this->data = $object;
+ unset($this->data['_formatobj']);
+ }
+
+ /**
+ *
+ */
+ public function is_valid()
+ {
+ return $this->data || (is_object($this->obj) && $this->obj->uid() /*$this->obj->isValid()*/);
+ }
+
+ /**
+ * Convert the Contact object into a hash array data structure
+ *
+ * @return array Contact data as hash array
+ */
+ public function to_array()
+ {
+ // return cached result
+ if (!empty($this->data))
+ return $this->data;
+
+ $this->init();
+
+ // read object properties into local data object
+ $object = array(
+ 'uid' => $this->obj->uid(),
+ # 'changed' => $this->obj->lastModified(),
+ 'name' => $this->obj->name(),
+ );
+
+ $nc = $this->obj->nameComponents();
+ $object['surname'] = join(' ', self::vector2array($nc->surnames()));
+ $object['firstname'] = join(' ', self::vector2array($nc->given()));
+ $object['middlename'] = join(' ', self::vector2array($nc->additional()));
+ $object['prefix'] = join(' ', self::vector2array($nc->prefixes()));
+ $object['suffix'] = join(' ', self::vector2array($nc->suffixes()));
+ $object['nickname'] = join(' ', self::vector2array($this->obj->nickNames()));
+ $object['profession'] = join(' ', self::vector2array($this->obj->titles()));
+
+ // organisation related properties (affiliation)
+ $orgs = $this->obj->affiliations();
+ if ($orgs->size()) {
+ $org = $orgs->get(0);
+ $object['organization'] = $org->organisation();
+ $object['jobtitle'] = join(' ', self::vector2array($org->roles()));
+ $object['department'] = join(' ', self::vector2array($org->organisationalUnits()));
+ $this->read_relateds($org->relateds(), $object);
+ }
+
+ $object['email'] = self::vector2array($this->obj->emailAddresses());
+ $object['im'] = self::vector2array($this->obj->imAddresses());
+
+ $urls = $this->obj->urls();
+ for ($i=0; $i < $urls->size(); $i++) {
+ $url = $urls->get($i);
+ $subtype = $url->type() == Url::Blog ? 'blog' : 'homepage';
+ $object['website'][] = array('url' => $url->url(), 'type' => $subtype);
+ }
+
+ // addresses
+ $this->read_addresses($this->obj->addresses(), $object);
+ if ($org && ($offices = $org->addresses()))
+ $this->read_addresses($offices, $object, 'office');
+
+ // telehones
+ $tels = $this->obj->telephones();
+ $teltypes = array_flip($this->phonetypes);
+ for ($i=0; $i < $tels->size(); $i++) {
+ $tel = $tels->get($i);
+ $object['phone'][] = array('number' => $tel->number(), 'type' => $teltypes[$tel->types()]);
+ }
+
+ $object['notes'] = $this->obj->note();
+ $object['freebusyurl'] = $this->obj->freeBusyUrl();
+
+ if ($bday = self::php_datetime($this->obj->bDay()))
+ $object['birthday'] = $bday->format('c');
+
+ if ($anniversary = self::php_datetime($this->obj->anniversary()))
+ $object['anniversary'] = $anniversary->format('c');
+
+ $gendermap = array_flip($this->gendermap);
+ if (($g = $this->obj->gender()) && $gendermap[$g])
+ $object['gender'] = $gendermap[$g];
+
+ if ($this->obj->photoMimetype())
+ $object['photo'] = $this->obj->photo();
+
+ // relateds -> spouse, children
+ $this->read_relateds($this->obj->relateds(), $object);
+
+ // crypto settings: currently only key values are supported
+ $keys = $this->obj->keys();
+ for ($i=0; is_object($keys) && $i < $keys->size(); $i++) {
+ $key = $keys->get($i);
+ if ($key->type() == Key::PGP)
+ $object['pgppublickey'] = $key->key();
+ else if ($key->type() == Key::PKCS7_MIME)
+ $object['pkcs7publickey'] = $key->key();
+ }
+
+ $this->data = $object;
+ return $this->data;
+ }
+
+ /**
+ * Callback for kolab_storage_cache to get words to index for fulltext search
+ *
+ * @return array List of words to save in cache
+ */
+ public function get_words()
+ {
+ $data = '';
+ foreach (self::$fulltext_cols as $col) {
+ $val = is_array($this->data[$col]) ? join(' ', $this->data[$col]) : $this->data[$col];
+ if (strlen($val))
+ $data .= $val . ' ';
+ }
+
+ return array_unique(rcube_utils::normalize_string($data, true));
+ }
+
+ /**
+ * Load data from old Kolab2 format
+ *
+ * @param array Hash array with object properties
+ */
+ public function fromkolab2($record)
+ {
+ $object = array(
+ 'uid' => $record['uid'],
+ 'email' => array(),
+ 'phone' => array(),
+ );
+
+ foreach ($this->kolab2_fieldmap as $kolab => $rcube) {
+ if (is_array($record[$kolab]) || strlen($record[$kolab]))
+ $object[$rcube] = $record[$kolab];
+ }
+
+ if (isset($record['gender']))
+ $object['gender'] = $this->kolab2_gender[$record['gender']];
+
+ foreach ((array)$record['email'] as $i => $email)
+ $object['email'][] = $email['smtp-address'];
+
+ if (!$record['email'] && $record['emails'])
+ $object['email'] = preg_split('/,\s*/', $record['emails']);
+
+ if (is_array($record['address'])) {
+ foreach ($record['address'] as $i => $adr) {
+ $object['address'][] = array(
+ 'type' => $this->kolab2_addresstypes[$adr['type']] ? $this->kolab2_addresstypes[$adr['type']] : $adr['type'],
+ 'street' => $adr['street'],
+ 'locality' => $adr['locality'],
+ 'code' => $adr['postal-code'],
+ 'region' => $adr['region'],
+ 'country' => $adr['country'],
+ );
+ }
+ }
+
+ // office location goes into an address block
+ if ($record['office-location'])
+ $object['address'][] = array('type' => 'office', 'locality' => $record['office-location']);
+
+ // merge initials into nickname
+ if ($record['initials'])
+ $object['nickname'] = trim($object['nickname'] . ', ' . $record['initials'], ', ');
+
+ // remove empty fields
+ $this->data = array_filter($object);
+ }
+
+ /**
+ * Helper method to copy contents of an Address vector to the contact data object
+ */
+ private function read_addresses($addresses, &$object, $type = null)
+ {
+ $adrtypes = array_flip($this->addresstypes);
+
+ for ($i=0; $i < $addresses->size(); $i++) {
+ $adr = $addresses->get($i);
+ $object['address'][] = array(
+ 'type' => $type ? $type : ($adrtypes[$adr->types()] ? $adrtypes[$adr->types()] : ''), /*$adr->label()),*/
+ 'street' => $adr->street(),
+ 'code' => $adr->code(),
+ 'locality' => $adr->locality(),
+ 'region' => $adr->region(),
+ 'country' => $adr->country()
+ );
+ }
+ }
+
+ /**
+ * Helper method to map contents of a Related vector to the contact data object
+ */
+ private function read_relateds($rels, &$object)
+ {
+ $typemap = array_flip($this->relatedmap);
+
+ for ($i=0; $i < $rels->size(); $i++) {
+ $rel = $rels->get($i);
+ if ($rel->type() != Related::Text) // we can't handle UID relations yet
+ continue;
+
+ $types = $rel->relationTypes();
+ foreach ($typemap as $t => $field) {
+ if ($types & $t) {
+ $object[$field][] = $rel->text();
+ break;
+ }
+ }
+ }
+ }
+}
diff --git a/plugins/libkolab/lib/kolab_format_distributionlist.php b/plugins/libkolab/lib/kolab_format_distributionlist.php
new file mode 100644
index 00000000..592387e2
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_format_distributionlist.php
@@ -0,0 +1,160 @@
+<?php
+
+/**
+ * Kolab Distribution List model class
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli@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_format_distributionlist extends kolab_format
+{
+ public $CTYPE = 'application/vcard+xml';
+
+ function __construct($xmldata = null)
+ {
+ $this->obj = new DistList;
+ $this->xmldata = $xmldata;
+ }
+
+ /**
+ * Load Kolab object data from the given XML block
+ *
+ * @param string XML data
+ */
+ public function load($xml)
+ {
+ $this->obj = kolabformat::readDistlist($xml, false);
+ $this->loaded = true;
+ }
+
+ /**
+ * Write object data to XML format
+ *
+ * @return string XML data
+ */
+ public function write()
+ {
+ $this->init();
+ $this->xmldata = kolabformat::writeDistlist($this->obj);
+
+ if (!parent::format_errors())
+ parent::update_uid();
+ else
+ $this->xmldata = null;
+
+ return $this->xmldata;
+ }
+
+ public function set(&$object)
+ {
+ $this->init();
+
+ // set some automatic values if missing
+ if (!empty($object['uid']))
+ $this->obj->setUid($object['uid']);
+
+ $this->obj->setName($object['name']);
+
+ $seen = array();
+ $members = new vectorcontactref;
+ foreach ($object['member'] as $member) {
+ if ($member['uid'])
+ $m = new ContactReference(ContactReference::UidReference, $member['uid']);
+ else if ($member['email'])
+ $m = new ContactReference(ContactReference::EmailReference, $member['email']);
+ else
+ continue;
+
+ $m->setName($member['name']);
+ $members->push($m);
+ $seen[$member['email']]++;
+ }
+
+ $this->obj->setMembers($members);
+
+ // cache this data
+ $this->data = $object;
+ unset($this->data['_formatobj']);
+ }
+
+ public function is_valid()
+ {
+ return $this->data || (is_object($this->obj) && $this->obj->isValid());
+ }
+
+ /**
+ * Load data from old Kolab2 format
+ */
+ public function fromkolab2($record)
+ {
+ $object = array(
+ 'uid' => $record['uid'],
+ 'changed' => $record['last-modification-date'],
+ 'name' => $record['last-name'],
+ 'member' => array(),
+ );
+
+ foreach ($record['member'] as $member) {
+ $object['member'][] = array(
+ 'email' => $member['smtp-address'],
+ 'name' => $member['display-name'],
+ 'uid' => $member['uid'],
+ );
+ }
+
+ $this->data = $object;
+ }
+
+ /**
+ * Convert the Distlist object into a hash array data structure
+ *
+ * @return array Distribution list data as hash array
+ */
+ public function to_array()
+ {
+ // return cached result
+ if (!empty($this->data))
+ return $this->data;
+
+ $this->init();
+
+ // read object properties
+ $object = array(
+ 'uid' => $this->obj->uid(),
+# 'changed' => $this->obj->lastModified(),
+ 'name' => $this->obj->name(),
+ 'member' => array(),
+ );
+
+ $members = $this->obj->members();
+ for ($i=0; $i < $members->size(); $i++) {
+ $member = $members->get($i);
+# if ($member->type() == ContactReference::UidReference && ($uid = $member->uid()))
+ $object['member'][] = array(
+ 'uid' => $member->uid(),
+ 'email' => $member->email(),
+ 'name' => $member->name(),
+ );
+ }
+
+ $this->data = $object;
+ return $this->data;
+ }
+
+}
diff --git a/plugins/libkolab/lib/kolab_format_event.php b/plugins/libkolab/lib/kolab_format_event.php
new file mode 100644
index 00000000..699cfb81
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_format_event.php
@@ -0,0 +1,638 @@
+<?php
+
+/**
+ * Kolab Event model class
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli@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_format_event extends kolab_format
+{
+ public $CTYPE = 'application/calendar+xml';
+
+ public static $fulltext_cols = array('title', 'description', 'location', 'attendees:name', 'attendees:email');
+
+ private $sensitivity_map = array(
+ 'public' => kolabformat::ClassPublic,
+ 'private' => kolabformat::ClassPrivate,
+ 'confidential' => kolabformat::ClassConfidential,
+ );
+
+ private $role_map = array(
+ 'REQ-PARTICIPANT' => kolabformat::Required,
+ 'OPT-PARTICIPANT' => kolabformat::Optional,
+ 'NON-PARTICIPANT' => kolabformat::NonParticipant,
+ 'CHAIR' => kolabformat::Chair,
+ );
+
+ private $rrule_type_map = array(
+ 'MINUTELY' => RecurrenceRule::Minutely,
+ 'HOURLY' => RecurrenceRule::Hourly,
+ 'DAILY' => RecurrenceRule::Daily,
+ 'WEEKLY' => RecurrenceRule::Weekly,
+ 'MONTHLY' => RecurrenceRule::Monthly,
+ 'YEARLY' => RecurrenceRule::Yearly,
+ );
+
+ private $weekday_map = array(
+ 'MO' => kolabformat::Monday,
+ 'TU' => kolabformat::Tuesday,
+ 'WE' => kolabformat::Wednesday,
+ 'TH' => kolabformat::Thursday,
+ 'FR' => kolabformat::Friday,
+ 'SA' => kolabformat::Saturday,
+ 'SU' => kolabformat::Sunday,
+ );
+
+ private $alarm_type_map = array(
+ 'DISPLAY' => Alarm::DisplayAlarm,
+ 'EMAIL' => Alarm::EMailAlarm,
+ 'AUDIO' => Alarm::AudioAlarm,
+ );
+
+ private $status_map = array(
+ 'UNKNOWN' => kolabformat::PartNeedsAction,
+ 'NEEDS-ACTION' => kolabformat::PartNeedsAction,
+ 'TENTATIVE' => kolabformat::PartTentative,
+ 'ACCEPTED' => kolabformat::PartAccepted,
+ 'DECLINED' => kolabformat::PartDeclined,
+ 'DELEGATED' => kolabformat::PartDelegated,
+ );
+
+ private $kolab2_rolemap = array(
+ 'required' => 'REQ-PARTICIPANT',
+ 'optional' => 'OPT-PARTICIPANT',
+ 'resource' => 'CHAIR',
+ );
+ private $kolab2_statusmap = array(
+ 'none' => 'NEEDS-ACTION',
+ 'tentative' => 'TENTATIVE',
+ 'accepted' => 'CONFIRMED',
+ 'accepted' => 'ACCEPTED',
+ 'declined' => 'DECLINED',
+ );
+ private $kolab2_monthmap = array('', 'january', 'february', 'march', 'april', 'may', 'june', 'july', 'august', 'september', 'october', 'november', 'december');
+
+
+ /**
+ * Default constructor
+ */
+ function __construct($xmldata = null)
+ {
+ $this->obj = new Event;
+ $this->xmldata = $xmldata;
+ }
+
+ /**
+ * Load Contact object data from the given XML block
+ *
+ * @param string XML data
+ */
+ public function load($xml)
+ {
+ $this->obj = kolabformat::readEvent($xml, false);
+ $this->loaded = true;
+ }
+
+ /**
+ * Write Contact object data to XML format
+ *
+ * @return string XML data
+ */
+ public function write()
+ {
+ $this->init();
+ $this->xmldata = kolabformat::writeEvent($this->obj);
+
+ if (!parent::format_errors())
+ parent::update_uid();
+ else
+ $this->xmldata = null;
+
+ return $this->xmldata;
+ }
+
+ /**
+ * Set contact properties to the kolabformat object
+ *
+ * @param array Contact data as hash array
+ */
+ public function set(&$object)
+ {
+ $this->init();
+
+ // set some automatic values if missing
+ if (!$this->obj->created()) {
+ if (!empty($object['created']))
+ $object['created'] = new DateTime('now', self::$timezone);
+ $this->obj->setCreated(self::get_datetime($object['created']));
+ }
+
+ if (!empty($object['uid']))
+ $this->obj->setUid($object['uid']);
+
+ // increment sequence
+ $this->obj->setSequence($this->obj->sequence()+1);
+
+ // do the hard work of setting object values
+ $this->obj->setStart(self::get_datetime($object['start'], null, $object['allday']));
+ $this->obj->setEnd(self::get_datetime($object['end'], null, $object['allday']));
+ $this->obj->setSummary($object['title']);
+ $this->obj->setLocation($object['location']);
+ $this->obj->setDescription($object['description']);
+ $this->obj->setPriority($object['priority']);
+ $this->obj->setClassification($this->sensitivity_map[$object['sensitivity']]);
+ $this->obj->setCategories(self::array2vector($object['categories']));
+ $this->obj->setTransparency($object['free_busy'] == 'free');
+
+ $status = kolabformat::StatusUndefined;
+ if ($object['free_busy'] == 'tentative')
+ $status = kolabformat::StatusTentative;
+ if ($object['cancelled'])
+ $status = kolabformat::StatusCancelled;
+ $this->obj->setStatus($status);
+
+ // process event attendees
+ $organizer = new ContactReference;
+ $attendees = new vectorattendee;
+ foreach ((array)$object['attendees'] as $attendee) {
+ $cr = new ContactReference(ContactReference::EmailReference, $attendee['email']);
+ $cr->setName($attendee['name']);
+
+ if ($attendee['role'] == 'ORGANIZER') {
+ $organizer = $cr;
+ }
+ else {
+ $att = new Attendee;
+ $att->setContact($cr);
+ $att->setPartStat($this->status_map[$attendee['status']]);
+ $att->setRole($this->role_map[$attendee['role']] ? $this->role_map[$attendee['role']] : kolabformat::Required);
+ $att->setRSVP((bool)$attendee['rsvp']);
+
+ if ($att->isValid()) {
+ $attendees->push($att);
+ }
+ else {
+ raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Invalid event attendee: " . json_encode($attendee),
+ ), true);
+ }
+ }
+ }
+ $this->obj->setOrganizer($organizer);
+ $this->obj->setAttendees($attendees);
+
+ // save recurrence rule
+ if ($object['recurrence']) {
+ $rr = new RecurrenceRule;
+ $rr->setFrequency($this->rrule_type_map[$object['recurrence']['FREQ']]);
+
+ if ($object['recurrence']['INTERVAL'])
+ $rr->setInterval(intval($object['recurrence']['INTERVAL']));
+
+ if ($object['recurrence']['BYDAY']) {
+ $byday = new vectordaypos;
+ foreach (explode(',', $object['recurrence']['BYDAY']) as $day) {
+ $occurrence = 0;
+ if (preg_match('/^([\d-]+)([A-Z]+)$/', $day, $m)) {
+ $occurrence = intval($m[1]);
+ $day = $m[2];
+ }
+ if (isset($this->weekday_map[$day]))
+ $byday->push(new DayPos($occurrence, $this->weekday_map[$day]));
+ }
+ $rr->setByday($byday);
+ }
+
+ if ($object['recurrence']['BYMONTHDAY']) {
+ $bymday = new vectori;
+ foreach (explode(',', $object['recurrence']['BYMONTHDAY']) as $day)
+ $bymday->push(intval($day));
+ $rr->setBymonthday($bymday);
+ }
+
+ if ($object['recurrence']['BYMONTH']) {
+ $bymonth = new vectori;
+ foreach (explode(',', $object['recurrence']['BYMONTH']) as $month)
+ $bymonth->push(intval($month));
+ $rr->setBymonth($bymonth);
+ }
+
+ if ($object['recurrence']['COUNT'])
+ $rr->setCount(intval($object['recurrence']['COUNT']));
+ else if ($object['recurrence']['UNTIL'])
+ $rr->setEnd(self::get_datetime($object['recurrence']['UNTIL'], null, true));
+
+ if ($rr->isValid()) {
+ $this->obj->setRecurrenceRule($rr);
+
+ // add exception dates (only if recurrence rule is valid)
+ $exdates = new vectordatetime;
+ foreach ((array)$object['recurrence']['EXDATE'] as $exdate)
+ $exdates->push(self::get_datetime($exdate, null, true));
+ $this->obj->setExceptionDates($exdates);
+ }
+ else {
+ raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Invalid event recurrence rule: " . json_encode($object['recurrence']),
+ ), true);
+ }
+ }
+
+ // save alarm
+ $valarms = new vectoralarm;
+ if ($object['alarms']) {
+ list($offset, $type) = explode(":", $object['alarms']);
+
+ if ($type == 'EMAIL') { // email alarms implicitly go to event owner
+ $recipients = new vectorcontactref;
+ $recipients->push(new ContactReference(ContactReference::EmailReference, $object['_owner']));
+ $alarm = new Alarm($object['title'], strval($object['description']), $recipients);
+ }
+ else { // default: display alarm
+ $alarm = new Alarm($object['title']);
+ }
+
+ if (preg_match('/^@(\d+)/', $offset, $d)) {
+ $alarm->setStart(self::get_datetime($d[1], new DateTimeZone('UTC')));
+ }
+ else if (preg_match('/^([-+]?)(\d+)([SMHDW])/', $offset, $d)) {
+ $days = $hours = $minutes = $seconds = 0;
+ switch ($d[3]) {
+ case 'W': $days = 7*intval($d[2]); break;
+ case 'D': $days = intval($d[2]); break;
+ case 'H': $hours = intval($d[2]); break;
+ case 'M': $minutes = intval($d[2]); break;
+ case 'S': $seconds = intval($d[2]); break;
+ }
+ $alarm->setRelativeStart(new Duration($days, $hours, $minutes, $seconds, $d[1] == '-'), $d[1] == '-' ? kolabformat::Start : kolabformat::End);
+ }
+
+ $valarms->push($alarm);
+ }
+ $this->obj->setAlarms($valarms);
+
+ // save attachments
+ $vattach = new vectorattachment;
+ foreach ((array)$object['_attachments'] as $name => $attr) {
+ if (empty($attr))
+ continue;
+ $attach = new Attachment;
+ $attach->setLabel($name);
+ $attach->setUri('cid:' . $name, $attr['mimetype']);
+ $vattach->push($attach);
+ }
+ $this->obj->setAttachments($vattach);
+
+ // cache this data
+ $this->data = $object;
+ unset($this->data['_formatobj']);
+ }
+
+ /**
+ *
+ */
+ public function is_valid()
+ {
+ return $this->data || (is_object($this->obj) && $this->obj->isValid() && $this->obj->uid());
+ }
+
+ /**
+ * Convert the Contact object into a hash array data structure
+ *
+ * @return array Contact data as hash array
+ */
+ public function to_array()
+ {
+ // return cached result
+ if (!empty($this->data))
+ return $this->data;
+
+ $this->init();
+
+ $sensitivity_map = array_flip($this->sensitivity_map);
+
+ // read object properties
+ $object = array(
+ 'uid' => $this->obj->uid(),
+ 'changed' => self::php_datetime($this->obj->lastModified()),
+ 'title' => $this->obj->summary(),
+ 'location' => $this->obj->location(),
+ 'description' => $this->obj->description(),
+ 'allday' => $this->obj->start()->isDateOnly(),
+ 'start' => self::php_datetime($this->obj->start()),
+ 'end' => self::php_datetime($this->obj->end()),
+ 'categories' => self::vector2array($this->obj->categories()),
+ 'free_busy' => $this->obj->transparency() ? 'free' : 'busy', // TODO: transparency is only boolean
+ 'sensitivity' => $sensitivity_map[$this->obj->classification()],
+ 'priority' => $this->obj->priority(),
+ );
+
+ // status defines different event properties...
+ $status = $this->obj->status();
+ if ($status == kolabformat::StatusTentative)
+ $object['free_busy'] = 'tentative';
+ else if ($status == kolabformat::StatusCancelled)
+ $objec['cancelled'] = true;
+
+ // read organizer and attendees
+ if ($organizer = $this->obj->organizer()) {
+ $object['attendees'][] = array(
+ 'role' => 'ORGANIZER',
+ 'email' => $organizer->email(),
+ 'name' => $organizer->name(),
+ );
+ }
+
+ $role_map = array_flip($this->role_map);
+ $status_map = array_flip($this->status_map);
+ $attvec = $this->obj->attendees();
+ for ($i=0; $i < $attvec->size(); $i++) {
+ $attendee = $attvec->get($i);
+ $cr = $attendee->contact();
+ $object['attendees'][] = array(
+ 'role' => $role_map[$attendee->role()],
+ 'status' => $status_map[$attendee->partStat()],
+ 'rsvp' => $attendee->rsvp(),
+ 'email' => $cr->email(),
+ 'name' => $cr->name(),
+ );
+ }
+
+ // read recurrence rule
+ if (($rr = $this->obj->recurrenceRule()) && $rr->isValid()) {
+ $rrule_type_map = array_flip($this->rrule_type_map);
+ $object['recurrence'] = array('FREQ' => $rrule_type_map[$rr->frequency()]);
+
+ if ($intvl = $rr->interval())
+ $object['recurrence']['INTERVAL'] = $intvl;
+
+ if (($count = $rr->count()) && $count > 0) {
+ $object['recurrence']['COUNT'] = $count;
+ }
+ else if ($until = self::php_datetime($rr->end())) {
+ $until->setTime($object['start']->format('G'), $object['start']->format('i'), 0);
+ $object['recurrence']['UNTIL'] = $until->format('U');
+ }
+
+ if (($byday = $rr->byday()) && $byday->size()) {
+ $weekday_map = array_flip($this->weekday_map);
+ $weekdays = array();
+ for ($i=0; $i < $byday->size(); $i++) {
+ $daypos = $byday->get($i);
+ $prefix = $daypos->occurence();
+ $weekdays[] = ($prefix ? $prefix : '') . $weekday_map[$daypos->weekday()];
+ }
+ $object['recurrence']['BYDAY'] = join(',', $weekdays);
+ }
+
+ if (($bymday = $rr->bymonthday()) && $bymday->size()) {
+ $object['recurrence']['BYMONTHDAY'] = join(',', self::vector2array($bymday));
+ }
+
+ if (($bymonth = $rr->bymonth()) && $bymonth->size()) {
+ $object['recurrence']['BYMONTH'] = join(',', self::vector2array($bymonth));
+ }
+
+ if ($exceptions = $this->obj->exceptionDates()) {
+ for ($i=0; $i < $exceptions->size(); $i++) {
+ if ($exdate = self::php_datetime($exceptions->get($i)))
+ $object['recurrence']['EXDATE'][] = $exdate->format('U');
+ }
+ }
+ }
+
+ // read alarm
+ $valarms = $this->obj->alarms();
+ $alarm_types = array_flip($this->alarm_type_map);
+ for ($i=0; $i < $valarms->size(); $i++) {
+ $alarm = $valarms->get($i);
+ $type = $alarm_types[$alarm->type()];
+
+ if ($type == 'DISPLAY' || $type == 'EMAIL') { // only DISPLAY and EMAIL alarms are supported
+ if ($start = self::php_datetime($alarm->start())) {
+ $object['alarms'] = '@' . $start->format('U');
+ }
+ else if ($offset = $alarm->relativeStart()) {
+ $value = $alarm->relativeTo() == kolabformat::End ? '+' : '-';
+ if ($w = $offset->weeks()) $value .= $w . 'W';
+ else if ($d = $offset->days()) $value .= $d . 'D';
+ else if ($h = $offset->hours()) $value .= $h . 'H';
+ else if ($m = $offset->minutes()) $value .= $m . 'M';
+ else if ($s = $offset->seconds()) $value .= $s . 'S';
+ else continue;
+
+ $object['alarms'] = $value;
+ }
+ $object['alarms'] .= ':' . $type;
+ break;
+ }
+ }
+
+ // handle attachments
+ $vattach = $this->obj->attachments();
+ for ($i=0; $i < $vattach->size(); $i++) {
+ $attach = $vattach->get($i);
+
+ // skip cid: attachments which are mime message parts handled by kolab_storage_folder
+ if (substr($attach->uri(), 0, 4) != 'cid') {
+ $name = $attach->label();
+ $data = $attach->data();
+ $object['_attachments'][$name] = array(
+ 'mimetype' => $attach->mimetype(),
+ 'size' => strlen($data),
+ 'content' => $data,
+ );
+ }
+ }
+
+ $this->data = $object;
+ return $this->data;
+ }
+
+ /**
+ * Callback for kolab_storage_cache to get object specific tags to cache
+ *
+ * @return array List of tags to save in cache
+ */
+ public function get_tags()
+ {
+ $tags = array();
+
+ foreach ((array)$this->data['categories'] as $cat) {
+ $tags[] = rcube_utils::normalize_string($cat);
+ }
+
+ if (!empty($this->data['alarms'])) {
+ $tags[] = 'x-has-alarms';
+ }
+
+ return $tags;
+ }
+
+ /**
+ * Callback for kolab_storage_cache to get words to index for fulltext search
+ *
+ * @return array List of words to save in cache
+ */
+ public function get_words()
+ {
+ $data = '';
+ foreach (self::$fulltext_cols as $colname) {
+ list($col, $field) = explode(':', $colname);
+
+ if ($field) {
+ $a = array();
+ foreach ((array)$this->data[$col] as $attr)
+ $a[] = $attr[$field];
+ $val = join(' ', $a);
+ }
+ else {
+ $val = is_array($this->data[$col]) ? join(' ', $this->data[$col]) : $this->data[$col];
+ }
+
+ if (strlen($val))
+ $data .= $val . ' ';
+ }
+
+ return array_unique(rcube_utils::normalize_string($data, true));
+ }
+
+ /**
+ * Load data from old Kolab2 format
+ */
+ public function fromkolab2($rec)
+ {
+ if (PEAR::isError($rec))
+ return;
+
+ $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']));
+
+ // in Roundcube all-day events go from 12:00 to 13:00
+ if ($allday) {
+ $now = new DateTime('now', self::$timezone);
+ $gmt_offset = $now->getOffset();
+
+ $rec['start-date'] += 12 * 3600;
+ $rec['end-date'] -= 11 * 3600;
+ $rec['end-date'] -= $gmt_offset - date('Z', $rec['end-date']); // shift times from server's timezone to user's timezone
+ $rec['start-date'] -= $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->kolab2_monthmap);
+ $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
+ }
+ }
+
+ $attendees = array();
+ 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' => $this->kolab2_rolemap[$attendee['role']],
+ 'name' => $attendee['display-name'],
+ 'email' => $attendee['smtp-address'],
+ 'status' => $this->kolab2_statusmap[$attendee['status']],
+ 'rsvp' => $attendee['request-response'],
+ );
+ $_attendees .= $rec['organizer']['display-name'] . ' ' . $rec['organizer']['smtp-address'] . ' ';
+ }
+
+ $this->data = array(
+ '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,
+ 'categories' => explode(',', $rec['categories']),
+ 'attachments' => $attachments,
+ 'attendees' => $attendees,
+ 'free_busy' => $rec['show-time-as'],
+ 'priority' => $rec['priority'],
+ 'sensitivity' => $rec['sensitivity'],
+ 'changed' => $rec['last-modification-date'],
+ );
+ }
+}
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
new file mode 100644
index 00000000..5924530d
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -0,0 +1,462 @@
+<?php
+
+/**
+ * Kolab storage class providing static methods to access groupware objects on a Kolab server.
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli@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_storage
+{
+ const CTYPE_KEY = '/shared/vendor/kolab/folder-type';
+ const SERVERSIDE_SUBSCRIPTION = 0;
+ const CLIENTSIDE_SUBSCRIPTION = 1;
+
+ public static $last_error;
+
+ private static $ready = false;
+ private static $config;
+ private static $cache;
+ private static $imap;
+
+
+ /**
+ * Setup the environment needed by the libs
+ */
+ public static function setup()
+ {
+ if (self::$ready)
+ return true;
+
+ $rcmail = rcube::get_instance();
+ self::$config = $rcmail->config;
+ self::$imap = $rcmail->get_storage();
+ self::$ready = class_exists('kolabformat') &&
+ (self::$imap->get_capability('METADATA') || self::$imap->get_capability('ANNOTATEMORE') || self::$imap->get_capability('ANNOTATEMORE2'));
+
+ if (self::$ready) {
+ // set imap options
+ self::$imap->set_options(array(
+ 'skip_deleted' => true,
+ 'threading' => false,
+ ));
+ self::$imap->set_pagesize(9999);
+ }
+
+ return self::$ready;
+ }
+
+
+ /**
+ * Get a list of storage folders for the given data type
+ *
+ * @param string Data type to list folders for (contact,distribution-list,event,task,note)
+ *
+ * @return array List of Kolab_Folder objects (folder names in UTF7-IMAP)
+ */
+ public static function get_folders($type)
+ {
+ $folders = $folderdata = array();
+
+ if (self::setup()) {
+ foreach ((array)self::list_folders('', '*', $type, false, $folderdata) as $foldername) {
+ $folders[$foldername] = new kolab_storage_folder($foldername, $folderdata[$foldername]);
+ }
+ }
+
+ return $folders;
+ }
+
+
+ /**
+ * Getter for a specific storage folder
+ *
+ * @param string IMAP folder to access (UTF7-IMAP)
+ * @return object kolab_storage_folder The folder object
+ */
+ public static function get_folder($folder)
+ {
+ return self::setup() ? new kolab_storage_folder($folder) : null;
+ }
+
+
+ /**
+ * Getter for a single Kolab object, identified by its UID.
+ * This will search all folders storing objects of the given type.
+ *
+ * @param string Object UID
+ * @param string Object type (contact,distribution-list,event,task,note)
+ * @return array The Kolab object represented as hash array or false if not found
+ */
+ public static function get_object($uid, $type)
+ {
+ self::setup();
+ $folder = null;
+ foreach ((array)self::list_folders('', '*', $type) as $foldername) {
+ if (!$folder)
+ $folder = new kolab_storage_folder($foldername);
+ else
+ $folder->set_folder($foldername);
+
+ if ($object = $folder->get_object($uid))
+ return $object;
+ }
+
+ return false;
+ }
+
+
+ /**
+ *
+ */
+ public static function get_freebusy_server()
+ {
+ return unslashify(self::$config->get('kolab_freebusy_server', 'https://' . $_SESSION['imap_host'] . '/freebusy'));
+ }
+
+
+ /**
+ * Compose an URL to query the free/busy status for the given user
+ */
+ public static function get_freebusy_url($email)
+ {
+ return self::get_freebusy_server() . '/' . $email . '.ifb';
+ }
+
+
+ /**
+ * 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)
+ {
+ self::setup();
+
+ $success = self::$imap->delete_folder($name);
+ self::$last_error = self::$imap->get_error_str();
+
+ return $success;
+ }
+
+ /**
+ * Creates IMAP folder
+ *
+ * @param string $name Folder name (UTF7-IMAP)
+ * @param string $type Folder type
+ * @param bool $subscribed Sets folder subscription
+ *
+ * @return bool True on success, false on failure
+ */
+ public static function folder_create($name, $type = null, $subscribed = false)
+ {
+ self::setup();
+
+ if ($saved = self::$imap->create_folder($name, $subscribed)) {
+ // set metadata for folder type
+ if ($type) {
+ $saved = self::$imap->set_metadata($name, array(self::CTYPE_KEY => $type));
+
+ // revert if metadata could not be set
+ if (!$saved) {
+ self::$imap->delete_folder($name);
+ }
+ }
+ }
+
+ if ($saved) {
+ return true;
+ }
+
+ self::$last_error = self::$imap->get_error_str();
+ return false;
+ }
+
+ /**
+ * 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)
+ {
+ self::setup();
+
+ $success = self::$imap->rename_folder($oldname, $newname);
+ self::$last_error = self::$imap->get_error_str();
+
+ return $success;
+ }
+
+
+ /**
+ * 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)
+ {
+ self::setup();
+
+ $found = false;
+ $namespace = self::$imap->get_namespace();
+
+ 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 = self::$imap->get_hierarchy_delimiter();
+
+ $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;
+ }
+
+ /**
+ * 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 = '')
+ {
+ // get all folders of specified type
+ $folders = self::get_folders($type);
+
+ $delim = self::$imap->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->get_owner() != $_SESSION['username']) {
+ $rights = $c_folder->get_myrights();
+ if (!preg_match('/[ck]/', $rights)) {
+ continue;
+ }
+ }
+
+ $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');
+ }
+
+ // 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;
+ }
+
+
+ /**
+ * Returns a list of folder names
+ *
+ * @param string Optional root folder
+ * @param string Optional name pattern
+ * @param string Data type to list folders for (contact,distribution-list,event,task,note,mail)
+ * @param string Enable to return subscribed folders only
+ * @param array Will be filled with folder-types data
+ *
+ * @return array List of folders
+ */
+ public static function list_folders($root = '', $mbox = '*', $filter = null, $subscribed = false, &$folderdata = array())
+ {
+ if (!self::setup()) {
+ return null;
+ }
+
+ if (!$filter) {
+ // Get ALL folders list, standard way
+ if ($subscribed) {
+ return self::$imap->list_folders_subscribed($root, $mbox);
+ }
+ else {
+ return self::$imap->list_folders($root, $mbox);
+ }
+ }
+
+ $prefix = $root . $mbox;
+
+ // get folders types
+ $folderdata = self::$imap->get_metadata($prefix, self::CTYPE_KEY);
+
+ if (!is_array($folderdata)) {
+ return array();
+ }
+
+ $folderdata = array_map('implode', $folderdata);
+ $regexp = '/^' . preg_quote($filter, '/') . '(\..+)?$/';
+
+ // In some conditions we can skip LIST command (?)
+ if ($subscribed == false && $filter != 'mail' && $prefix == '*') {
+ foreach ($folderdata as $folder => $type) {
+ if (!preg_match($regexp, $type)) {
+ unset($folderdata[$folder]);
+ }
+ }
+ return array_keys($folderdata);
+ }
+
+ // Get folders list
+ if ($subscribed) {
+ $folders = self::$imap->list_folders_subscribed_direct($root, $mbox);
+ }
+ else {
+ $folders = self::$imap->list_folders_direct($root, $mbox);
+ }
+
+ // In case of an error, return empty list (?)
+ if (!is_array($folders)) {
+ return array();
+ }
+
+ // Filter folders list
+ foreach ($folders as $idx => $folder) {
+ $type = $folderdata[$folder];
+
+ if ($filter == 'mail' && empty($type)) {
+ continue;
+ }
+ if (empty($type) || !preg_match($regexp, $type)) {
+ unset($folders[$idx]);
+ }
+ }
+
+ return $folders;
+ }
+
+}
diff --git a/plugins/libkolab/lib/kolab_storage_cache.php b/plugins/libkolab/lib/kolab_storage_cache.php
new file mode 100644
index 00000000..be94bb87
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_storage_cache.php
@@ -0,0 +1,568 @@
+<?php
+
+/**
+ * Kolab storage cache class providing a local caching layer for Kolab groupware objects.
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli@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_storage_cache
+{
+ private $db;
+ private $imap;
+ private $folder;
+ private $uid2msg;
+ private $objects;
+ private $index = array();
+ private $resource_uri;
+ private $enabled = true;
+ private $synched = false;
+ private $ready = false;
+
+ private $binary_cols = array('photo','pgppublickey','pkcs7publickey');
+
+
+ /**
+ * Default constructor
+ */
+ public function __construct(kolab_storage_folder $storage_folder = null)
+ {
+ $rcmail = rcube::get_instance();
+ $this->db = $rcmail->get_dbh();
+ $this->imap = $rcmail->get_storage();
+ $this->enabled = $rcmail->config->get('kolab_cache', false);
+
+ if ($storage_folder)
+ $this->set_folder($storage_folder);
+ }
+
+
+ /**
+ * Connect cache with a storage folder
+ *
+ * @param kolab_storage_folder The storage folder instance to connect with
+ */
+ public function set_folder(kolab_storage_folder $storage_folder)
+ {
+ $this->folder = $storage_folder;
+
+ if (empty($this->folder->name)) {
+ $this->ready = false;
+ return;
+ }
+
+ // compose fully qualified ressource uri for this instance
+ $this->resource_uri = $this->folder->get_resource_uri();
+ $this->ready = $this->enabled;
+ }
+
+
+ /**
+ * Synchronize local cache data with remote
+ */
+ public function synchronize()
+ {
+ // only sync once per request cycle
+ if ($this->synched)
+ return;
+
+ // lock synchronization for this folder or wait if locked
+ $this->_sync_lock();
+
+ // synchronize IMAP mailbox cache
+ $this->imap->folder_sync($this->folder->name);
+
+ // compare IMAP index with object cache index
+ $imap_index = $this->imap->index($this->folder->name);
+ $this->index = $imap_index->get();
+
+ // determine objects to fetch or to invalidate
+ if ($this->ready) {
+ // read cache index
+ $sql_result = $this->db->query(
+ "SELECT msguid, uid FROM kolab_cache WHERE resource=? AND type<>?",
+ $this->resource_uri,
+ 'lock'
+ );
+
+ $old_index = array();
+ while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+ $old_index[] = $sql_arr['msguid'];
+ $this->uid2msg[$sql_arr['uid']] = $sql_arr['msguid'];
+ }
+
+ // fetch new objects from imap
+ $fetch_index = array_diff($this->index, $old_index);
+ foreach ($this->_fetch($fetch_index, '*') as $object) {
+ $msguid = $object['_msguid'];
+ $this->set($msguid, $object);
+ }
+
+ // delete invalid entries from local DB
+ $del_index = array_diff($old_index, $this->index);
+ if (!empty($del_index)) {
+ $quoted_ids = join(',', array_map(array($this->db, 'quote'), $del_index));
+ $this->db->query(
+ "DELETE FROM kolab_cache WHERE resource=? AND msguid IN ($quoted_ids)",
+ $this->resource_uri
+ );
+ }
+ }
+
+ // remove lock
+ $this->_sync_unlock();
+
+ $this->synched = time();
+ }
+
+
+ /**
+ * Read a single entry from cache or
+ *
+ * @param string Related IMAP message UID
+ * @param string Object type to read
+ * @param string IMAP folder name the entry relates to
+ * @param array Hash array with object properties or null if not found
+ */
+ public function get($msguid, $type = null, $foldername = null)
+ {
+ // delegate to another cache instance
+ if ($foldername && $foldername != $this->folder->name) {
+ return kolab_storage::get_folder($foldername)->cache->get($msguid, $object);
+ }
+
+ // load object if not in memory
+ if (!isset($this->objects[$msguid])) {
+ if ($this->ready) {
+ $sql_result = $this->db->query(
+ "SELECT * FROM kolab_cache ".
+ "WHERE resource=? AND msguid=?",
+ $this->resource_uri,
+ $msguid
+ );
+
+ if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+ $this->objects[$msguid] = $this->_unserialize($sql_arr);
+ }
+ }
+
+ // fetch from IMAP if not present in cache
+ if (empty($this->objects[$msguid])) {
+ $result = $this->_fetch(array($msguid), $type, $foldername);
+ $this->objects[$msguid] = $result[0];
+ }
+ }
+
+ return $this->objects[$msguid];
+ }
+
+
+ /**
+ * Insert/Update a cache entry
+ *
+ * @param string Related IMAP message UID
+ * @param mixed Hash array with object properties to save or false to delete the cache entry
+ * @param string IMAP folder name the entry relates to
+ */
+ public function set($msguid, $object, $foldername = null)
+ {
+ // delegate to another cache instance
+ if ($foldername && $foldername != $this->folder->name) {
+ kolab_storage::get_folder($foldername)->cache->set($msguid, $object);
+ return;
+ }
+
+ // write to cache
+ if ($this->ready) {
+ // remove old entry
+ $this->db->query("DELETE FROM kolab_cache WHERE resource=? AND msguid=?",
+ $this->resource_uri, $msguid);
+
+ // write new object data if not false (wich means deleted)
+ if ($object) {
+ $sql_data = $this->_serialize($object);
+ $objtype = $object['_type'] ? $object['_type'] : $this->folder->type;
+
+ $result = $this->db->query(
+ "INSERT INTO kolab_cache ".
+ " (resource, type, msguid, uid, data, xml, dtstart, dtend, tags, words)".
+ " VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
+ $this->resource_uri,
+ $objtype,
+ $msguid,
+ $object['uid'],
+ $sql_data['data'],
+ $sql_data['xml'],
+ $sql_data['dtstart'],
+ $sql_data['dtend'],
+ $sql_data['tags'],
+ $sql_data['words']
+ );
+
+ if (!$this->db->affected_rows($result)) {
+ rcmail::raise_error(array(
+ 'code' => 900, 'type' => 'php',
+ 'message' => "Failed to write to kolab cache"
+ ), true);
+ }
+ }
+ }
+
+ // keep a copy in memory for fast access
+ $this->objects[$msguid] = $object;
+
+ if ($object)
+ $this->uid2msg[$object['uid']] = $msguid;
+ }
+
+ /**
+ * Move an existing cache entry to a new resource
+ *
+ * @param string Entry's IMAP message UID
+ * @param string Entry's Object UID
+ * @param string Target IMAP folder to move it to
+ */
+ public function move($msguid, $objuid, $target_folder)
+ {
+ $target = kolab_storage::get_folder($target_folder);
+
+ // resolve new message UID in target folder
+ if ($new_msguid = $target->cache->uid2msguid($objuid)) {
+ $this->db->query(
+ "UPDATE kolab_cache SET resource=?, msguid=? ".
+ "WHERE resource=? AND msguid=?",
+ $target->get_resource_uri(),
+ $new_msguid,
+ $this->resource_uri,
+ $msguid
+ );
+ }
+ else {
+ // just clear cache entry
+ $this->set($msguid, false);
+ }
+
+ unset($this->uid2msg[$uid]);
+ }
+
+
+ /**
+ * Remove all objects from local cache
+ */
+ public function purge($type = null)
+ {
+ $result = $this->db->query(
+ "DELETE FROM kolab_cache WHERE resource=?".
+ ($type ? ' AND type=?' : ''),
+ $this->resource_uri,
+ $type
+ );
+ return $this->db->affected_rows($result);
+ }
+
+
+ /**
+ * Select Kolab objects filtered by the given query
+ *
+ * @param array Pseudo-SQL query as list of filter parameter triplets
+ * triplet: array('<colname>', '<comparator>', '<value>')
+ * @return array List of Kolab data objects (each represented as hash array)
+ */
+ public function select($query = array())
+ {
+ $result = array();
+
+ // read from local cache DB (assume it to be synchronized)
+ if ($this->ready) {
+ $sql_result = $this->db->query(
+ "SELECT * FROM kolab_cache ".
+ "WHERE resource=? " . $this->_sql_where($query),
+ $this->resource_uri
+ );
+
+ while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
+ if ($object = $this->_unserialize($sql_arr))
+ $result[] = $object;
+ }
+ }
+ else {
+ // extract object type from query parameter
+ $filter = $this->_query2assoc($query);
+
+ // use 'list' for folder's default objects
+ if ($filter['type'] == $this->type) {
+ $index = $this->index;
+ }
+ else { // search by object type
+ $search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_storage_folder::KTYPE_PREFIX . $filter['type'];
+ $index = $this->imap->search_once($this->folder->name, $search)->get();
+ }
+
+ // fetch all messages in $index from IMAP
+ $result = $this->_fetch($index, $filter['type']);
+
+ // TODO: post-filter result according to query
+ }
+
+ return $result;
+ }
+
+
+ /**
+ * Get number of objects mathing the given query
+ *
+ * @param array $query Pseudo-SQL query as list of filter parameter triplets
+ * @return integer The number of objects of the given type
+ */
+ public function count($query = array())
+ {
+ $count = 0;
+
+ // cache is in sync, we can count records in local DB
+ if ($this->synched) {
+ $sql_result = $this->db->query(
+ "SELECT COUNT(*) AS NUMROWS FROM kolab_cache ".
+ "WHERE resource=? " . $this->_sql_where($query),
+ $this->resource_uri
+ );
+
+ $sql_arr = $this->db->fetch_assoc($sql_result);
+ $count = intval($sql_arr['NUMROWS']);
+ }
+ else {
+ // search IMAP by object type
+ $filter = $this->_query2assoc($query);
+ $ctype = kolab_storage_folder::KTYPE_PREFIX . $filter['type'];
+ $index = $this->imap->search_once($this->folder->name, 'UNDELETED HEADER X-Kolab-Type ' . $ctype);
+ $count = $index->count();
+ }
+
+ return $count;
+ }
+
+
+ /**
+ * Helper method to compose a valid SQL query from pseudo filter triplets
+ */
+ private function _sql_where($query)
+ {
+ $sql_where = '';
+ foreach ($query as $param) {
+ if ($param[1] == '=' && is_array($param[2])) {
+ $qvalue = '(' . join(',', array_map(array($this->db, 'quote'), $param[2])) . ')';
+ $param[1] = 'IN';
+ }
+ else {
+ $qvalue = $this->db->quote($param[2]);
+ }
+
+ $sql_where .= sprintf(' AND %s %s %s',
+ $this->db->quote_identifier($param[0]),
+ $param[1],
+ $qvalue
+ );
+ }
+
+ return $sql_where;
+ }
+
+ /**
+ * Helper method to convert the given pseudo-query triplets into
+ * an associative filter array with 'equals' values only
+ */
+ private function _query2assoc($query)
+ {
+ // extract object type from query parameter
+ $filter = array();
+ foreach ($query as $param) {
+ if ($param[1] == '=')
+ $filter[$param[0]] = $param[2];
+ }
+ return $filter;
+ }
+
+ /**
+ * Fetch messages from IMAP
+ *
+ * @param array List of message UIDs to fetch
+ * @return array List of parsed Kolab objects
+ */
+ private function _fetch($index, $type = null, $folder = null)
+ {
+ $results = array();
+ foreach ((array)$index as $msguid) {
+ if ($object = $this->folder->read_object($msguid, $type, $folder)) {
+ $results[] = $object;
+ $this->uid2msg[$object['uid']] = $msguid;
+ }
+ }
+
+ return $results;
+ }
+
+
+ /**
+ * Helper method to convert the given Kolab object into a dataset to be written to cache
+ */
+ private function _serialize($object)
+ {
+ $bincols = array_flip($this->binary_cols);
+ $sql_data = array('dtstart' => null, 'dtend' => null, 'xml' => '', 'tags' => '', 'words' => '');
+
+ // set type specific values
+ if ($this->folder->type == 'event') {
+ // database runs in server's timezone so using date() is what we want
+ $sql_data['dtstart'] = date('Y-m-d H:i:s', is_object($object['start']) ? $object['start']->format('U') : $object['start']);
+ $sql_data['dtend'] = date('Y-m-d H:i:s', is_object($object['end']) ? $object['end']->format('U') : $object['end']);
+
+ // extend date range for recurring events
+ if ($object['recurrence']) {
+ $sql_data['dtend'] = date('Y-m-d H:i:s', $object['recurrence']['UNTIL'] ?: strtotime('now + 2 years'));
+ }
+ }
+
+ if ($object['_formatobj']) {
+ $sql_data['xml'] = (string)$object['_formatobj']->write();
+ $sql_data['tags'] = ' ' . join(' ', $object['_formatobj']->get_tags()) . ' '; // pad with spaces for strict/prefix search
+ $sql_data['words'] = ' ' . join(' ', $object['_formatobj']->get_words()) . ' ';
+ }
+
+ // extract object data
+ $data = array();
+ foreach ($object as $key => $val) {
+ if ($val === "" || $val === null) {
+ // skip empty properties
+ continue;
+ }
+ if (isset($bincols[$key])) {
+ $data[$key] = base64_encode($val);
+ }
+ else if ($key[0] != '_') {
+ $data[$key] = $val;
+ }
+ else if ($key == '_attachments') {
+ foreach ($val as $k => $att) {
+ unset($att['content'], $att['path']);
+ if ($att['id'])
+ $data[$key][$k] = $att;
+ }
+ }
+ }
+
+ $sql_data['data'] = serialize($data);
+ return $sql_data;
+ }
+
+ /**
+ * Helper method to turn stored cache data into a valid storage object
+ */
+ private function _unserialize($sql_arr)
+ {
+ $object = unserialize($sql_arr['data']);
+
+ // decode binary properties
+ foreach ($this->binary_cols as $key) {
+ if (!empty($object[$key]))
+ $object[$key] = base64_decode($object[$key]);
+ }
+
+ // add meta data
+ $object['_type'] = $sql_arr['type'];
+ $object['_msguid'] = $sql_arr['msguid'];
+ $object['_mailbox'] = $this->folder->name;
+ $object['_formatobj'] = kolab_format::factory($sql_arr['type'], $sql_arr['xml']);
+
+ return $object;
+ }
+
+ /**
+ * Check lock record for this folder and wait if locked or set lock
+ */
+ private function _sync_lock()
+ {
+ if (!$this->ready)
+ return;
+
+ $sql_arr = $this->db->fetch_assoc($this->db->query(
+ "SELECT msguid AS locked, ".$this->db->unixtimestamp('created')." AS created FROM kolab_cache ".
+ "WHERE resource=? AND type=?",
+ $this->resource_uri,
+ 'lock'
+ ));
+
+ // create lock record if not exists
+ if (!$sql_arr) {
+ $this->db->query(
+ "INSERT INTO kolab_cache (resource, type, msguid, created, uid, data, xml)".
+ " VALUES (?, ?, 1, ?, '', '', '')",
+ $this->resource_uri,
+ 'lock',
+ date('Y-m-d H:i:s')
+ );
+ }
+ // wait if locked (expire locks after 10 minutes)
+ else if (intval($sql_arr['locked']) > 0 && (time() - $sql_arr['created']) < 600) {
+ usleep(500000);
+ return $this->_sync_lock();
+ }
+ // set lock
+ else {
+ $this->db->query(
+ "UPDATE kolab_cache SET msguid=1, created=? ".
+ "WHERE resource=? AND type=?",
+ date('Y-m-d H:i:s'),
+ $this->resource_uri,
+ 'lock'
+ );
+ }
+ }
+
+ /**
+ * Remove lock for this folder
+ */
+ private function _sync_unlock()
+ {
+ $this->db->query(
+ "UPDATE kolab_cache SET msguid=0, created='' ".
+ "WHERE resource=? AND type=?",
+ $this->resource_uri,
+ 'lock'
+ );
+ }
+
+ /**
+ * Resolve an object UID into an IMAP message UID
+ *
+ * @param string Kolab object UID
+ * @param boolean Include deleted objects
+ * @return int The resolved IMAP message UID
+ */
+ public function uid2msguid($uid, $deleted = false)
+ {
+ if (!isset($this->uid2msg[$uid])) {
+ // use IMAP SEARCH to get the right message
+ $index = $this->imap->search_once($this->folder->name, ($deleted ? '' : 'UNDELETED ') . 'HEADER SUBJECT ' . $uid);
+ $results = $index->get();
+ $this->uid2msg[$uid] = $results[0];
+ }
+
+ return $this->uid2msg[$uid];
+ }
+
+}
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
new file mode 100644
index 00000000..da0718a8
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -0,0 +1,835 @@
+<?php
+
+/**
+ * The kolab_storage_folder class represents an IMAP folder on the Kolab server.
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli@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_storage_folder
+{
+ const KTYPE_PREFIX = 'application/x-vnd.kolab.';
+
+ /**
+ * The folder name.
+ * @var string
+ */
+ public $name;
+
+ /**
+ * The type of this folder.
+ * @var string
+ */
+ public $type;
+
+ /**
+ * The attached cache object
+ * @var kolab_storage_cache
+ */
+ public $cache;
+
+ private $type_annotation;
+ private $imap;
+ private $info;
+ private $owner;
+ private $resource_uri;
+ private $uid2msg = array();
+
+
+ /**
+ * Default constructor
+ */
+ function __construct($name, $type = null)
+ {
+ $this->imap = rcube::get_instance()->get_storage();
+ $this->imap->set_options(array('skip_deleted' => true));
+ $this->cache = new kolab_storage_cache($this);
+ $this->set_folder($name, $type);
+ }
+
+
+ /**
+ * Set the IMAP folder this instance connects to
+ *
+ * @param string The folder name/path
+ * @param string Optional folder type if known
+ */
+ public function set_folder($name, $type = null)
+ {
+ if (!$type) {
+ $metadata = $this->imap->get_metadata($name, array(kolab_storage::CTYPE_KEY));
+ $type = $metadata[$name][kolab_storage::CTYPE_KEY];
+ }
+
+ $this->name = $name;
+ $this->type_annotation = $type;
+ $this->type = reset(explode('.', $type));
+ $this->resource_uri = null;
+
+ $this->imap->set_folder($this->name);
+ $this->cache->set_folder($this);
+ }
+
+
+ /**
+ *
+ */
+ private function get_folder_info()
+ {
+ if (!isset($this->info))
+ $this->info = $this->imap->folder_info($this->name);
+
+ return $this->info;
+ }
+
+
+ /**
+ * Returns IMAP metadata/annotations (GETMETADATA/GETANNOTATION)
+ *
+ * @param array List of metadata keys to read
+ * @return array Metadata entry-value hash array on success, NULL on error
+ */
+ public function get_metadata($keys)
+ {
+ $metadata = $this->imap->get_metadata($this->name, (array)$keys);
+ return $metadata[$this->name];
+ }
+
+
+ /**
+ * Sets IMAP metadata/annotations (SETMETADATA/SETANNOTATION)
+ *
+ * @param array $entries Entry-value array (use NULL value as NIL)
+ * @return boolean True on success, False on failure
+ */
+ public function set_metadata($entries)
+ {
+ return $this->imap->set_metadata($this->name, $entries);
+ }
+
+
+ /**
+ * Returns the owner of the folder.
+ *
+ * @return string The owner of this folder.
+ */
+ public function get_owner()
+ {
+ // return cached value
+ if (isset($this->owner))
+ return $this->owner;
+
+ $info = $this->get_folder_info();
+ $rcmail = rcube::get_instance();
+
+ switch ($info['namespace']) {
+ case 'personal':
+ $this->owner = $rcmail->user->get_username();
+ break;
+
+ case 'shared':
+ $this->owner = 'anonymous';
+ break;
+
+ default:
+ $owner = '';
+ list($prefix, $user) = explode($this->imap->get_hierarchy_delimiter(), $info['name']);
+ if (strpos($user, '@') === false) {
+ $domain = strstr($rcmail->user->get_username(), '@');
+ if (!empty($domain))
+ $user .= $domain;
+ }
+ $this->owner = $user;
+ break;
+ }
+
+ return $this->owner;
+ }
+
+
+ /**
+ * 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()
+ {
+ return $this->imap->folder_namespace($this->name);
+ }
+
+
+ /**
+ * Get IMAP ACL information for this folder
+ *
+ * @return string Permissions as string
+ */
+ public function get_myrights()
+ {
+ $rights = $this->info['rights'];
+
+ if (!is_array($rights))
+ $rights = $this->imap->my_rights($this->name);
+
+ return join('', (array)$rights);
+ }
+
+
+ /**
+ * Compose a unique resource URI for this IMAP folder
+ */
+ public function get_resource_uri()
+ {
+ if (!empty($this->resource_uri))
+ return $this->resource_uri;
+
+ // strip namespace prefix from folder name
+ $ns = $this->get_namespace();
+ $nsdata = $this->imap->get_namespace($ns);
+ if (is_array($nsdata[0]) && strlen($nsdata[0][0]) && strpos($this->name, $nsdata[0][0]) === 0) {
+ $subpath = substr($this->name, strlen($nsdata[0][0]));
+ if ($ns == 'other') {
+ list($user, $suffix) = explode($nsdata[0][1], $subpath);
+ $subpath = $suffix;
+ }
+ }
+ else {
+ $subpath = $this->name;
+ }
+
+ // compose fully qualified ressource uri for this instance
+ $this->resource_uri = 'imap://' . urlencode($this->get_owner()) . '@' . $this->imap->options['host'] . '/' . $subpath;
+ return $this->resource_uri;
+ }
+
+
+ /**
+ * Check subscription status of this folder
+ *
+ * @param string Subscription type (kolab_storage::SERVERSIDE_SUBSCRIPTION or kolab_storage::CLIENTSIDE_SUBSCRIPTION)
+ * @return boolean True if subscribed, false if not
+ */
+ public function is_subscribed($type = 0)
+ {
+ static $subscribed; // local cache
+
+ if ($type == kolab_storage::SERVERSIDE_SUBSCRIPTION) {
+ if (!$subscribed)
+ $subscribed = $this->imap->list_folders_subscribed();
+
+ return in_array($this->name, $subscribed);
+ }
+ else if (kolab_storage::CLIENTSIDE_SUBSCRIPTION) {
+ // TODO: implement this
+ return true;
+ }
+
+ return false;
+ }
+
+ /**
+ * Change subscription status of this folder
+ *
+ * @param boolean The desired subscription status: true = subscribed, false = not subscribed
+ * @param string Subscription type (kolab_storage::SERVERSIDE_SUBSCRIPTION or kolab_storage::CLIENTSIDE_SUBSCRIPTION)
+ * @return True on success, false on error
+ */
+ public function subscribe($subscribed, $type = 0)
+ {
+ if ($type == kolab_storage::SERVERSIDE_SUBSCRIPTION) {
+ return $subscribed ? $this->imap->subscribe($this->name) : $this->imap->unsubscribe($this->name);
+ }
+ else {
+ // TODO: implement this
+ }
+
+ return false;
+ }
+
+
+ /**
+ * Get number of objects stored in this folder
+ *
+ * @param string $type Object type (e.g. contact, event, todo, journal, note, configuration)
+ * @return integer The number of objects of the given type
+ */
+ public function count($type = null)
+ {
+ if (!$type) $type = $this->type;
+
+ // synchronize cache first
+ $this->cache->synchronize();
+
+ return $this->cache->count(array(array('type','=',$type)));
+ }
+
+
+ /**
+ * List all Kolab objects of the given type
+ *
+ * @param string $type Object type (e.g. contact, event, todo, journal, note, configuration)
+ * @return array List of Kolab data objects (each represented as hash array)
+ */
+ public function get_objects($type = null)
+ {
+ if (!$type) $type = $this->type;
+
+ // synchronize caches
+ $this->cache->synchronize();
+
+ // fetch objects from cache
+ return $this->cache->select(array(array('type','=',$type)));
+ }
+
+
+ /**
+ * Select *some* Kolab objects matching the given query
+ *
+ * @param array Pseudo-SQL query as list of filter parameter triplets
+ * triplet: array('<colname>', '<comparator>', '<value>')
+ * @return array List of Kolab data objects (each represented as hash array)
+ */
+ public function select($query = array())
+ {
+ // check query argument
+ if (empty($query))
+ return $this->get_objects();
+
+ $type = null;
+ foreach ($query as $i => $param) {
+ if ($param[0] == 'type') {
+ $type = $param[2];
+ }
+ else if (($param[0] == 'dtstart' || $param[0] == 'dtend') && is_numeric($param[2])) {
+ $query[$i][2] = date('Y-m-d H:i:s', $param[2]);
+ }
+ }
+
+ // add type selector if not in $query
+ if (!$type)
+ $query[] = array('type','=',$this->type);
+
+ // synchronize caches
+ $this->cache->synchronize();
+
+ // fetch objects from cache
+ return $this->cache->select($query);
+ }
+
+
+ /**
+ * Getter for a single Kolab object, identified by its UID
+ *
+ * @param string Object UID
+ * @return array The Kolab object represented as hash array
+ */
+ public function get_object($uid)
+ {
+ // synchronize caches
+ $this->cache->synchronize();
+
+ $msguid = $this->cache->uid2msguid($uid);
+ if ($msguid && ($object = $this->cache->get($msguid)))
+ return $object;
+
+ return false;
+ }
+
+
+ /**
+ * Fetch a Kolab object attachment which is stored in a separate part
+ * of the mail MIME message that represents the Kolab record.
+ *
+ * @param string Object's UID
+ * @param string The attachment's mime number
+ * @param string IMAP folder where message is stored;
+ * If set, that also implies that the given UID is an IMAP UID
+ * @return mixed The attachment content as binary string
+ */
+ public function get_attachment($uid, $part, $mailbox = null)
+ {
+ if ($msguid = ($mailbox ? $uid : $this->cache->uid2msguid($uid))) {
+ $this->imap->set_folder($mailbox ? $mailbox : $this->name);
+ return $this->imap->get_message_part($msguid, $part);
+ }
+
+ return null;
+ }
+
+
+ /**
+ * Fetch the mime message from the storage server and extract
+ * the Kolab groupware object from it
+ *
+ * @param string The IMAP message UID to fetch
+ * @param string The object type expected (use wildcard '*' to accept all types)
+ * @param string The folder name where the message is stored
+ * @return mixed Hash array representing the Kolab object, a kolab_format instance or false if not found
+ */
+ public function read_object($msguid, $type = null, $folder = null)
+ {
+ if (!$type) $type = $this->type;
+ if (!$folder) $folder = $this->name;
+
+ $this->imap->set_folder($folder);
+
+ $headers = $this->imap->get_message_headers($msguid);
+ $object_type = substr($headers->others['x-kolab-type'], strlen(self::KTYPE_PREFIX));
+ $content_type = self::KTYPE_PREFIX . $object_type;
+
+ // check object type header and abort on mismatch
+ if ($type != '*' && $object_type != $type)
+ return false;
+
+ $message = new rcube_message($msguid);
+ $attachments = array();
+
+ // get XML part
+ foreach ((array)$message->attachments as $part) {
+ if (!$xml && ($part->mimetype == $content_type || preg_match('!application/([a-z]+\+)?xml!', $part->mimetype))) {
+ $xml = $part->body ? $part->body : $message->get_part_content($part->mime_id);
+ }
+ else if ($part->filename || $part->content_id) {
+ $key = $part->content_id ? trim($part->content_id, '<>') : $part->filename;
+ $attachments[$key] = array(
+ 'id' => $part->mime_id,
+ 'mimetype' => $part->mimetype,
+ 'size' => $part->size,
+ );
+ }
+ }
+
+ if (!$xml) {
+ raise_error(array(
+ 'code' => 600,
+ 'type' => 'php',
+ 'file' => __FILE__,
+ 'line' => __LINE__,
+ 'message' => "Could not find Kolab data part in message $msguid ($this->name).",
+ ), true);
+ return false;
+ }
+
+ $format = kolab_format::factory($object_type);
+
+ if (is_a($format, 'PEAR_Error'))
+ return false;
+
+ // check kolab format version
+ if (strpos($xml, '<' . $object_type) !== false) {
+ // old Kolab 2.0 format detected
+ $handler = class_exists('Horde_Kolab_Format') ? Horde_Kolab_Format::factory('XML', $object_type) : null;
+ if (!is_object($handler) || is_a($handler, 'PEAR_Error')) {
+ return false;
+ }
+
+ // XML-to-array
+ $object = $handler->load($xml);
+ $format->fromkolab2($object);
+ }
+ else {
+ // load Kolab 3 format using libkolabxml
+ $format->load($xml);
+ }
+
+ if ($format->is_valid()) {
+ $object = $format->to_array();
+ $object['_type'] = $object_type;
+ $object['_msguid'] = $msguid;
+ $object['_mailbox'] = $this->name;
+ $object['_attachments'] = array_merge((array)$object['_attachments'], $attachments);
+ $object['_formatobj'] = $format;
+
+ return $object;
+ }
+ else {
+ // try to extract object UID from XML block
+ if (preg_match('!<uid>(.+)</uid>!Uims', $xml, $m))
+ $msgadd = " UID = " . trim(strip_tags($m[1]));
+
+ raise_error(array(
+ 'code' => 600,
+ 'type' => 'php',
+ 'file' => __FILE__,
+ 'line' => __LINE__,
+ 'message' => "Could not parse Kolab object data in message $msguid ($this->name)." . $msgadd,
+ ), true);
+ }
+
+ return false;
+ }
+
+
+ /**
+ * Save an object in this folder.
+ *
+ * @param array $object The array that holds the data of the object.
+ * @param string $type The type of the kolab object.
+ * @param string $uid The UID of the old object if it existed before
+ * @return boolean True on success, false on error
+ */
+ public function save(&$object, $type = null, $uid = null)
+ {
+ if (!$type)
+ $type = $this->type;
+
+ // copy attachments from old message
+ if (!empty($object['_msguid']) && ($old = $this->cache->get($object['_msguid'], $type, $object['_mailbox']))) {
+ foreach ((array)$old['_attachments'] as $name => $att) {
+ if (!isset($object['_attachments'][$name])) {
+ $object['_attachments'][$name] = $old['_attachments'][$name];
+ }
+ // load photo.attachment from old Kolab2 format to be directly embedded in xcard block
+ if ($name == 'photo.attachment' && !isset($object['photo']) && !$object['_attachments'][$name]['content'] && $att['id']) {
+ $object['photo'] = $this->get_attachment($object['_msguid'], $att['id'], $object['_mailbox']);
+ unset($object['_attachments'][$name]);
+ }
+ }
+ }
+
+ if ($raw_msg = $this->build_message($object, $type)) {
+ $result = $this->imap->save_message($this->name, $raw_msg, '', false);
+
+ // delete old message
+ if ($result && !empty($object['_msguid']) && !empty($object['_mailbox'])) {
+ $this->imap->delete_message($object['_msguid'], $object['_mailbox']);
+ $this->cache->set($object['_msguid'], false, $object['_mailbox']);
+ }
+ else if ($result && $uid && ($msguid = $this->cache->uid2msguid($uid))) {
+ $this->imap->delete_message($msguid, $this->name);
+ $this->cache->set($object['_msguid'], false);
+ }
+
+ // update cache with new UID
+ if ($result) {
+ $object['_msguid'] = $result;
+ $this->cache->set($result, $object);
+ }
+ }
+
+ return $result;
+ }
+
+
+ /**
+ * Delete the specified object from this folder.
+ *
+ * @param mixed $object The Kolab object to delete or object UID
+ * @param boolean $expunge Should the folder be expunged?
+ * @param boolean $trigger Should the folder update be triggered?
+ *
+ * @return boolean True if successful, false on error
+ */
+ public function delete($object, $expunge = true, $trigger = true)
+ {
+ $msguid = is_array($object) ? $object['_msguid'] : $this->cache->uid2msguid($object);
+ $success = false;
+
+ if ($msguid && $expunge) {
+ $success = $this->imap->delete_message($msguid, $this->name);
+ }
+ else if ($msguid) {
+ $success = $this->imap->set_flag($msguid, 'DELETED', $this->name);
+ }
+
+ if ($success) {
+ $this->cache->set($result, false);
+ }
+
+ return $success;
+ }
+
+
+ /**
+ *
+ */
+ public function delete_all()
+ {
+ $this->cache->purge();
+ return $this->imap->clear_folder($this->name);
+ }
+
+
+ /**
+ * Restore a previously deleted object
+ *
+ * @param string Object UID
+ * @return mixed Message UID on success, false on error
+ */
+ public function undelete($uid)
+ {
+ if ($msguid = $this->cache->uid2msguid($uid, true)) {
+ if ($this->imap->set_flag($msguid, 'UNDELETED', $this->name)) {
+ return $msguid;
+ }
+ }
+
+ return false;
+ }
+
+
+ /**
+ * Move a Kolab object message to another IMAP folder
+ *
+ * @param string Object UID
+ * @param string IMAP folder to move object to
+ * @return boolean True on success, false on failure
+ */
+ public function move($uid, $target_folder)
+ {
+ if ($msguid = $this->cache->uid2msguid($uid)) {
+ if ($success = $this->imap->move_message($msguid, $target_folder, $this->name)) {
+ $this->cache->move($msguid, $uid, $target_folder);
+ return true;
+ }
+ else {
+ raise_error(array(
+ 'code' => 600, 'type' => 'php',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Failed to move message $msguid to $target_folder: " . $this->imap->get_error_str(),
+ ), true);
+ }
+ }
+
+ return false;
+ }
+
+
+ /**
+ * Creates source of the configuration object message
+ */
+ private function build_message(&$object, $type)
+ {
+ // load old object to preserve data we don't understand/process
+ if (is_object($object['_formatobj']))
+ $format = $object['_formatobj'];
+ else if ($object['_msguid'] && ($old = $this->cache->get($object['_msguid'], $type, $object['_mailbox'])))
+ $format = $old['_formatobj'];
+
+ // create new kolab_format instance
+ if (!$format)
+ $format = kolab_format::factory($type);
+
+ $format->set($object);
+ $xml = $format->write();
+ $object['uid'] = $format->uid; // read UID from format
+ $object['_formatobj'] = $format;
+
+ if (!$format->is_valid() || empty($object['uid'])) {
+ return false;
+ }
+
+ $mime = new Mail_mime("\r\n");
+ $rcmail = rcube::get_instance();
+ $headers = array();
+ $part_id = 1;
+
+ if ($ident = $rcmail->user->get_identity()) {
+ $headers['From'] = $ident['email'];
+ $headers['To'] = $ident['email'];
+ }
+ $headers['Date'] = date('r');
+ $headers['X-Kolab-Type'] = self::KTYPE_PREFIX . $type;
+ $headers['Subject'] = $object['uid'];
+// $headers['Message-ID'] = $rcmail->gen_message_id();
+ $headers['User-Agent'] = $rcmail->config->get('useragent');
+
+ $mime->headers($headers);
+ $mime->setTXTBody('This is a Kolab Groupware object. '
+ . 'To view this object you will need an email client that understands the Kolab Groupware format. '
+ . "For a list of such email clients please visit http://www.kolab.org/\n\n");
+
+ $mime->addAttachment($xml, // file
+ $format->CTYPE, // content-type
+ 'kolab.xml', // filename
+ false, // is_file
+ '8bit', // encoding
+ 'attachment', // disposition
+ RCMAIL_CHARSET // charset
+ );
+ $part_id++;
+
+ // save object attachments as separate parts
+ // TODO: optimize memory consumption by using tempfiles for transfer
+ foreach ((array)$object['_attachments'] as $name => $att) {
+ if (empty($att['content']) && !empty($att['id'])) {
+ $msguid = !empty($object['_msguid']) ? $object['_msguid'] : $object['uid'];
+ $att['content'] = $this->get_attachment($msguid, $att['id'], $object['_mailbox']);
+ }
+
+ $headers = array('Content-ID' => Mail_mimePart::encodeHeader('Content-ID', '<' . $name . '>', RCMAIL_CHARSET, 'quoted-printable'));
+
+ if (!empty($att['content'])) {
+ $mime->addAttachment($att['content'], $att['mimetype'], $name, false, 'base64', 'attachment', '', '', '', null, null, '', null, $headers);
+ $part_id++;
+ }
+ else if (!empty($att['path'])) {
+ $mime->addAttachment($att['path'], $att['mimetype'], $name, true, 'base64', 'attachment', '', '', '', null, null, '', null, $headers);
+ $part_id++;
+ }
+
+ $object['_attachments'][$name]['id'] = $part_id;
+ }
+
+ return $mime->getMessage();
+ }
+
+
+ /**
+ * Triggers any required updates after changes within the
+ * folder. This is currently only required for handling free/busy
+ * information with Kolab.
+ *
+ * @return boolean|PEAR_Error True if successfull.
+ */
+ public function trigger()
+ {
+ $owner = $this->get_owner();
+ $result = false;
+
+ switch($this->type) {
+ case 'event':
+ if ($this->get_namespace() == 'personal') {
+ $result = $this->trigger_url(
+ sprintf('%s/trigger/%s/%s.pfb', kolab_storage::get_freebusy_server(), $owner, $this->imap->mod_folder($this->name)),
+ $this->imap->options['user'],
+ $this->imap->options['password']
+ );
+ }
+ break;
+
+ default:
+ return true;
+ }
+
+ if ($result && is_object($result) && is_a($result, 'PEAR_Error')) {
+ return PEAR::raiseError(sprintf("Failed triggering folder %s. Error was: %s",
+ $this->name, $result->getMessage()));
+ }
+
+ return $result;
+ }
+
+ /**
+ * Triggers a URL.
+ *
+ * @param string $url The URL to be triggered.
+ * @param string $auth_user Username to authenticate with
+ * @param string $auth_passwd Password for basic auth
+ * @return boolean|PEAR_Error True if successfull.
+ */
+ private function trigger_url($url, $auth_user = null, $auth_passwd = null)
+ {
+ require_once('HTTP/Request2.php');
+
+ try {
+ $rcmail = rcube::get_instance();
+ $request = new HTTP_Request2($url);
+ $request->setConfig(array('ssl_verify_peer' => $rcmail->config->get('kolab_ssl_verify_peer', true)));
+
+ // set authentication credentials
+ if ($auth_user && $auth_passwd)
+ $request->setAuth($auth_user, $auth_passwd);
+
+ $result = $request->send();
+ // rcube::write_log('trigger', $result->getBody());
+ }
+ catch (Exception $e) {
+ return PEAR::raiseError($e->getMessage());
+ }
+
+ return true;
+ }
+
+
+ /* Legacy methods to keep compatibility with the old Horde Kolab_Storage classes */
+
+ /**
+ * Compatibility method
+ */
+ public function getOwner()
+ {
+ PEAR::raiseError("Call to deprecated method kolab_storage_folder::getOwner()");
+ return $this->get_owner();
+ }
+
+ /**
+ * Get IMAP ACL information for this folder
+ */
+ public function getMyRights()
+ {
+ PEAR::raiseError("Call to deprecated method kolab_storage_folder::getMyRights()");
+ return $this->get_myrights();
+ }
+
+ /**
+ * NOP to stay compatible with the formerly used Horde classes
+ */
+ public function getData()
+ {
+ PEAR::raiseError("Call to deprecated method kolab_storage_folder::getData()");
+ return $this;
+ }
+
+ /**
+ * List all Kolab objects of the given type
+ */
+ public function getObjects($type = null)
+ {
+ PEAR::raiseError("Call to deprecated method kolab_storage_folder::getObjects()");
+ return $this->get_objects($type);
+ }
+
+ /**
+ * Getter for a single Kolab object, identified by its UID
+ */
+ public function getObject($uid)
+ {
+ PEAR::raiseError("Call to deprecated method kolab_storage_folder::getObject()");
+ return $this->get_object($uid);
+ }
+
+ /**
+ *
+ */
+ public function getAttachment($key)
+ {
+ PEAR::raiseError("Call to deprecated method not returning anything.");
+ return null;
+ }
+
+ /**
+ * Alias function of delete()
+ */
+ public function deleteMessage($id, $trigger = true, $expunge = true)
+ {
+ PEAR::raiseError("Call to deprecated method kolab_storage_folder::deleteMessage()");
+ return $this->delete(array('_msguid' => $id), $trigger, $expunge);
+ }
+
+ /**
+ *
+ */
+ public function deleteAll()
+ {
+ PEAR::raiseError("Call to deprecated method kolab_storage_folder::deleteAll()");
+ return $this->delete_all();
+ }
+
+
+}
+
diff --git a/plugins/libkolab/libkolab.php b/plugins/libkolab/libkolab.php
new file mode 100644
index 00000000..5d64dfb4
--- /dev/null
+++ b/plugins/libkolab/libkolab.php
@@ -0,0 +1,74 @@
+<?php
+
+/**
+ * Kolab core library
+ *
+ * Plugin to setup a basic environment for the interaction with a Kolab server.
+ * Other Kolab-related plugins will depend on it and can use the library classes
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli@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 libkolab extends rcube_plugin
+{
+ /**
+ * Required startup method of a Roundcube plugin
+ */
+ public function init()
+ {
+ // load local config
+ $this->load_config();
+
+ $this->add_hook('storage_init', array($this, 'storage_init'));
+
+ // extend include path to load bundled lib classes
+ $include_path = $this->home . '/lib' . PATH_SEPARATOR . ini_get('include_path');
+ set_include_path($include_path);
+
+ $rcmail = rcmail::get_instance();
+ try {
+ kolab_format::$timezone = new DateTimeZone($rcmail->config->get('timezone', 'GMT'));
+ }
+ catch (Exception $e) {
+ raise_error($e, true);
+ kolab_format::$timezone = new DateTimeZone('GMT');
+ }
+
+ // load (old) dependencies if available
+ if (@include_once('Horde/Util.php')) {
+ include_once 'Horde/Kolab/Format.php';
+ include_once 'Horde/Kolab/Format/XML.php';
+ include_once 'Horde/Kolab/Format/XML/contact.php';
+ include_once 'Horde/Kolab/Format/XML/event.php';
+
+ String::setDefaultCharset('UTF-8');
+ }
+ }
+
+ /**
+ * Hook into IMAP FETCH HEADER.FIELDS command and request Kolab-specific headers
+ */
+ function storage_init($p)
+ {
+ $p['fetch_headers'] = trim($p['fetch_headers'] .' X-KOLAB-TYPE');
+ return $p;
+ }
+
+
+}
diff --git a/plugins/odfviewer/odfviewer.php b/plugins/odfviewer/odfviewer.php
index a64cf857..1e106bbd 100644
--- a/plugins/odfviewer/odfviewer.php
+++ b/plugins/odfviewer/odfviewer.php
@@ -1,156 +1,159 @@
<?php
/**
* Open Document Viewer plugin
*
* Render Open Documents directly in the preview window
* by using the WebODF library by Tobias Hintze http://webodf.org/
*
* @version 0.2
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2011, Kolab Systems AG
*
* 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 odfviewer extends rcube_plugin
{
- public $task = 'mail|logout';
+ public $task = 'mail|calendar|logout';
private $tempdir = 'plugins/odfviewer/files/';
private $tempbase = 'plugins/odfviewer/files/';
private $odf_mimetypes = array(
'application/vnd.oasis.opendocument.chart',
'application/vnd.oasis.opendocument.chart-template',
'application/vnd.oasis.opendocument.formula',
'application/vnd.oasis.opendocument.formula-template',
'application/vnd.oasis.opendocument.graphics',
'application/vnd.oasis.opendocument.graphics-template',
'application/vnd.oasis.opendocument.presentation',
'application/vnd.oasis.opendocument.presentation-template',
'application/vnd.oasis.opendocument.text',
'application/vnd.oasis.opendocument.text-master',
'application/vnd.oasis.opendocument.text-template',
'application/vnd.oasis.opendocument.spreadsheet',
'application/vnd.oasis.opendocument.spreadsheet-template',
);
function init()
{
$this->tempdir = $this->home . '/files/';
$this->tempbase = $this->urlbase . 'files/';
// webODF only supports IE9 or higher
$ua = new rcube_browser;
if ($ua->ie && $ua->ver < 9)
return;
-
// extend list of mimetypes that should open in preview
$rcmail = rcmail::get_instance();
- if ($rcmail->action == 'preview' || $rcmail->action == 'show') {
+ if ($rcmail->action == 'preview' || $rcmail->action == 'show' || $rcmail->task == 'calendar') {
$mimetypes = $rcmail->config->get('client_mimetypes', 'text/plain,text/html,text/xml,image/jpeg,image/gif,image/png,application/x-javascript,application/pdf,application/x-shockwave-flash');
if (!is_array($mimetypes))
$mimetypes = explode(',', $mimetypes);
$rcmail->config->set('client_mimetypes', array_merge($mimetypes, $this->odf_mimetypes));
}
$this->add_hook('message_part_get', array($this, 'get_part'));
$this->add_hook('session_destroy', array($this, 'session_cleanup'));
}
/**
* Handler for message attachment download
*/
function get_part($args)
{
- global $IMAP, $MESSAGE;
-
if (!$args['download'] && $args['mimetype'] && in_array($args['mimetype'], $this->odf_mimetypes)) {
if (empty($_GET['_load'])) {
$suffix = preg_match('/(\.\w+)$/', $args['part']->filename, $m) ? $m[1] : '.odt';
$fn = md5(session_id() . $_SERVER['REQUEST_URI']) . $suffix;
-
+
// FIXME: copy file to disk because only apache can send the file correctly
$tempfn = $this->tempdir . $fn;
if (!file_exists($tempfn)) {
- $fp = fopen($tempfn, 'w');
- $IMAP->get_message_part($MESSAGE->uid, $args['part']->mime_id, $args['part'], false, $fp);
- fclose($fp);
-
+ if ($args['body']) {
+ file_put_contents($tempfn, $args['body']);
+ }
+ else {
+ $fp = fopen($tempfn, 'w');
+ $imap = rcmail::get_instance()->get_storage();
+ $imap->get_message_part($args['uid'], $args['id'], $args['part'], false, $fp);
+ fclose($fp);
+ }
+
// remember tempfiles in session to clean up on logout
$_SESSION['odfviewer']['tempfiles'][] = $fn;
}
// send webODF viewer page
$html = file_get_contents($this->home . '/odf.html');
header("Content-Type: text/html; charset=" . RCMAIL_CHARSET);
echo strtr($html, array(
'%%DOCROOT%%' => $this->urlbase,
'%%DOCURL%%' => $this->tempbase . $fn, # $_SERVER['REQUEST_URI'].'&_load=1',
));
$args['abort'] = true;
}
/*
else {
if ($_SERVER['REQUEST_METHOD'] == 'HEAD') {
header("Content-Length: " . max(10, $args['part']->size)); # content-length has to be present
$args['body'] = ' '; # send empty body
return $args;
}
}
*/
}
return $args;
}
/**
* Remove temp files opened during this session
*/
function session_cleanup()
{
foreach ((array)$_SESSION['odfviewer']['tempfiles'] as $fn) {
@unlink($this->tempdir . $fn);
}
// also trigger general garbage collection because not everybody logs out properly
$this->gc_cleanup();
}
/**
* Garbage collector function for temp files.
* Remove temp files older than two days
*/
function gc_cleanup()
{
$rcmail = rcmail::get_instance();
$tmp = unslashify($this->tempdir);
$expire = mktime() - 172800; // expire in 48 hours
if ($dir = opendir($tmp)) {
while (($fname = readdir($dir)) !== false) {
if ($fname[0] == '.')
continue;
if (filemtime($tmp.'/'.$fname) < $expire)
@unlink($tmp.'/'.$fname);
}
closedir($dir);
}
}
}
diff --git a/plugins/odfviewer/webodf.js b/plugins/odfviewer/webodf.js
index 5d04c2a6..5fd92485 100644
--- a/plugins/odfviewer/webodf.js
+++ b/plugins/odfviewer/webodf.js
@@ -1,331 +1,341 @@
// Input 0
/*
@licstart
The JavaScript code in this page is free software: you can redistribute it
and/or modify it under the terms of the GNU Affero General Public License
(GNU AGPL) as published by the Free Software Foundation, either version 3 of
the License, or (at your option) any later version. The code is distributed
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
FITNESS FOR A PARTICULAR PURPOSE. See the GNU AGPL for more details.
As additional permission under GNU AGPL version 3 section 7, you
may distribute non-source (e.g., minimized or compacted) forms of
that code without the copy of the GNU GPL normally required by
section 4, provided you include this license notice and a URL
through which recipients can access the Corresponding Source.
As a special exception to the AGPL, any HTML file which merely makes function
calls to this code, and for that purpose includes it by reference shall be
deemed a separate work for copyright law purposes. In addition, the copyright
holders of this code give you permission to combine this code with free
software libraries that are released under the GNU LGPL. You may copy and
distribute such a system following the terms of the GNU AGPL for this code
and the LGPL for the libraries. If you modify this code, you may extend this
exception to your version of the code, but you are not obligated to do so.
If you do not wish to do so, delete this exception statement from your
version.
This license applies to this entire compilation.
@licend
@source: http://www.webodf.org/
@source: http://gitorious.org/odfkit/webodf/
*/
var core={},gui={},xmldom={},odf={};
// Input 1
function Runtime(){}Runtime.ByteArray=function(){};Runtime.ByteArray.prototype.slice=function(){};Runtime.prototype.byteArrayFromArray=function(){};Runtime.prototype.byteArrayFromString=function(){};Runtime.prototype.byteArrayToString=function(){};Runtime.prototype.concatByteArrays=function(){};Runtime.prototype.read=function(){};Runtime.prototype.readFile=function(){};Runtime.prototype.readFileSync=function(){};Runtime.prototype.loadXML=function(){};Runtime.prototype.writeFile=function(){};
-Runtime.prototype.isFile=function(){};Runtime.prototype.getFileSize=function(){};Runtime.prototype.deleteFile=function(){};Runtime.prototype.log=function(){};Runtime.prototype.setTimeout=function(){};Runtime.prototype.libraryPaths=function(){};Runtime.prototype.type=function(){};Runtime.prototype.getDOMImplementation=function(){};Runtime.prototype.getWindow=function(){};var IS_COMPILED_CODE=true;
-Runtime.byteArrayToString=function(i,k){function e(e){var a="",b,h=e.length,c,d,f;for(b=0;b<h;b+=1)c=e[b],c<128?a+=String.fromCharCode(c):(b+=1,d=e[b],c<224?a+=String.fromCharCode((c&31)<<6|d&63):(b+=1,f=e[b],a+=String.fromCharCode((c&15)<<12|(d&63)<<6|f&63)));return a}if(k==="utf8")return e(i);else k!=="binary"&&this.log("Unsupported encoding: "+k);return function(e){var a="",b,h=e.length;for(b=0;b<h;b+=1)a+=String.fromCharCode(e[b]&255);return a}(i)};
-Runtime.getFunctionName=function(i){return i.name===void 0?(i=/function\s+(\w+)/.exec(i))&&i[1]:i.name};
-function BrowserRuntime(i){function k(b,a){var c,d,f;a?f=b:a=b;if(i){d=i.ownerDocument;if(f)c=d.createElement("span"),c.className=f,c.appendChild(d.createTextNode(f)),i.appendChild(c),i.appendChild(d.createTextNode(" "));c=d.createElement("span");c.appendChild(d.createTextNode(a));i.appendChild(c);i.appendChild(d.createElement("br"))}else console&&console.log(a)}var e=this,g={},a=window.ArrayBuffer&&window.Uint8Array;this.ByteArray=a?function(b){Uint8Array.prototype.slice=function(b,c){if(c===void 0)b===
-void 0&&(b=0),c=this.length;var a=this.subarray(b,c),f,j;c-=b;f=new Uint8Array(new ArrayBuffer(c));for(j=0;j<c;j+=1)f[j]=a[j];return f};return new Uint8Array(new ArrayBuffer(b))}:function(b){var a=[];a.length=b;return a};this.concatByteArrays=a?function(b,a){var c,d=b.length,f=a.length,j=new this.ByteArray(d+f);for(c=0;c<d;c+=1)j[c]=b[c];for(c=0;c<f;c+=1)j[c+d]=a[c];return j}:function(b,a){return b.concat(a)};this.byteArrayFromArray=function(b){return b.slice()};this.byteArrayFromString=function(b,
-a){if(a==="utf8"){var c=b.length,d,f,j,g=0;for(f=0;f<c;f+=1)j=b.charCodeAt(f),g+=1+(j>128)+(j>2048);d=new e.ByteArray(g);for(f=g=0;f<c;f+=1)j=b.charCodeAt(f),j<128?(d[g]=j,g+=1):j<2048?(d[g]=192|j>>>6,d[g+1]=128|j&63,g+=2):(d[g]=224|j>>>12&15,d[g+1]=128|j>>>6&63,d[g+2]=128|j&63,g+=3);return d}else a!=="binary"&&e.log("unknown encoding: "+a);c=b.length;d=new e.ByteArray(c);for(f=0;f<c;f+=1)d[f]=b.charCodeAt(f)&255;return d};this.byteArrayToString=Runtime.byteArrayToString;this.readFile=function(b,
-a,c){if(g.hasOwnProperty(b))c(null,g[b]);else{var d=new XMLHttpRequest;d.open("GET",b,true);d.onreadystatechange=function(){var f;d.readyState===4&&(d.status===0&&!d.responseText?c("File "+b+" is empty."):d.status===200||d.status===0?(f=a==="binary"?typeof VBArray!=="undefined"?(new VBArray(d.responseBody)).toArray():e.byteArrayFromString(d.responseText,"binary"):d.responseText,g[b]=f,c(null,f)):c(d.responseText||d.statusText))};d.overrideMimeType&&(a!=="binary"?d.overrideMimeType("text/plain; charset="+
-a):d.overrideMimeType("text/plain; charset=x-user-defined"));try{d.send(null)}catch(f){c(f.message)}}};this.read=function(b,a,c,d){if(g.hasOwnProperty(b))d(null,g[b].slice(a,a+c));else{var f=new XMLHttpRequest;f.open("GET",b,true);f.onreadystatechange=function(){var j;f.readyState===4&&(f.status===0&&!f.responseText?d("File "+b+" is empty."):f.status===200||f.status===0?(j=typeof VBArray!=="undefined"?(new VBArray(f.responseBody)).toArray():e.byteArrayFromString(f.responseText,"binary"),g[b]=j,d(null,
-j.slice(a,a+c))):d(f.responseText||f.statusText))};f.overrideMimeType&&f.overrideMimeType("text/plain; charset=x-user-defined");try{f.send(null)}catch(j){d(j.message)}}};this.readFileSync=function(b,a){var c=new XMLHttpRequest,d;c.open("GET",b,false);c.overrideMimeType&&(a!=="binary"?c.overrideMimeType("text/plain; charset="+a):c.overrideMimeType("text/plain; charset=x-user-defined"));try{if(c.send(null),c.status===200||c.status===0)d=c.responseText}catch(f){}return d};this.writeFile=function(b,a,
-c){g[b]=a;var d=new XMLHttpRequest;d.open("PUT",b,true);d.onreadystatechange=function(){d.readyState===4&&(d.status===0&&!d.responseText?c("File "+b+" is empty."):d.status>=200&&d.status<300||d.status===0?c(null):c("Status "+String(d.status)+": "+d.responseText||d.statusText))};a=a.buffer&&!d.sendAsBinary?a.buffer:e.byteArrayToString(a,"binary");try{d.sendAsBinary?d.sendAsBinary(a):d.send(a)}catch(f){e.log("HUH? "+f+" "+a),c(f.message)}};this.deleteFile=function(b,a){var c=new XMLHttpRequest;c.open("DELETE",
-b,true);c.onreadystatechange=function(){c.readyState===4&&(c.status<200&&c.status>=300?a(c.responseText):a(null))};c.send(null)};this.loadXML=function(b,a){var c=new XMLHttpRequest;c.open("GET",b,true);c.overrideMimeType("text/xml");c.onreadystatechange=function(){c.readyState===4&&(c.status===0&&!c.responseText?a("File "+b+" is empty."):c.status===200||c.status===0?a(null,c.responseXML):a(c.responseText))};try{c.send(null)}catch(d){a(d.message)}};this.isFile=function(b,a){e.getFileSize(b,function(b){a(b!==
--1)})};this.getFileSize=function(b,a){var c=new XMLHttpRequest;c.open("HEAD",b,true);c.onreadystatechange=function(){if(c.readyState===4){var b=c.getResponseHeader("Content-Length");b?a(parseInt(b,10)):a(-1)}};c.send(null)};this.log=k;this.setTimeout=function(b,a){setTimeout(function(){b()},a)};this.libraryPaths=function(){return["lib"]};this.setCurrentDirectory=function(){};this.type=function(){return"BrowserRuntime"};this.getDOMImplementation=function(){return window.document.implementation};this.exit=
-function(b){k("Calling exit with code "+String(b)+", but exit() is not implemented.")};this.getWindow=function(){return window}}
-function NodeJSRuntime(){var i=require("fs"),k="";this.ByteArray=function(e){return new Buffer(e)};this.byteArrayFromArray=function(e){var g=new Buffer(e.length),a,b=e.length;for(a=0;a<b;a+=1)g[a]=e[a];return g};this.concatByteArrays=function(e,g){var a=new Buffer(e.length+g.length);e.copy(a,0,0);g.copy(a,e.length,0);return a};this.byteArrayFromString=function(e,g){return new Buffer(e,g)};this.byteArrayToString=function(e,g){return e.toString(g)};this.readFile=function(e,g,a){g!=="binary"?i.readFile(e,
-g,a):i.readFile(e,null,a)};this.writeFile=function(e,g,a){i.writeFile(e,g,"binary",function(b){a(b||null)})};this.deleteFile=i.unlink;this.read=function(e,g,a,b){k&&(e=k+"/"+e);i.open(e,"r+",666,function(h,c){if(h)b(h);else{var d=new Buffer(a);i.read(c,d,0,a,g,function(a){i.close(c);b(a,d)})}})};this.readFileSync=function(e,g){return!g?"":i.readFileSync(e,g)};this.loadXML=function(){throw"Not implemented.";};this.isFile=function(e,g){k&&(e=k+"/"+e);i.stat(e,function(a,b){g(!a&&b.isFile())})};this.getFileSize=
-function(e,g){k&&(e=k+"/"+e);i.stat(e,function(a,b){a?g(-1):g(b.size)})};this.log=function(e){process.stderr.write(e+"\n")};this.setTimeout=function(e,g){setTimeout(function(){e()},g)};this.libraryPaths=function(){return[__dirname]};this.setCurrentDirectory=function(e){k=e};this.currentDirectory=function(){return k};this.type=function(){return"NodeJSRuntime"};this.getDOMImplementation=function(){return null};this.exit=process.exit;this.getWindow=function(){return null}}
-function RhinoRuntime(){var i=this,k=Packages.javax.xml.parsers.DocumentBuilderFactory.newInstance(),e,g,a="";k.setValidating(false);k.setNamespaceAware(true);k.setExpandEntityReferences(false);k.setSchema(null);g=Packages.org.xml.sax.EntityResolver({resolveEntity:function(a,h){var c=new Packages.java.io.FileReader(h);return new Packages.org.xml.sax.InputSource(c)}});e=k.newDocumentBuilder();e.setEntityResolver(g);this.ByteArray=function(a){return[a]};this.byteArrayFromArray=function(a){return a};
-this.byteArrayFromString=function(a){var h=[],c,d=a.length;for(c=0;c<d;c+=1)h[c]=a.charCodeAt(c)&255;return h};this.byteArrayToString=Runtime.byteArrayToString;this.concatByteArrays=function(a,h){return a.concat(h)};this.loadXML=function(a,h){var c=new Packages.java.io.File(a),d;try{d=e.parse(c)}catch(f){print(f);h(f);return}h(null,d)};this.readFile=function(a,h,c){var d=new Packages.java.io.File(a),f=h==="binary"?"latin1":h;d.isFile()?(a=readFile(a,f),h==="binary"&&(a=i.byteArrayFromString(a,"binary")),
-c(null,a)):c(a+" is not a file.")};this.writeFile=function(a,h,c){var a=new Packages.java.io.FileOutputStream(a),d,f=h.length;for(d=0;d<f;d+=1)a.write(h[d]);a.close();c(null)};this.deleteFile=function(a,h){(new Packages.java.io.File(a))["delete"]()?h(null):h("Could not delete "+a)};this.read=function(b,h,c,d){a&&(b=a+"/"+b);var f;f=b;var j="binary";(new Packages.java.io.File(f)).isFile()?(j==="binary"&&(j="latin1"),f=readFile(f,j)):f=null;f?d(null,this.byteArrayFromString(f.substring(h,h+c),"binary")):
-d("Cannot read "+b)};this.readFileSync=function(a,h){return!h?"":readFile(a,h)};this.isFile=function(b,h){a&&(b=a+"/"+b);var c=new Packages.java.io.File(b);h(c.isFile())};this.getFileSize=function(b,h){a&&(b=a+"/"+b);var c=new Packages.java.io.File(b);h(c.length())};this.log=print;this.setTimeout=function(a){a()};this.libraryPaths=function(){return["lib"]};this.setCurrentDirectory=function(b){a=b};this.currentDirectory=function(){return a};this.type=function(){return"RhinoRuntime"};this.getDOMImplementation=
-function(){return e.getDOMImplementation()};this.exit=quit;this.getWindow=function(){return null}}var runtime=function(){return typeof window!=="undefined"?new BrowserRuntime(window.document.getElementById("logoutput")):typeof require!=="undefined"?new NodeJSRuntime:new RhinoRuntime}();
-(function(){function i(e){var a=e[0],b;b=eval("if (typeof "+a+" === 'undefined') {eval('"+a+" = {};');}"+a);for(a=1;a<e.length-1;a+=1)b.hasOwnProperty(e[a])||(b=b[e[a]]={});return b[e[e.length-1]]}var k={},e={};runtime.loadClass=function(g){if(!IS_COMPILED_CODE&&!k.hasOwnProperty(g)){var a=g.split("."),b;b=i(a);if(!b&&(b=function(a){var b,d,f,j,g;d=a.replace(".","/")+".js";j=runtime.libraryPaths();runtime.currentDirectory&&j.push(runtime.currentDirectory());for(g=0;!b&&g<j.length;g+=1){f=j[g];if(!e.hasOwnProperty(f))if((b=
-runtime.readFileSync(j[g]+"/manifest.js","utf8"))&&b.length)try{e[f]=eval(b)}catch(i){e[f]=null,runtime.log("Cannot load manifest for "+f+".")}else e[f]=null;b=null;if((f=e[f])&&f.indexOf&&f.indexOf(d)!==-1)try{b=runtime.readFileSync(j[g]+"/"+d,"utf8")}catch(l){throw runtime.log("Error loading "+a+" "+l),l;}}if(b===void 0)throw"Cannot load class "+a;try{b=eval(a+" = eval(code);")}catch(k){throw runtime.log("Error loading "+a+" "+k),k;}return b}(g),!b||Runtime.getFunctionName(b)!==a[a.length-1]))throw runtime.log("Loaded code is not for "+
-a[a.length-1]),"Loaded code is not for "+a[a.length-1];k[g]=true}}})();
-(function(i){function k(e){if(e.length){var g=e[0];runtime.readFile(g,"utf8",function(a,b){function h(){var a;(a=eval(b))&&runtime.exit(a)}var c="";runtime.libraryPaths();g.indexOf("/")!==-1&&(c=g.substring(0,g.indexOf("/")));runtime.setCurrentDirectory(c);a?(runtime.log(a),runtime.exit(1)):h.apply(null,e)})}}i=Array.prototype.slice.call(i);runtime.type()==="NodeJSRuntime"?k(process.argv.slice(2)):runtime.type()==="RhinoRuntime"?k(i):k(i.slice(1))})(typeof arguments!=="undefined"&&arguments);
+Runtime.prototype.isFile=function(){};Runtime.prototype.getFileSize=function(){};Runtime.prototype.deleteFile=function(){};Runtime.prototype.log=function(){};Runtime.prototype.setTimeout=function(){};Runtime.prototype.libraryPaths=function(){};Runtime.prototype.type=function(){};Runtime.prototype.getDOMImplementation=function(){};Runtime.prototype.getWindow=function(){};var IS_COMPILED_CODE=!0;
+Runtime.byteArrayToString=function(g,m){function e(e){var a="",c,b=e.length,d,o,f;for(c=0;c<b;c+=1)d=e[c],128>d?a+=String.fromCharCode(d):(c+=1,o=e[c],224>d?a+=String.fromCharCode((d&31)<<6|o&63):(c+=1,f=e[c],a+=String.fromCharCode((d&15)<<12|(o&63)<<6|f&63)));return a}if("utf8"===m)return e(g);"binary"!==m&&this.log("Unsupported encoding: "+m);return function(e){var a="",c,b=e.length;for(c=0;c<b;c+=1)a+=String.fromCharCode(e[c]&255);return a}(g)};
+Runtime.getFunctionName=function(g){return void 0===g.name?(g=/function\s+(\w+)/.exec(g))&&g[1]:g.name};
+function BrowserRuntime(g){function m(c,b){var d,a,f;b?f=c:b=c;g?(a=g.ownerDocument,f&&(d=a.createElement("span"),d.className=f,d.appendChild(a.createTextNode(f)),g.appendChild(d),g.appendChild(a.createTextNode(" "))),d=a.createElement("span"),d.appendChild(a.createTextNode(b)),g.appendChild(d),g.appendChild(a.createElement("br"))):console&&console.log(b)}var e=this,k={},a=window.ArrayBuffer&&window.Uint8Array;this.ByteArray=a?function(c){Uint8Array.prototype.slice=function(c,d){void 0===d&&(void 0===
+c&&(c=0),d=this.length);var a=this.subarray(c,d),f,h,d=d-c;f=new Uint8Array(new ArrayBuffer(d));for(h=0;h<d;h+=1)f[h]=a[h];return f};return new Uint8Array(new ArrayBuffer(c))}:function(c){var b=[];b.length=c;return b};this.concatByteArrays=a?function(c,b){var d,a=c.length,f=b.length,h=new this.ByteArray(a+f);for(d=0;d<a;d+=1)h[d]=c[d];for(d=0;d<f;d+=1)h[d+a]=b[d];return h}:function(c,b){return c.concat(b)};this.byteArrayFromArray=function(c){return c.slice()};this.byteArrayFromString=function(c,b){if("utf8"===
+b){var d=c.length,a,f,h,i=0;for(f=0;f<d;f+=1)h=c.charCodeAt(f),i+=1+(128<h)+(2048<h);a=new e.ByteArray(i);for(f=i=0;f<d;f+=1)h=c.charCodeAt(f),128>h?(a[i]=h,i+=1):2048>h?(a[i]=192|h>>>6,a[i+1]=128|h&63,i+=2):(a[i]=224|h>>>12&15,a[i+1]=128|h>>>6&63,a[i+2]=128|h&63,i+=3);return a}"binary"!==b&&e.log("unknown encoding: "+b);d=c.length;a=new e.ByteArray(d);for(f=0;f<d;f+=1)a[f]=c.charCodeAt(f)&255;return a};this.byteArrayToString=Runtime.byteArrayToString;this.readFile=function(c,b,d){if(k.hasOwnProperty(c))d(null,
+k[c]);else{var a=new XMLHttpRequest;a.open("GET",c,!0);a.onreadystatechange=function(){var f;4===a.readyState&&(0===a.status&&!a.responseText?d("File "+c+" is empty."):200===a.status||0===a.status?(f="binary"===b?"undefined"!==typeof VBArray?(new VBArray(a.responseBody)).toArray():e.byteArrayFromString(a.responseText,"binary"):a.responseText,k[c]=f,d(null,f)):d(a.responseText||a.statusText))};a.overrideMimeType&&("binary"!==b?a.overrideMimeType("text/plain; charset="+b):a.overrideMimeType("text/plain; charset=x-user-defined"));
+try{a.send(null)}catch(f){d(f.message)}}};this.read=function(c,a,d,o){if(k.hasOwnProperty(c))o(null,k[c].slice(a,a+d));else{var f=new XMLHttpRequest;f.open("GET",c,!0);f.onreadystatechange=function(){var i;4===f.readyState&&(0===f.status&&!f.responseText?o("File "+c+" is empty."):200===f.status||0===f.status?(i="undefined"!==typeof VBArray?(new VBArray(f.responseBody)).toArray():e.byteArrayFromString(f.responseText,"binary"),k[c]=i,o(null,i.slice(a,a+d))):o(f.responseText||f.statusText))};f.overrideMimeType&&
+f.overrideMimeType("text/plain; charset=x-user-defined");try{f.send(null)}catch(h){o(h.message)}}};this.readFileSync=function(c,a){var d=new XMLHttpRequest,o;d.open("GET",c,!1);d.overrideMimeType&&("binary"!==a?d.overrideMimeType("text/plain; charset="+a):d.overrideMimeType("text/plain; charset=x-user-defined"));try{if(d.send(null),200===d.status||0===d.status)o=d.responseText}catch(f){}return o};this.writeFile=function(c,a,d){k[c]=a;var o=new XMLHttpRequest;o.open("PUT",c,!0);o.onreadystatechange=
+function(){4===o.readyState&&(0===o.status&&!o.responseText?d("File "+c+" is empty."):200<=o.status&&300>o.status||0===o.status?d(null):d("Status "+o.status+": "+o.responseText||o.statusText))};a=a.buffer&&!o.sendAsBinary?a.buffer:e.byteArrayToString(a,"binary");try{o.sendAsBinary?o.sendAsBinary(a):o.send(a)}catch(f){e.log("HUH? "+f+" "+a),d(f.message)}};this.deleteFile=function(c,a){var d=new XMLHttpRequest;d.open("DELETE",c,!0);d.onreadystatechange=function(){4===d.readyState&&(200>d.status&&300<=
+d.status?a(d.responseText):a(null))};d.send(null)};this.loadXML=function(a,b){var d=new XMLHttpRequest;d.open("GET",a,!0);d.overrideMimeType&&d.overrideMimeType("text/xml");d.onreadystatechange=function(){4===d.readyState&&(0===d.status&&!d.responseText?b("File "+a+" is empty."):200===d.status||0===d.status?b(null,d.responseXML):b(d.responseText))};try{d.send(null)}catch(o){b(o.message)}};this.isFile=function(a,b){e.getFileSize(a,function(a){b(-1!==a)})};this.getFileSize=function(a,b){var d=new XMLHttpRequest;
+d.open("HEAD",a,!0);d.onreadystatechange=function(){if(4===d.readyState){var a=d.getResponseHeader("Content-Length");a?b(parseInt(a,10)):b(-1)}};d.send(null)};this.log=m;this.setTimeout=function(a,b){setTimeout(function(){a()},b)};this.libraryPaths=function(){return["lib"]};this.setCurrentDirectory=function(){};this.type=function(){return"BrowserRuntime"};this.getDOMImplementation=function(){return window.document.implementation};this.exit=function(a){m("Calling exit with code "+a+", but exit() is not implemented.")};
+this.getWindow=function(){return window}}
+function NodeJSRuntime(){var g=require("fs"),m="";this.ByteArray=function(e){return new Buffer(e)};this.byteArrayFromArray=function(e){var k=new Buffer(e.length),a,c=e.length;for(a=0;a<c;a+=1)k[a]=e[a];return k};this.concatByteArrays=function(e,k){var a=new Buffer(e.length+k.length);e.copy(a,0,0);k.copy(a,e.length,0);return a};this.byteArrayFromString=function(e,k){return new Buffer(e,k)};this.byteArrayToString=function(e,k){return e.toString(k)};this.readFile=function(e,k,a){"binary"!==k?g.readFile(e,
+k,a):g.readFile(e,null,a)};this.writeFile=function(e,k,a){g.writeFile(e,k,"binary",function(c){a(c||null)})};this.deleteFile=g.unlink;this.read=function(e,k,a,c){m&&(e=m+"/"+e);g.open(e,"r+",666,function(b,d){if(b)c(b);else{var o=new Buffer(a);g.read(d,o,0,a,k,function(a){g.close(d);c(a,o)})}})};this.readFileSync=function(e,k){return!k?"":g.readFileSync(e,k)};this.loadXML=function(){throw"Not implemented.";};this.isFile=function(e,k){m&&(e=m+"/"+e);g.stat(e,function(a,c){k(!a&&c.isFile())})};this.getFileSize=
+function(e,k){m&&(e=m+"/"+e);g.stat(e,function(a,c){a?k(-1):k(c.size)})};this.log=function(e){process.stderr.write(e+"\n")};this.setTimeout=function(e,k){setTimeout(function(){e()},k)};this.libraryPaths=function(){return[__dirname]};this.setCurrentDirectory=function(e){m=e};this.currentDirectory=function(){return m};this.type=function(){return"NodeJSRuntime"};this.getDOMImplementation=function(){return null};this.exit=process.exit;this.getWindow=function(){return null}}
+function RhinoRuntime(){var g=this,m=Packages.javax.xml.parsers.DocumentBuilderFactory.newInstance(),e,k,a="";m.setValidating(!1);m.setNamespaceAware(!0);m.setExpandEntityReferences(!1);m.setSchema(null);k=Packages.org.xml.sax.EntityResolver({resolveEntity:function(a,b){var d=new Packages.java.io.FileReader(b);return new Packages.org.xml.sax.InputSource(d)}});e=m.newDocumentBuilder();e.setEntityResolver(k);this.ByteArray=function(a){return[a]};this.byteArrayFromArray=function(a){return a};this.byteArrayFromString=
+function(a){var b=[],d,o=a.length;for(d=0;d<o;d+=1)b[d]=a.charCodeAt(d)&255;return b};this.byteArrayToString=Runtime.byteArrayToString;this.concatByteArrays=function(a,b){return a.concat(b)};this.loadXML=function(a,b){var d=new Packages.java.io.File(a),o;try{o=e.parse(d)}catch(f){print(f);b(f);return}b(null,o)};this.readFile=function(a,b,d){var o=new Packages.java.io.File(a),f="binary"===b?"latin1":b;o.isFile()?(a=readFile(a,f),"binary"===b&&(a=g.byteArrayFromString(a,"binary")),d(null,a)):d(a+" is not a file.")};
+this.writeFile=function(a,b,d){var a=new Packages.java.io.FileOutputStream(a),o,f=b.length;for(o=0;o<f;o+=1)a.write(b[o]);a.close();d(null)};this.deleteFile=function(a,b){(new Packages.java.io.File(a))["delete"]()?b(null):b("Could not delete "+a)};this.read=function(c,b,d,o){a&&(c=a+"/"+c);var f;f=c;var h="binary";(new Packages.java.io.File(f)).isFile()?("binary"===h&&(h="latin1"),f=readFile(f,h)):f=null;f?o(null,this.byteArrayFromString(f.substring(b,b+d),"binary")):o("Cannot read "+c)};this.readFileSync=
+function(a,b){return!b?"":readFile(a,b)};this.isFile=function(c,b){a&&(c=a+"/"+c);var d=new Packages.java.io.File(c);b(d.isFile())};this.getFileSize=function(c,b){a&&(c=a+"/"+c);var d=new Packages.java.io.File(c);b(d.length())};this.log=print;this.setTimeout=function(a){a()};this.libraryPaths=function(){return["lib"]};this.setCurrentDirectory=function(c){a=c};this.currentDirectory=function(){return a};this.type=function(){return"RhinoRuntime"};this.getDOMImplementation=function(){return e.getDOMImplementation()};
+this.exit=quit;this.getWindow=function(){return null}}var runtime=function(){return"undefined"!==typeof window?new BrowserRuntime(window.document.getElementById("logoutput")):"undefined"!==typeof require?new NodeJSRuntime:new RhinoRuntime}();
+(function(){function g(e){var a=e[0],c;c=eval("if (typeof "+a+" === 'undefined') {eval('"+a+" = {};');}"+a);for(a=1;a<e.length-1;a+=1)c.hasOwnProperty(e[a])||(c=c[e[a]]={});return c[e[e.length-1]]}var m={},e={};runtime.loadClass=function(k){function a(a){var a=a.replace(".","/")+".js",b=runtime.libraryPaths(),c,h,i;runtime.currentDirectory&&b.push(runtime.currentDirectory());for(c=0;c<b.length;c+=1){h=b[c];if(!e.hasOwnProperty(h))if((i=runtime.readFileSync(b[c]+"/manifest.js","utf8"))&&i.length)try{e[h]=
+eval(i)}catch(j){e[h]=null,runtime.log("Cannot load manifest for "+h+".")}else e[h]=null;if((h=e[h])&&h.indexOf&&-1!==h.indexOf(a))return b[c]+"/"+a}return null}if(!IS_COMPILED_CODE&&!m.hasOwnProperty(k)){var c=k.split("."),b;b=g(c);if(!b&&(b=function(c){var b,f;f=a(c);if(!f)throw c+" is not listed in any manifest.js.";try{b=runtime.readFileSync(f,"utf8")}catch(e){throw runtime.log("Error loading "+c+" "+e),e;}if(void 0===b)throw"Cannot load class "+c;try{b=eval(c+" = eval(code);")}catch(i){throw runtime.log("Error loading "+
+c+" "+i),i;}return b}(k),!b||Runtime.getFunctionName(b)!==c[c.length-1]))throw runtime.log("Loaded code is not for "+c[c.length-1]),"Loaded code is not for "+c[c.length-1];m[k]=!0}}})();
+(function(g){function m(e){if(e.length){var g=e[0];runtime.readFile(g,"utf8",function(a,c){function b(){var a;(a=eval(c))&&runtime.exit(a)}var d="";runtime.libraryPaths();-1!==g.indexOf("/")&&(d=g.substring(0,g.indexOf("/")));runtime.setCurrentDirectory(d);a?(runtime.log(a),runtime.exit(1)):b.apply(null,e)})}}g=Array.prototype.slice.call(g);"NodeJSRuntime"===runtime.type()?m(process.argv.slice(2)):"RhinoRuntime"===runtime.type()?m(g):m(g.slice(1))})("undefined"!==typeof arguments&&arguments);
// Input 2
-core.Base64=function(){function i(a){var b=[],f,c=a.length;for(f=0;f<c;f+=1)b[f]=a.charCodeAt(f)&255;return b}function k(a){var b,f="",c,d=a.length-2;for(c=0;c<d;c+=3)b=a[c]<<16|a[c+1]<<8|a[c+2],f+=u[b>>>18],f+=u[b>>>12&63],f+=u[b>>>6&63],f+=u[b&63];c===d+1?(b=a[c]<<4,f+=u[b>>>6],f+=u[b&63],f+="=="):c===d&&(b=a[c]<<10|a[c+1]<<2,f+=u[b>>>12],f+=u[b>>>6&63],f+=u[b&63],f+="=");return f}function e(a){var a=a.replace(/[^A-Za-z0-9+\/]+/g,""),b=[],f=a.length%4,c,d=a.length,h;for(c=0;c<d;c+=4)h=(n[a.charAt(c)]||
-0)<<18|(n[a.charAt(c+1)]||0)<<12|(n[a.charAt(c+2)]||0)<<6|(n[a.charAt(c+3)]||0),b.push(h>>16,h>>8&255,h&255);b.length-=[0,0,2,1][f];return b}function g(a){var b=[],f,c=a.length,d;for(f=0;f<c;f+=1)d=a[f],d<128?b.push(d):d<2048?b.push(192|d>>>6,128|d&63):b.push(224|d>>>12&15,128|d>>>6&63,128|d&63);return b}function a(a){var b=[],f,c=a.length,d,h,j;for(f=0;f<c;f+=1)d=a[f],d<128?b.push(d):(f+=1,h=a[f],d<224?b.push((d&31)<<6|h&63):(f+=1,j=a[f],b.push((d&15)<<12|(h&63)<<6|j&63)));return b}function b(a){return k(i(a))}
-function h(a){return String.fromCharCode.apply(String,e(a))}function c(b){return a(i(b))}function d(b){return String.fromCharCode.apply(String,a(b))}function f(a,b,f){for(var c="",d,h,j;b<f;b+=1)d=a.charCodeAt(b)&255,d<128?c+=String.fromCharCode(d):(b+=1,h=a.charCodeAt(b)&255,d<224?c+=String.fromCharCode((d&31)<<6|h&63):(b+=1,j=a.charCodeAt(b)&255,c+=String.fromCharCode((d&15)<<12|(h&63)<<6|j&63)));return c}function j(a,b){function c(){var e=j+d;if(e>a.length)e=a.length;h+=f(a,j,e);j=e;e=j===a.length;
-b(h,e)&&!e&&runtime.setTimeout(c,0)}var d=1E5,h="",j=0;a.length<d?b(f(a,0,a.length),true):(typeof a!=="string"&&(a=a.slice()),c())}function p(a){return g(i(a))}function m(a){return String.fromCharCode.apply(String,g(a))}function l(a){return String.fromCharCode.apply(String,g(i(a)))}var u="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";(function(){var a=[],b,f="A".charCodeAt(0),c="a".charCodeAt(0),d="0".charCodeAt(0);for(b=0;b<26;b+=1)a.push(f+b);for(b=0;b<26;b+=1)a.push(c+b);for(b=
-0;b<10;b+=1)a.push(d+b);a.push("+".charCodeAt(0));a.push("/".charCodeAt(0));return a})();var n=function(a){var b={},f,c;for(f=0,c=a.length;f<c;f+=1)b[a.charAt(f)]=f;return b}(u),q,r,y,C;(y=runtime.getWindow()&&runtime.getWindow().btoa)?q=function(a){return y(l(a))}:(y=b,q=function(a){return k(p(a))});(C=runtime.getWindow()&&runtime.getWindow().atob)?r=function(a){a=C(a);return f(a,0,a.length)}:(C=h,r=function(a){return d(e(a))});return function(){this.convertByteArrayToBase64=this.convertUTF8ArrayToBase64=
-k;this.convertBase64ToByteArray=this.convertBase64ToUTF8Array=e;this.convertUTF16ArrayToByteArray=this.convertUTF16ArrayToUTF8Array=g;this.convertByteArrayToUTF16Array=this.convertUTF8ArrayToUTF16Array=a;this.convertUTF8StringToBase64=b;this.convertBase64ToUTF8String=h;this.convertUTF8StringToUTF16Array=c;this.convertByteArrayToUTF16String=this.convertUTF8ArrayToUTF16String=d;this.convertUTF8StringToUTF16String=j;this.convertUTF16StringToByteArray=this.convertUTF16StringToUTF8Array=p;this.convertUTF16ArrayToUTF8String=
-m;this.convertUTF16StringToUTF8String=l;this.convertUTF16StringToBase64=q;this.convertBase64ToUTF16String=r;this.fromBase64=h;this.toBase64=b;this.atob=C;this.btoa=y;this.utob=l;this.btou=j;this.encode=q;this.encodeURI=function(a){return q(a).replace(/[+\/]/g,function(a){return a==="+"?"-":"_"}).replace(/\\=+$/,"")};this.decode=function(a){return r(a.replace(/[\-_]/g,function(a){return a==="-"?"+":"/"}))}}}();
+core.Base64=function(){function g(a){var c=[],b,d=a.length;for(b=0;b<d;b+=1)c[b]=a.charCodeAt(b)&255;return c}function m(a){var b,c="",d,f=a.length-2;for(d=0;d<f;d+=3)b=a[d]<<16|a[d+1]<<8|a[d+2],c+=x[b>>>18],c+=x[b>>>12&63],c+=x[b>>>6&63],c+=x[b&63];d===f+1?(b=a[d]<<4,c+=x[b>>>6],c+=x[b&63],c+="=="):d===f&&(b=a[d]<<10|a[d+1]<<2,c+=x[b>>>12],c+=x[b>>>6&63],c+=x[b&63],c+="=");return c}function e(a){var a=a.replace(/[^A-Za-z0-9+\/]+/g,""),c=[],b=a.length%4,d,f=a.length,e;for(d=0;d<f;d+=4)e=(p[a.charAt(d)]||
+0)<<18|(p[a.charAt(d+1)]||0)<<12|(p[a.charAt(d+2)]||0)<<6|(p[a.charAt(d+3)]||0),c.push(e>>16,e>>8&255,e&255);c.length-=[0,0,2,1][b];return c}function k(a){var c=[],b,d=a.length,f;for(b=0;b<d;b+=1)f=a[b],128>f?c.push(f):2048>f?c.push(192|f>>>6,128|f&63):c.push(224|f>>>12&15,128|f>>>6&63,128|f&63);return c}function a(a){var c=[],b,d=a.length,f,e,l;for(b=0;b<d;b+=1)f=a[b],128>f?c.push(f):(b+=1,e=a[b],224>f?c.push((f&31)<<6|e&63):(b+=1,l=a[b],c.push((f&15)<<12|(e&63)<<6|l&63)));return c}function c(a){return m(g(a))}
+function b(a){return String.fromCharCode.apply(String,e(a))}function d(c){return a(g(c))}function o(c){for(var c=a(c),b="",d=0;d<c.length;)b+=String.fromCharCode.apply(String,c.slice(d,d+45E3)),d+=45E3;return b}function f(a,c,b){var d="",f,e,l;for(l=c;l<b;l+=1)c=a.charCodeAt(l)&255,128>c?d+=String.fromCharCode(c):(l+=1,f=a.charCodeAt(l)&255,224>c?d+=String.fromCharCode((c&31)<<6|f&63):(l+=1,e=a.charCodeAt(l)&255,d+=String.fromCharCode((c&15)<<12|(f&63)<<6|e&63)));return d}function h(a,c){function b(){var l=
+j+d;l>a.length&&(l=a.length);e+=f(a,j,l);j=l;l=j===a.length;c(e,l)&&!l&&runtime.setTimeout(b,0)}var d=1E5,e="",j=0;a.length<d?c(f(a,0,a.length),!0):("string"!==typeof a&&(a=a.slice()),b())}function i(a){return k(g(a))}function j(a){return String.fromCharCode.apply(String,k(a))}function n(a){return String.fromCharCode.apply(String,k(g(a)))}var x="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";(function(){var a=[],c;for(c=0;26>c;c+=1)a.push(65+c);for(c=0;26>c;c+=1)a.push(97+c);for(c=
+0;10>c;c+=1)a.push(48+c);a.push(43);a.push(47);return a})();var p=function(a){var c={},b,d;for(b=0,d=a.length;b<d;b+=1)c[a.charAt(b)]=b;return c}(x),t,r,z,s;(z=runtime.getWindow()&&runtime.getWindow().btoa)?t=function(a){return z(n(a))}:(z=c,t=function(a){return m(i(a))});(s=runtime.getWindow()&&runtime.getWindow().atob)?r=function(a){a=s(a);return f(a,0,a.length)}:(s=b,r=function(a){return o(e(a))});return function(){this.convertByteArrayToBase64=this.convertUTF8ArrayToBase64=m;this.convertBase64ToByteArray=
+this.convertBase64ToUTF8Array=e;this.convertUTF16ArrayToByteArray=this.convertUTF16ArrayToUTF8Array=k;this.convertByteArrayToUTF16Array=this.convertUTF8ArrayToUTF16Array=a;this.convertUTF8StringToBase64=c;this.convertBase64ToUTF8String=b;this.convertUTF8StringToUTF16Array=d;this.convertByteArrayToUTF16String=this.convertUTF8ArrayToUTF16String=o;this.convertUTF8StringToUTF16String=h;this.convertUTF16StringToByteArray=this.convertUTF16StringToUTF8Array=i;this.convertUTF16ArrayToUTF8String=j;this.convertUTF16StringToUTF8String=
+n;this.convertUTF16StringToBase64=t;this.convertBase64ToUTF16String=r;this.fromBase64=b;this.toBase64=c;this.atob=s;this.btoa=z;this.utob=n;this.btou=h;this.encode=t;this.encodeURI=function(a){return t(a).replace(/[+\/]/g,function(a){return"+"===a?"-":"_"}).replace(/\\=+$/,"")};this.decode=function(a){return r(a.replace(/[\-_]/g,function(a){return"-"===a?"+":"/"}))}}}();
// Input 3
-core.RawDeflate=function(){function i(){this.dl=this.fc=0}function k(){this.extra_bits=this.static_tree=this.dyn_tree=null;this.max_code=this.max_length=this.elems=this.extra_base=0}function e(a,b,f,c){this.good_length=a;this.max_lazy=b;this.nice_length=f;this.max_chain=c}function g(){this.next=null;this.len=0;this.ptr=Array(a);this.off=0}var a=8192,b,h,c,d,f=null,j,p,m,l,u,n,q,r,y,C,s,v,E,B,z,G,o,x,t,w,L,Q,R,X,A,J,H,S,K,F,D,U,M,I,O,T,N,$,Y,oa,da,ea,V,fa,pa,aa,ga,Z,ha,ia,qa,ra=[0,0,0,0,0,0,0,0,1,
-1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0],ba=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],Ha=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7],va=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],ja;ja=[new e(0,0,0,0),new e(4,4,8,4),new e(4,5,16,8),new e(4,6,32,32),new e(4,4,16,16),new e(8,16,32,32),new e(8,16,128,128),new e(8,32,128,256),new e(32,128,258,1024),new e(32,258,258,4096)];var ka=function(d){f[p+j++]=d;if(p+j==a&&j!=0){var e;b!=null?(d=b,b=b.next):d=new g;d.next=null;d.len=
-d.off=0;h==null?h=c=d:c=c.next=d;d.len=j-p;for(e=0;e<d.len;e++)d.ptr[e]=f[p+e];j=p=0}},la=function(b){b&=65535;p+j<a-2?(f[p+j++]=b&255,f[p+j++]=b>>>8):(ka(b&255),ka(b>>>8))},ma=function(){s=(s<<5^l[o+3-1]&255)&8191;v=q[32768+s];q[o&32767]=v;q[32768+s]=o},W=function(a,b){P(b[a].fc,b[a].dl)},wa=function(a,b,f){return a[b].fc<a[f].fc||a[b].fc==a[f].fc&&N[b]<=N[f]},xa=function(a,b,f){var c;for(c=0;c<f&&qa<ia.length;c++)a[b+c]=ia.charCodeAt(qa++)&255;return c},ya=function(a){var b=L,f=o,c,d=G,h=o>32506?
-o-32506:0,j=o+258,e=l[f+d-1],g=l[f+d];G>=X&&(b>>=2);do if(c=a,!(l[c+d]!=g||l[c+d-1]!=e||l[c]!=l[f]||l[++c]!=l[f+1])){f+=2;c++;do;while(l[++f]==l[++c]&&l[++f]==l[++c]&&l[++f]==l[++c]&&l[++f]==l[++c]&&l[++f]==l[++c]&&l[++f]==l[++c]&&l[++f]==l[++c]&&l[++f]==l[++c]&&f<j);c=258-(j-f);f=j-258;if(c>d){x=a;d=c;if(c>=258)break;e=l[f+d-1];g=l[f+d]}}while((a=q[a&32767])>h&&--b!=0);return d},sa=function(){var a,b,f=65536-w-o;if(f==-1)f--;else if(o>=65274){for(a=0;a<32768;a++)l[a]=l[a+32768];x-=32768;o-=32768;
-C-=32768;for(a=0;a<8192;a++)b=q[32768+a],q[32768+a]=b>=32768?b-32768:0;for(a=0;a<32768;a++)b=q[a],q[a]=b>=32768?b-32768:0;f+=32768}t||(a=xa(l,o+w,f),a<=0?t=true:w+=a)},Ia=function(a,b,f){var c;if(!d){if(!t){y=r=0;var e,g;if(S[0].dl==0){F.dyn_tree=A;F.static_tree=H;F.extra_bits=ra;F.extra_base=257;F.elems=286;F.max_length=15;F.max_code=0;D.dyn_tree=J;D.static_tree=S;D.extra_bits=ba;D.extra_base=0;D.elems=30;D.max_length=15;D.max_code=0;U.dyn_tree=K;U.static_tree=null;U.extra_bits=Ha;U.extra_base=0;
-U.elems=19;U.max_length=7;for(g=e=U.max_code=0;g<28;g++){oa[g]=e;for(c=0;c<1<<ra[g];c++)$[e++]=g}$[e-1]=g;for(g=e=0;g<16;g++){da[g]=e;for(c=0;c<1<<ba[g];c++)Y[e++]=g}for(e>>=7;g<30;g++){da[g]=e<<7;for(c=0;c<1<<ba[g]-7;c++)Y[256+e++]=g}for(c=0;c<=15;c++)M[c]=0;for(c=0;c<=143;)H[c++].dl=8,M[8]++;for(;c<=255;)H[c++].dl=9,M[9]++;for(;c<=279;)H[c++].dl=7,M[7]++;for(;c<=287;)H[c++].dl=8,M[8]++;za(H,287);for(c=0;c<30;c++)S[c].dl=5,S[c].fc=Aa(c,5);Ba()}for(c=0;c<8192;c++)q[32768+c]=0;Q=ja[R].max_lazy;X=ja[R].good_length;
-L=ja[R].max_chain;C=o=0;w=xa(l,0,65536);if(w<=0)t=true,w=0;else{for(t=false;w<262&&!t;)sa();for(c=s=0;c<2;c++)s=(s<<5^l[c]&255)&8191}h=null;p=j=0;R<=3?(G=2,z=0):(z=2,B=0);m=false}d=true;if(w==0)return m=true,0}if((c=Ca(a,b,f))==f)return f;if(m)return c;if(R<=3)for(;w!=0&&h==null;){ma();v!=0&&o-v<=32506&&(z=ya(v),z>w&&(z=w));if(z>=3)if(g=ca(o-x,z-3),w-=z,z<=Q){z--;do o++,ma();while(--z!=0);o++}else o+=z,z=0,s=l[o]&255,s=(s<<5^l[o+1]&255)&8191;else g=ca(0,l[o]&255),w--,o++;g&&(na(0),C=o);for(;w<262&&
-!t;)sa()}else for(;w!=0&&h==null;){ma();G=z;E=x;z=2;v!=0&&G<Q&&o-v<=32506&&(z=ya(v),z>w&&(z=w),z==3&&o-x>4096&&z--);if(G>=3&&z<=G){g=ca(o-1-E,G-3);w-=G-1;G-=2;do o++,ma();while(--G!=0);B=0;z=2;o++;g&&(na(0),C=o)}else B!=0?ca(0,l[o-1]&255)&&(na(0),C=o):B=1,o++,w--;for(;w<262&&!t;)sa()}w==0&&(B!=0&&ca(0,l[o-1]&255),na(1),m=true);return c+Ca(a,c+b,f-c)},Ca=function(a,c,d){var e,g,q;for(e=0;h!=null&&e<d;){g=d-e;if(g>h.len)g=h.len;for(q=0;q<g;q++)a[c+e+q]=h.ptr[h.off+q];h.off+=g;h.len-=g;e+=g;if(h.len==
-0)g=h,h=h.next,g.next=b,b=g}if(e==d)return e;if(p<j){g=d-e;g>j-p&&(g=j-p);for(q=0;q<g;q++)a[c+e+q]=f[p+q];p+=g;e+=g;j==p&&(j=p=0)}return e},Ba=function(){var a;for(a=0;a<286;a++)A[a].fc=0;for(a=0;a<30;a++)J[a].fc=0;for(a=0;a<19;a++)K[a].fc=0;A[256].fc=1;aa=V=fa=pa=Z=ha=0;ga=1},ta=function(a,b){for(var c=I[b],f=b<<1;f<=O;){f<O&&wa(a,I[f+1],I[f])&&f++;if(wa(a,c,I[f]))break;I[b]=I[f];b=f;f<<=1}I[b]=c},za=function(a,b){var c=Array(16),f=0,d;for(d=1;d<=15;d++)f=f+M[d-1]<<1,c[d]=f;for(f=0;f<=b;f++)if(d=
-a[f].dl,d!=0)a[f].fc=Aa(c[d]++,d)},ua=function(a){var b=a.dyn_tree,c=a.static_tree,f=a.elems,d,e=-1,h=f;O=0;T=573;for(d=0;d<f;d++)b[d].fc!=0?(I[++O]=e=d,N[d]=0):b[d].dl=0;for(;O<2;)d=I[++O]=e<2?++e:0,b[d].fc=1,N[d]=0,Z--,c!=null&&(ha-=c[d].dl);a.max_code=e;for(d=O>>1;d>=1;d--)ta(b,d);do d=I[1],I[1]=I[O--],ta(b,1),c=I[1],I[--T]=d,I[--T]=c,b[h].fc=b[d].fc+b[c].fc,N[h]=N[d]>N[c]+1?N[d]:N[c]+1,b[d].dl=b[c].dl=h,I[1]=h++,ta(b,1);while(O>=2);I[--T]=I[1];h=a.dyn_tree;d=a.extra_bits;var f=a.extra_base,c=
-a.max_code,g=a.max_length,j=a.static_tree,q,t,o,r,i=0;for(t=0;t<=15;t++)M[t]=0;h[I[T]].dl=0;for(a=T+1;a<573;a++)if(q=I[a],t=h[h[q].dl].dl+1,t>g&&(t=g,i++),h[q].dl=t,!(q>c))M[t]++,o=0,q>=f&&(o=d[q-f]),r=h[q].fc,Z+=r*(t+o),j!=null&&(ha+=r*(j[q].dl+o));if(i!=0){do{for(t=g-1;M[t]==0;)t--;M[t]--;M[t+1]+=2;M[g]--;i-=2}while(i>0);for(t=g;t!=0;t--)for(q=M[t];q!=0;)if(d=I[--a],!(d>c)){if(h[d].dl!=t)Z+=(t-h[d].dl)*h[d].fc,h[d].fc=t;q--}}za(b,e)},Da=function(a,b){var c,f=-1,d,e=a[0].dl,h=0,g=7,j=4;e==0&&(g=
-138,j=3);a[b+1].dl=65535;for(c=0;c<=b;c++)d=e,e=a[c+1].dl,++h<g&&d==e||(h<j?K[d].fc+=h:d!=0?(d!=f&&K[d].fc++,K[16].fc++):h<=10?K[17].fc++:K[18].fc++,h=0,f=d,e==0?(g=138,j=3):d==e?(g=6,j=3):(g=7,j=4))},Ea=function(a,b){var c,f=-1,d,e=a[0].dl,h=0,g=7,j=4;e==0&&(g=138,j=3);for(c=0;c<=b;c++)if(d=e,e=a[c+1].dl,!(++h<g&&d==e)){if(h<j){do W(d,K);while(--h!=0)}else d!=0?(d!=f&&(W(d,K),h--),W(16,K),P(h-3,2)):h<=10?(W(17,K),P(h-3,3)):(W(18,K),P(h-11,7));h=0;f=d;e==0?(g=138,j=3):d==e?(g=6,j=3):(g=7,j=4)}},na=
-function(a){var b,c,f,d;d=o-C;ea[pa]=aa;ua(F);ua(D);Da(A,F.max_code);Da(J,D.max_code);ua(U);for(f=18;f>=3;f--)if(K[va[f]].dl!=0)break;Z+=3*(f+1)+14;b=Z+3+7>>3;c=ha+3+7>>3;c<=b&&(b=c);if(d+4<=b&&C>=0){P(0+a,3);Fa();la(d);la(~d);for(f=0;f<d;f++)ka(l[C+f])}else if(c==b)P(2+a,3),Ga(H,S);else{P(4+a,3);d=F.max_code+1;b=D.max_code+1;f+=1;P(d-257,5);P(b-1,5);P(f-4,4);for(c=0;c<f;c++)P(K[va[c]].dl,3);Ea(A,d-1);Ea(J,b-1);Ga(A,J)}Ba();a!=0&&Fa()},ca=function(a,b){n[V++]=b;a==0?A[b].fc++:(a--,A[$[b]+256+1].fc++,
-J[(a<256?Y[a]:Y[256+(a>>7)])&255].fc++,u[fa++]=a,aa|=ga);ga<<=1;(V&7)==0&&(ea[pa++]=aa,aa=0,ga=1);if(R>2&&(V&4095)==0){var c=V*8,f=o-C,d;for(d=0;d<30;d++)c+=J[d].fc*(5+ba[d]);c>>=3;if(fa<parseInt(V/2,10)&&c<parseInt(f/2,10))return true}return V==8191||fa==8192},Ga=function(a,b){var c,f=0,d=0,h=0,e=0,g,j;if(V!=0){do(f&7)==0&&(e=ea[h++]),c=n[f++]&255,(e&1)==0?W(c,a):(g=$[c],W(g+256+1,a),j=ra[g],j!=0&&(c-=oa[g],P(c,j)),c=u[d++],g=(c<256?Y[c]:Y[256+(c>>7)])&255,W(g,b),j=ba[g],j!=0&&(c-=da[g],P(c,j))),
-e>>=1;while(f<V)}W(256,a)},P=function(a,c){y>16-c?(r|=a<<y,la(r),r=a>>16-y,y+=c-16):(r|=a<<y,y+=c)},Aa=function(a,c){var b=0;do b|=a&1,a>>=1,b<<=1;while(--c>0);return b>>1},Fa=function(){y>8?la(r):y>0&&ka(r);y=r=0};this.deflate=function(e,g){var j,o;ia=e;qa=0;typeof g=="undefined"&&(g=6);(j=g)?j<1?j=1:j>9&&(j=9):j=6;R=j;t=d=false;if(f==null){b=h=c=null;f=Array(a);l=Array(65536);u=Array(8192);n=Array(32832);q=Array(65536);A=Array(573);for(j=0;j<573;j++)A[j]=new i;J=Array(61);for(j=0;j<61;j++)J[j]=
-new i;H=Array(288);for(j=0;j<288;j++)H[j]=new i;S=Array(30);for(j=0;j<30;j++)S[j]=new i;K=Array(39);for(j=0;j<39;j++)K[j]=new i;F=new k;D=new k;U=new k;M=Array(16);I=Array(573);N=Array(573);$=Array(256);Y=Array(512);oa=Array(29);da=Array(30);ea=Array(1024)}for(var r=Array(1024),w=[];(j=Ia(r,0,r.length))>0;){var x=Array(j);for(o=0;o<j;o++)x[o]=String.fromCharCode(r[o]);w[w.length]=x.join("")}ia=null;return w.join("")}};
+core.RawDeflate=function(){function g(){this.dl=this.fc=0}function m(){this.extra_bits=this.static_tree=this.dyn_tree=null;this.max_code=this.max_length=this.elems=this.extra_base=0}function e(a,c,b,d){this.good_length=a;this.max_lazy=c;this.nice_length=b;this.max_chain=d}function k(){this.next=null;this.len=0;this.ptr=[];this.ptr.length=a;this.off=0}var a=8192,c,b,d,o,f=null,h,i,j,n,x,p,t,r,z,s,q,u,E,C,w,G,l,v,y,B,M,F,R,S,A,K,I,P,L,H,D,U,N,J,V,aa,T,ba,Q,$,W,ea,X,ia,pa,ca,fa,Y,da,ja,qa,ra=[0,0,0,
+0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0],ga=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],Ha=[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,3,7],va=[16,17,18,0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],ka;ka=[new e(0,0,0,0),new e(4,4,8,4),new e(4,5,16,8),new e(4,6,32,32),new e(4,4,16,16),new e(8,16,32,32),new e(8,16,128,128),new e(8,32,128,256),new e(32,128,258,1024),new e(32,258,258,4096)];var la=function(l){f[i+h++]=l;if(i+h===a){var e;if(0!==h){null!==c?(l=c,c=c.next):l=new k;
+l.next=null;l.len=l.off=0;null===b?b=d=l:d=d.next=l;l.len=h-i;for(e=0;e<l.len;e++)l.ptr[e]=f[i+e];h=i=0}}},ma=function(c){c&=65535;i+h<a-2?(f[i+h++]=c&255,f[i+h++]=c>>>8):(la(c&255),la(c>>>8))},na=function(){q=(q<<5^n[l+3-1]&255)&8191;u=t[32768+q];t[l&32767]=u;t[32768+q]=l},O=function(a,c){z>16-c?(r|=a<<z,ma(r),r=a>>16-z,z+=c-16):(r|=a<<z,z+=c)},Z=function(a,c){O(c[a].fc,c[a].dl)},wa=function(a,c,b){return a[c].fc<a[b].fc||a[c].fc===a[b].fc&&T[c]<=T[b]},xa=function(a,c,b){var d;for(d=0;d<b&&qa<ja.length;d++)a[c+
+d]=ja.charCodeAt(qa++)&255;return d},sa=function(){var a,c,b=65536-B-l;if(-1===b)b--;else if(65274<=l){for(a=0;32768>a;a++)n[a]=n[a+32768];v-=32768;l-=32768;s-=32768;for(a=0;8192>a;a++)c=t[32768+a],t[32768+a]=32768<=c?c-32768:0;for(a=0;32768>a;a++)c=t[a],t[a]=32768<=c?c-32768:0;b+=32768}y||(a=xa(n,l+B,b),0>=a?y=!0:B+=a)},ya=function(a){var c=M,b=l,d,f=G,e=32506<l?l-32506:0,j=l+258,i=n[b+f-1],q=n[b+f];G>=S&&(c>>=2);do if(d=a,!(n[d+f]!==q||n[d+f-1]!==i||n[d]!==n[b]||n[++d]!==n[b+1])){b+=2;d++;do++b;
+while(n[b]===n[++d]&&n[++b]===n[++d]&&n[++b]===n[++d]&&n[++b]===n[++d]&&n[++b]===n[++d]&&n[++b]===n[++d]&&n[++b]===n[++d]&&n[++b]===n[++d]&&b<j);d=258-(j-b);b=j-258;if(d>f){v=a;f=d;if(258<=d)break;i=n[b+f-1];q=n[b+f]}}while((a=t[a&32767])>e&&0!==--c);return f},ha=function(a,c){p[X++]=c;0===a?A[c].fc++:(a--,A[ba[c]+256+1].fc++,K[(256>a?Q[a]:Q[256+(a>>7)])&255].fc++,x[ia++]=a,ca|=fa);fa<<=1;0===(X&7)&&(ea[pa++]=ca,ca=0,fa=1);if(2<R&&0===(X&4095)){var b=8*X,d=l-s,f;for(f=0;30>f;f++)b+=K[f].fc*(5+ga[f]);
+b>>=3;if(ia<parseInt(X/2,10)&&b<parseInt(d/2,10))return!0}return 8191===X||8192===ia},ta=function(a,c){for(var b=J[c],d=c<<1;d<=V;){d<V&&wa(a,J[d+1],J[d])&&d++;if(wa(a,b,J[d]))break;J[c]=J[d];c=d;d<<=1}J[c]=b},za=function(a,c){var b=0;do b|=a&1,a>>=1,b<<=1;while(0<--c);return b>>1},Aa=function(a,c){var b=[];b.length=16;var d=0,f;for(f=1;15>=f;f++)d=d+N[f-1]<<1,b[f]=d;for(d=0;d<=c;d++)f=a[d].dl,0!==f&&(a[d].fc=za(b[f]++,f))},ua=function(a){var c=a.dyn_tree,b=a.static_tree,d=a.elems,f,l=-1,e=d;V=0;
+aa=573;for(f=0;f<d;f++)0!==c[f].fc?(J[++V]=l=f,T[f]=0):c[f].dl=0;for(;2>V;)f=J[++V]=2>l?++l:0,c[f].fc=1,T[f]=0,Y--,null!==b&&(da-=b[f].dl);a.max_code=l;for(f=V>>1;1<=f;f--)ta(c,f);do f=J[1],J[1]=J[V--],ta(c,1),b=J[1],J[--aa]=f,J[--aa]=b,c[e].fc=c[f].fc+c[b].fc,T[e]=T[f]>T[b]+1?T[f]:T[b]+1,c[f].dl=c[b].dl=e,J[1]=e++,ta(c,1);while(2<=V);J[--aa]=J[1];e=a.dyn_tree;f=a.extra_bits;var d=a.extra_base,b=a.max_code,j=a.max_length,i=a.static_tree,q,h,o,s,v=0;for(h=0;15>=h;h++)N[h]=0;e[J[aa]].dl=0;for(a=aa+
+1;573>a;a++)q=J[a],h=e[e[q].dl].dl+1,h>j&&(h=j,v++),e[q].dl=h,q>b||(N[h]++,o=0,q>=d&&(o=f[q-d]),s=e[q].fc,Y+=s*(h+o),null!==i&&(da+=s*(i[q].dl+o)));if(0!==v){do{for(h=j-1;0===N[h];)h--;N[h]--;N[h+1]+=2;N[j]--;v-=2}while(0<v);for(h=j;0!==h;h--)for(q=N[h];0!==q;)f=J[--a],f>b||(e[f].dl!==h&&(Y+=(h-e[f].dl)*e[f].fc,e[f].fc=h),q--)}Aa(c,l)},Ba=function(a,c){var b,d=-1,f,l=a[0].dl,e=0,h=7,j=4;0===l&&(h=138,j=3);a[c+1].dl=65535;for(b=0;b<=c;b++)f=l,l=a[b+1].dl,++e<h&&f===l||(e<j?L[f].fc+=e:0!==f?(f!==d&&
+L[f].fc++,L[16].fc++):10>=e?L[17].fc++:L[18].fc++,e=0,d=f,0===l?(h=138,j=3):f===l?(h=6,j=3):(h=7,j=4))},Ca=function(){8<z?ma(r):0<z&&la(r);z=r=0},Da=function(a,c){var b,d=0,f=0,l=0,e=0,h,j;if(0!==X){do 0===(d&7)&&(e=ea[l++]),b=p[d++]&255,0===(e&1)?Z(b,a):(h=ba[b],Z(h+256+1,a),j=ra[h],0!==j&&(b-=$[h],O(b,j)),b=x[f++],h=(256>b?Q[b]:Q[256+(b>>7)])&255,Z(h,c),j=ga[h],0!==j&&(b-=W[h],O(b,j))),e>>=1;while(d<X)}Z(256,a)},Ea=function(a,c){var b,d=-1,f,l=a[0].dl,e=0,h=7,j=4;0===l&&(h=138,j=3);for(b=0;b<=c;b++)if(f=
+l,l=a[b+1].dl,!(++e<h&&f===l)){if(e<j){do Z(f,L);while(0!==--e)}else 0!==f?(f!==d&&(Z(f,L),e--),Z(16,L),O(e-3,2)):10>=e?(Z(17,L),O(e-3,3)):(Z(18,L),O(e-11,7));e=0;d=f;0===l?(h=138,j=3):f===l?(h=6,j=3):(h=7,j=4)}},Fa=function(){var a;for(a=0;286>a;a++)A[a].fc=0;for(a=0;30>a;a++)K[a].fc=0;for(a=0;19>a;a++)L[a].fc=0;A[256].fc=1;ca=X=ia=pa=Y=da=0;fa=1},oa=function(a){var c,b,d,f;f=l-s;ea[pa]=ca;ua(H);ua(D);Ba(A,H.max_code);Ba(K,D.max_code);ua(U);for(d=18;3<=d&&!(0!==L[va[d]].dl);d--);Y+=3*(d+1)+14;c=
+Y+3+7>>3;b=da+3+7>>3;b<=c&&(c=b);if(f+4<=c&&0<=s){O(0+a,3);Ca();ma(f);ma(~f);for(d=0;d<f;d++)la(n[s+d])}else if(b===c)O(2+a,3),Da(I,P);else{O(4+a,3);f=H.max_code+1;c=D.max_code+1;d+=1;O(f-257,5);O(c-1,5);O(d-4,4);for(b=0;b<d;b++)O(L[va[b]].dl,3);Ea(A,f-1);Ea(K,c-1);Da(A,K)}Fa();0!==a&&Ca()},Ga=function(a,d,l){var e,j,q;for(e=0;null!==b&&e<l;){j=l-e;j>b.len&&(j=b.len);for(q=0;q<j;q++)a[d+e+q]=b.ptr[b.off+q];b.off+=j;b.len-=j;e+=j;0===b.len&&(j=b,b=b.next,j.next=c,c=j)}if(e===l)return e;if(i<h){j=l-
+e;j>h-i&&(j=h-i);for(q=0;q<j;q++)a[d+e+q]=f[i+q];i+=j;e+=j;h===i&&(h=i=0)}return e},Ia=function(a,c,d){var f;if(!o){if(!y){z=r=0;var e,g;if(0===P[0].dl){H.dyn_tree=A;H.static_tree=I;H.extra_bits=ra;H.extra_base=257;H.elems=286;H.max_length=15;H.max_code=0;D.dyn_tree=K;D.static_tree=P;D.extra_bits=ga;D.extra_base=0;D.elems=30;D.max_length=15;D.max_code=0;U.dyn_tree=L;U.static_tree=null;U.extra_bits=Ha;U.extra_base=0;U.elems=19;U.max_length=7;for(g=e=U.max_code=0;28>g;g++){$[g]=e;for(f=0;f<1<<ra[g];f++)ba[e++]=
+g}ba[e-1]=g;for(g=e=0;16>g;g++){W[g]=e;for(f=0;f<1<<ga[g];f++)Q[e++]=g}for(e>>=7;30>g;g++){W[g]=e<<7;for(f=0;f<1<<ga[g]-7;f++)Q[256+e++]=g}for(f=0;15>=f;f++)N[f]=0;for(f=0;143>=f;)I[f++].dl=8,N[8]++;for(;255>=f;)I[f++].dl=9,N[9]++;for(;279>=f;)I[f++].dl=7,N[7]++;for(;287>=f;)I[f++].dl=8,N[8]++;Aa(I,287);for(f=0;30>f;f++)P[f].dl=5,P[f].fc=za(f,5);Fa()}for(f=0;8192>f;f++)t[32768+f]=0;F=ka[R].max_lazy;S=ka[R].good_length;M=ka[R].max_chain;s=l=0;B=xa(n,0,65536);if(0>=B)y=!0,B=0;else{for(y=!1;262>B&&!y;)sa();
+for(f=q=0;2>f;f++)q=(q<<5^n[f]&255)&8191}b=null;i=h=0;3>=R?(G=2,w=0):(w=2,C=0);j=!1}o=!0;if(0===B)return j=!0,0}if((f=Ga(a,c,d))===d)return d;if(j)return f;if(3>=R)for(;0!==B&&null===b;){na();0!==u&&32506>=l-u&&(w=ya(u),w>B&&(w=B));if(3<=w)if(g=ha(l-v,w-3),B-=w,w<=F){w--;do l++,na();while(0!==--w);l++}else l+=w,w=0,q=n[l]&255,q=(q<<5^n[l+1]&255)&8191;else g=ha(0,n[l]&255),B--,l++;g&&(oa(0),s=l);for(;262>B&&!y;)sa()}else for(;0!==B&&null===b;){na();G=w;E=v;w=2;0!==u&&G<F&&32506>=l-u&&(w=ya(u),w>B&&
+(w=B),3===w&&4096<l-v&&w--);if(3<=G&&w<=G){g=ha(l-1-E,G-3);B-=G-1;G-=2;do l++,na();while(0!==--G);C=0;w=2;l++;g&&(oa(0),s=l)}else 0!==C?ha(0,n[l-1]&255)&&(oa(0),s=l):C=1,l++,B--;for(;262>B&&!y;)sa()}0===B&&(0!==C&&ha(0,n[l-1]&255),oa(1),j=!0);return f+Ga(a,f+c,d-f)};this.deflate=function(e,l){var j,h;ja=e;qa=0;"undefined"===typeof l&&(l=6);(j=l)?1>j?j=1:9<j&&(j=9):j=6;R=j;y=o=!1;if(null===f){c=b=d=null;f=[];f.length=a;n=[];n.length=65536;x=[];x.length=8192;p=[];p.length=32832;t=[];t.length=65536;
+A=[];A.length=573;for(j=0;573>j;j++)A[j]=new g;K=[];K.length=61;for(j=0;61>j;j++)K[j]=new g;I=[];I.length=288;for(j=0;288>j;j++)I[j]=new g;P=[];P.length=30;for(j=0;30>j;j++)P[j]=new g;L=[];L.length=39;for(j=0;39>j;j++)L[j]=new g;H=new m;D=new m;U=new m;N=[];N.length=16;J=[];J.length=573;T=[];T.length=573;ba=[];ba.length=256;Q=[];Q.length=512;$=[];$.length=29;W=[];W.length=30;ea=[];ea.length=1024}for(var q=Array(1024),i=[];0<(j=Ia(q,0,q.length));){var s=[];s.length=j;for(h=0;h<j;h++)s[h]=String.fromCharCode(q[h]);
+i[i.length]=s.join("")}ja=null;return i.join("")}};
// Input 4
-core.ByteArray=function(i){this.pos=0;this.data=i;this.readUInt32LE=function(){var i=this.data,e=this.pos+=4;return i[--e]<<24|i[--e]<<16|i[--e]<<8|i[--e]};this.readUInt16LE=function(){var i=this.data,e=this.pos+=2;return i[--e]<<8|i[--e]}};
+core.ByteArray=function(g){this.pos=0;this.data=g;this.readUInt32LE=function(){var g=this.data,e=this.pos+=4;return g[--e]<<24|g[--e]<<16|g[--e]<<8|g[--e]};this.readUInt16LE=function(){var g=this.data,e=this.pos+=2;return g[--e]<<8|g[--e]}};
// Input 5
-core.ByteArrayWriter=function(i){var k=this,e=new runtime.ByteArray(0);this.appendByteArrayWriter=function(g){e=runtime.concatByteArrays(e,g.getByteArray())};this.appendByteArray=function(g){e=runtime.concatByteArrays(e,g)};this.appendArray=function(g){e=runtime.concatByteArrays(e,runtime.byteArrayFromArray(g))};this.appendUInt16LE=function(e){k.appendArray([e&255,e>>8&255])};this.appendUInt32LE=function(e){k.appendArray([e&255,e>>8&255,e>>16&255,e>>24&255])};this.appendString=function(g){e=runtime.concatByteArrays(e,
-runtime.byteArrayFromString(g,i))};this.getLength=function(){return e.length};this.getByteArray=function(){return e}};
+core.ByteArrayWriter=function(g){var m=this,e=new runtime.ByteArray(0);this.appendByteArrayWriter=function(g){e=runtime.concatByteArrays(e,g.getByteArray())};this.appendByteArray=function(g){e=runtime.concatByteArrays(e,g)};this.appendArray=function(g){e=runtime.concatByteArrays(e,runtime.byteArrayFromArray(g))};this.appendUInt16LE=function(e){m.appendArray([e&255,e>>8&255])};this.appendUInt32LE=function(e){m.appendArray([e&255,e>>8&255,e>>16&255,e>>24&255])};this.appendString=function(k){e=runtime.concatByteArrays(e,
+runtime.byteArrayFromString(k,g))};this.getLength=function(){return e.length};this.getByteArray=function(){return e}};
// Input 6
-core.RawInflate=function(){var i,k,e=null,g,a,b,h,c,d,f,j,p,m,l,u,n,q,r=[0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535],y=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,0,0],C=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,99,99],s=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577],v=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],E=[16,17,18,
-0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],B=function(){this.list=this.next=null},z=function(){this.n=this.b=this.e=0;this.t=null},G=function(a,c,b,f,d,j){this.BMAX=16;this.N_MAX=288;this.status=0;this.root=null;this.m=0;var e=Array(this.BMAX+1),h,g,q,t,o,r,i,w=Array(this.BMAX+1),x,m,n,p=new z,l=Array(this.BMAX);t=Array(this.N_MAX);var v,y=Array(this.BMAX+1),k,s,C;C=this.root=null;for(o=0;o<e.length;o++)e[o]=0;for(o=0;o<w.length;o++)w[o]=0;for(o=0;o<l.length;o++)l[o]=null;for(o=0;o<t.length;o++)t[o]=
-0;for(o=0;o<y.length;o++)y[o]=0;h=c>256?a[256]:this.BMAX;x=a;m=0;o=c;do e[x[m]]++,m++;while(--o>0);if(e[0]==c)this.root=null,this.status=this.m=0;else{for(r=1;r<=this.BMAX;r++)if(e[r]!=0)break;i=r;j<r&&(j=r);for(o=this.BMAX;o!=0;o--)if(e[o]!=0)break;q=o;j>o&&(j=o);for(k=1<<r;r<o;r++,k<<=1)if((k-=e[r])<0){this.status=2;this.m=j;return}if((k-=e[o])<0)this.status=2,this.m=j;else{e[o]+=k;y[1]=r=0;x=e;m=1;for(n=2;--o>0;)y[n++]=r+=x[m++];x=a;o=m=0;do if((r=x[m++])!=0)t[y[r]++]=o;while(++o<c);c=y[q];y[0]=
-o=0;x=t;m=0;t=-1;v=w[0]=0;n=null;for(s=0;i<=q;i++)for(a=e[i];a-- >0;){for(;i>v+w[1+t];){v+=w[1+t];t++;s=(s=q-v)>j?j:s;if((g=1<<(r=i-v))>a+1){g-=a+1;for(n=i;++r<s;){if((g<<=1)<=e[++n])break;g-=e[n]}}v+r>h&&v<h&&(r=h-v);s=1<<r;w[1+t]=r;n=Array(s);for(g=0;g<s;g++)n[g]=new z;C=C==null?this.root=new B:C.next=new B;C.next=null;C.list=n;l[t]=n;if(t>0)y[t]=o,p.b=w[t],p.e=16+r,p.t=n,r=(o&(1<<v)-1)>>v-w[t],l[t-1][r].e=p.e,l[t-1][r].b=p.b,l[t-1][r].n=p.n,l[t-1][r].t=p.t}p.b=i-v;m>=c?p.e=99:x[m]<b?(p.e=x[m]<
-256?16:15,p.n=x[m++]):(p.e=d[x[m]-b],p.n=f[x[m++]-b]);g=1<<i-v;for(r=o>>v;r<s;r+=g)n[r].e=p.e,n[r].b=p.b,n[r].n=p.n,n[r].t=p.t;for(r=1<<i-1;(o&r)!=0;r>>=1)o^=r;for(o^=r;(o&(1<<v)-1)!=y[t];)v-=w[t],t--}this.m=w[1];this.status=k!=0&&q!=1?1:0}}},o=function(a){for(;h<a;)b|=(n.length==q?-1:n[q++])<<h,h+=8},x=function(a){return b&r[a]},t=function(a){b>>=a;h-=a},w=function(a,b,d){var e,h,g;if(d==0)return 0;for(g=0;;){o(l);h=p.list[x(l)];for(e=h.e;e>16;){if(e==99)return-1;t(h.b);e-=16;o(e);h=h.t[x(e)];e=
-h.e}t(h.b);if(e==16)k&=32767,a[b+g++]=i[k++]=h.n;else{if(e==15)break;o(e);f=h.n+x(e);t(e);o(u);h=m.list[x(u)];for(e=h.e;e>16;){if(e==99)return-1;t(h.b);e-=16;o(e);h=h.t[x(e)];e=h.e}t(h.b);o(e);j=k-h.n-x(e);for(t(e);f>0&&g<d;)f--,j&=32767,k&=32767,a[b+g++]=i[k++]=i[j++]}if(g==d)return d}c=-1;return g},L,Q=function(a,c,b){var f,d,e,h,j,g,q,r=Array(316);for(f=0;f<r.length;f++)r[f]=0;o(5);g=257+x(5);t(5);o(5);q=1+x(5);t(5);o(4);f=4+x(4);t(4);if(g>286||q>30)return-1;for(d=0;d<f;d++)o(3),r[E[d]]=x(3),t(3);
-for(;d<19;d++)r[E[d]]=0;l=7;d=new G(r,19,19,null,null,l);if(d.status!=0)return-1;p=d.root;l=d.m;h=g+q;for(f=e=0;f<h;)if(o(l),j=p.list[x(l)],d=j.b,t(d),d=j.n,d<16)r[f++]=e=d;else if(d==16){o(2);d=3+x(2);t(2);if(f+d>h)return-1;for(;d-- >0;)r[f++]=e}else{d==17?(o(3),d=3+x(3),t(3)):(o(7),d=11+x(7),t(7));if(f+d>h)return-1;for(;d-- >0;)r[f++]=0;e=0}l=9;d=new G(r,g,257,y,C,l);if(l==0)d.status=1;if(d.status!=0)return-1;p=d.root;l=d.m;for(f=0;f<q;f++)r[f]=r[f+g];u=6;d=new G(r,q,0,s,v,u);m=d.root;u=d.m;return u==
-0&&g>257?-1:d.status!=0?-1:w(a,c,b)};this.inflate=function(r,z){i==null&&(i=Array(65536));h=b=k=0;c=-1;d=false;f=j=0;p=null;n=r;q=0;var B=new runtime.ByteArray(z);a:{var E,H;for(E=0;E<z;){if(d&&c==-1)break;if(f>0){if(c!=0)for(;f>0&&E<z;)f--,j&=32767,k&=32767,B[0+E++]=i[k++]=i[j++];else{for(;f>0&&E<z;)f--,k&=32767,o(8),B[0+E++]=i[k++]=x(8),t(8);f==0&&(c=-1)}if(E==z)break}if(c==-1){if(d)break;o(1);x(1)!=0&&(d=true);t(1);o(2);c=x(2);t(2);p=null;f=0}switch(c){case 0:H=B;var S=0+E,K=z-E,F=void 0,F=h&7;
-t(F);o(16);F=x(16);t(16);o(16);if(F!=(~b&65535))H=-1;else{t(16);f=F;for(F=0;f>0&&F<K;)f--,k&=32767,o(8),H[S+F++]=i[k++]=x(8),t(8);f==0&&(c=-1);H=F}break;case 1:if(p!=null)H=w(B,0+E,z-E);else b:{H=B;S=0+E;K=z-E;if(e==null){for(var D=void 0,F=Array(288),D=void 0,D=0;D<144;D++)F[D]=8;for(;D<256;D++)F[D]=9;for(;D<280;D++)F[D]=7;for(;D<288;D++)F[D]=8;a=7;D=new G(F,288,257,y,C,a);if(D.status!=0){alert("HufBuild error: "+D.status);H=-1;break b}e=D.root;a=D.m;for(D=0;D<30;D++)F[D]=5;L=5;D=new G(F,30,0,s,
-v,L);if(D.status>1){e=null;alert("HufBuild error: "+D.status);H=-1;break b}g=D.root;L=D.m}p=e;m=g;l=a;u=L;H=w(H,S,K)}break;case 2:H=p!=null?w(B,0+E,z-E):Q(B,0+E,z-E);break;default:H=-1}if(H==-1)break a;E+=H}}n=null;return B}};
+core.RawInflate=function(){var g,m,e=null,k,a,c,b,d,o,f,h,i,j,n,x,p,t,r=[0,1,3,7,15,31,63,127,255,511,1023,2047,4095,8191,16383,32767,65535],z=[3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258,0,0],s=[0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0,99,99],q=[1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577],u=[0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13],E=[16,17,18,
+0,8,7,9,6,10,5,11,4,12,3,13,2,14,1,15],C=function(){this.list=this.next=null},w=function(){this.n=this.b=this.e=0;this.t=null},G=function(a,c,b,f,d,e){this.BMAX=16;this.N_MAX=288;this.status=0;this.root=null;this.m=0;var j=Array(this.BMAX+1),l,h,q,i,g,s,o,v=Array(this.BMAX+1),n,y,u,t=new w,k=Array(this.BMAX);i=Array(this.N_MAX);var p,z=Array(this.BMAX+1),B,r,x;x=this.root=null;for(g=0;g<j.length;g++)j[g]=0;for(g=0;g<v.length;g++)v[g]=0;for(g=0;g<k.length;g++)k[g]=null;for(g=0;g<i.length;g++)i[g]=
+0;for(g=0;g<z.length;g++)z[g]=0;l=256<c?a[256]:this.BMAX;n=a;y=0;g=c;do j[n[y]]++,y++;while(0<--g);if(j[0]==c)this.root=null,this.status=this.m=0;else{for(s=1;s<=this.BMAX&&!(0!=j[s]);s++);o=s;e<s&&(e=s);for(g=this.BMAX;0!=g&&!(0!=j[g]);g--);q=g;e>g&&(e=g);for(B=1<<s;s<g;s++,B<<=1)if(0>(B-=j[s])){this.status=2;this.m=e;return}if(0>(B-=j[g]))this.status=2,this.m=e;else{j[g]+=B;z[1]=s=0;n=j;y=1;for(u=2;0<--g;)z[u++]=s+=n[y++];n=a;g=y=0;do if(0!=(s=n[y++]))i[z[s]++]=g;while(++g<c);c=z[q];z[0]=g=0;n=
+i;y=0;i=-1;p=v[0]=0;u=null;for(r=0;o<=q;o++)for(a=j[o];0<a--;){for(;o>p+v[1+i];){p+=v[1+i];i++;r=(r=q-p)>e?e:r;if((h=1<<(s=o-p))>a+1){h-=a+1;for(u=o;++s<r&&!((h<<=1)<=j[++u]);)h-=j[u]}p+s>l&&p<l&&(s=l-p);r=1<<s;v[1+i]=s;u=Array(r);for(h=0;h<r;h++)u[h]=new w;x=null==x?this.root=new C:x.next=new C;x.next=null;x.list=u;k[i]=u;0<i&&(z[i]=g,t.b=v[i],t.e=16+s,t.t=u,s=(g&(1<<p)-1)>>p-v[i],k[i-1][s].e=t.e,k[i-1][s].b=t.b,k[i-1][s].n=t.n,k[i-1][s].t=t.t)}t.b=o-p;y>=c?t.e=99:n[y]<b?(t.e=256>n[y]?16:15,t.n=
+n[y++]):(t.e=d[n[y]-b],t.n=f[n[y++]-b]);h=1<<o-p;for(s=g>>p;s<r;s+=h)u[s].e=t.e,u[s].b=t.b,u[s].n=t.n,u[s].t=t.t;for(s=1<<o-1;0!=(g&s);s>>=1)g^=s;for(g^=s;(g&(1<<p)-1)!=z[i];)p-=v[i],i--}this.m=v[1];this.status=0!=B&&1!=q?1:0}}},l=function(a){for(;b<a;)c|=(p.length==t?-1:p[t++])<<b,b+=8},v=function(a){return c&r[a]},y=function(a){c>>=a;b-=a},B=function(a,c,b){var e,s,q;if(0==b)return 0;for(q=0;;){l(n);s=i.list[v(n)];for(e=s.e;16<e;){if(99==e)return-1;y(s.b);e-=16;l(e);s=s.t[v(e)];e=s.e}y(s.b);if(16==
+e)m&=32767,a[c+q++]=g[m++]=s.n;else{if(15==e)break;l(e);f=s.n+v(e);y(e);l(x);s=j.list[v(x)];for(e=s.e;16<e;){if(99==e)return-1;y(s.b);e-=16;l(e);s=s.t[v(e)];e=s.e}y(s.b);l(e);h=m-s.n-v(e);for(y(e);0<f&&q<b;)f--,h&=32767,m&=32767,a[c+q++]=g[m++]=g[h++]}if(q==b)return b}d=-1;return q},M,F=function(a,c,b){var f,d,e,h,g,o,t,p=Array(316);for(f=0;f<p.length;f++)p[f]=0;l(5);o=257+v(5);y(5);l(5);t=1+v(5);y(5);l(4);f=4+v(4);y(4);if(286<o||30<t)return-1;for(d=0;d<f;d++)l(3),p[E[d]]=v(3),y(3);for(;19>d;d++)p[E[d]]=
+0;n=7;d=new G(p,19,19,null,null,n);if(0!=d.status)return-1;i=d.root;n=d.m;h=o+t;for(f=e=0;f<h;)if(l(n),g=i.list[v(n)],d=g.b,y(d),d=g.n,16>d)p[f++]=e=d;else if(16==d){l(2);d=3+v(2);y(2);if(f+d>h)return-1;for(;0<d--;)p[f++]=e}else{17==d?(l(3),d=3+v(3),y(3)):(l(7),d=11+v(7),y(7));if(f+d>h)return-1;for(;0<d--;)p[f++]=0;e=0}n=9;d=new G(p,o,257,z,s,n);0==n&&(d.status=1);if(0!=d.status)return-1;i=d.root;n=d.m;for(f=0;f<t;f++)p[f]=p[f+o];x=6;d=new G(p,t,0,q,u,x);j=d.root;x=d.m;return 0==x&&257<o||0!=d.status?
+-1:B(a,c,b)};this.inflate=function(r,w){null==g&&(g=Array(65536));b=c=m=0;d=-1;o=!1;f=h=0;i=null;p=r;t=0;var C=new runtime.ByteArray(w);a:{var E,I;for(E=0;E<w&&!(o&&-1==d);){if(0<f){if(0!=d)for(;0<f&&E<w;)f--,h&=32767,m&=32767,C[0+E++]=g[m++]=g[h++];else{for(;0<f&&E<w;)f--,m&=32767,l(8),C[0+E++]=g[m++]=v(8),y(8);0==f&&(d=-1)}if(E==w)break}if(-1==d){if(o)break;l(1);0!=v(1)&&(o=!0);y(1);l(2);d=v(2);y(2);i=null;f=0}switch(d){case 0:I=C;var P=0+E,L=w-E,H=void 0,H=b&7;y(H);l(16);H=v(16);y(16);l(16);if(H!=
+(~c&65535))I=-1;else{y(16);f=H;for(H=0;0<f&&H<L;)f--,m&=32767,l(8),I[P+H++]=g[m++]=v(8),y(8);0==f&&(d=-1);I=H}break;case 1:if(null!=i)I=B(C,0+E,w-E);else b:{I=C;P=0+E;L=w-E;if(null==e){for(var D=void 0,H=Array(288),D=void 0,D=0;144>D;D++)H[D]=8;for(;256>D;D++)H[D]=9;for(;280>D;D++)H[D]=7;for(;288>D;D++)H[D]=8;a=7;D=new G(H,288,257,z,s,a);if(0!=D.status){alert("HufBuild error: "+D.status);I=-1;break b}e=D.root;a=D.m;for(D=0;30>D;D++)H[D]=5;M=5;D=new G(H,30,0,q,u,M);if(1<D.status){e=null;alert("HufBuild error: "+
+D.status);I=-1;break b}k=D.root;M=D.m}i=e;j=k;n=a;x=M;I=B(I,P,L)}break;case 2:I=null!=i?B(C,0+E,w-E):F(C,0+E,w-E);break;default:I=-1}if(-1==I)break a;E+=I}}p=null;return C}};
// Input 7
-core.Cursor=function(i,k){function e(a,e){for(var c=e;c&&c!==a;)c=c.parentNode;return c||e}function g(){var b,h,c;if(a.parentNode){h=0;for(b=a.parentNode.firstChild;b&&b!==a;)h+=1,b=b.nextSibling;if(a.previousSibling&&a.previousSibling.nodeType===3&&a.nextSibling&&a.nextSibling.nodeType===3)c=a.nextSibling,a.previousSibling.appendData(c.nodeValue);for(b=0;b<i.rangeCount;b+=1){var d=i.getRangeAt(b),f=h,j=void 0,g=void 0,j=a.parentNode,g=e(a,d.startContainer);e(a,d.endContainer);g===a?d.setStart(j,
-f):g===j&&d.startOffset>f&&d.setStart(j,d.startOffset-1);d.endContainer===a?d.setEnd(j,f):d.endContainer===j&&d.endOffset>f&&d.setEnd(j,d.endOffset-1)}if(c){for(b=0;b<i.rangeCount;b+=1){var d=i.getRangeAt(b),f=a.previousSibling,j=c,g=h,m=f.length-j.length;d.startContainer===j?d.setStart(f,m+d.startOffset):d.startContainer===f.parentNode&&d.startOffset===g&&d.setStart(f,m);d.endContainer===j?d.setEnd(f,m+d.endOffset):d.endContainer===f.parentNode&&d.endOffset===g&&d.setEnd(f,m)}c.parentNode.removeChild(c)}a.parentNode.removeChild(a)}}
-var a;a=k.createElementNS("urn:webodf:names:cursor","cursor");this.getNode=function(){return a};this.updateToSelection=function(){g();if(i.focusNode){var b=i.focusNode,e=i.focusOffset;if(b.nodeType===3){var c,d,f,j;j=b.parentNode;e===0?j.insertBefore(a,b):e===b.length?j.appendChild(a):(c=b.length,d=b.nextSibling,f=k.createTextNode(b.substringData(e,c)),b.deleteData(e,c),d?j.insertBefore(f,d):j.appendChild(f),j.insertBefore(a,f))}else if(b.nodeType!==9){for(c=b.firstChild;c&&e;)c=c.nextSibling,e-=
-1;b.insertBefore(a,c)}}};this.remove=function(){g()}};
+core.Cursor=function(g,m){function e(a,b){for(var d=b;d&&d!==a;)d=d.parentNode;return d||b}function k(){var c,b,d;if(a.parentNode){b=0;for(c=a.parentNode.firstChild;c&&c!==a;)b+=1,c=c.nextSibling;a.previousSibling&&3===a.previousSibling.nodeType&&a.nextSibling&&3===a.nextSibling.nodeType&&(d=a.nextSibling,a.previousSibling.appendData(d.nodeValue));for(c=0;c<g.rangeCount;c+=1){var o=g.getRangeAt(c),f=b,h=void 0,i=void 0,h=a.parentNode,i=e(a,o.startContainer);e(a,o.endContainer);i===a?o.setStart(h,
+f):i===h&&o.startOffset>f&&o.setStart(h,o.startOffset-1);o.endContainer===a?o.setEnd(h,f):o.endContainer===h&&o.endOffset>f&&o.setEnd(h,o.endOffset-1)}if(d){for(c=0;c<g.rangeCount;c+=1){var o=g.getRangeAt(c),f=a.previousSibling,h=d,i=b,j=f.length-h.length;o.startContainer===h?o.setStart(f,j+o.startOffset):o.startContainer===f.parentNode&&o.startOffset===i&&o.setStart(f,j);o.endContainer===h?o.setEnd(f,j+o.endOffset):o.endContainer===f.parentNode&&o.endOffset===i&&o.setEnd(f,j)}d.parentNode.removeChild(d)}a.parentNode.removeChild(a)}}
+var a;a=m.createElementNS("urn:webodf:names:cursor","cursor");this.getNode=function(){return a};this.updateToSelection=function(){k();if(g.focusNode){var c=g.focusNode,b=g.focusOffset;if(3===c.nodeType){var d,e,f,h;h=c.parentNode;0===b?h.insertBefore(a,c):b===c.length?h.appendChild(a):(d=c.length,e=c.nextSibling,f=m.createTextNode(c.substringData(b,d)),c.deleteData(b,d),e?h.insertBefore(f,e):h.appendChild(f),h.insertBefore(a,f))}else if(9!==c.nodeType){for(d=c.firstChild;d&&b;)d=d.nextSibling,b-=
+1;c.insertBefore(a,d)}}};this.remove=function(){k()}};
// Input 8
core.UnitTest=function(){};core.UnitTest.prototype.setUp=function(){};core.UnitTest.prototype.tearDown=function(){};core.UnitTest.prototype.description=function(){};core.UnitTest.prototype.tests=function(){};core.UnitTest.prototype.asyncTests=function(){};
-core.UnitTestRunner=function(){function i(a){g+=1;runtime.log("fail",a)}function k(a,b){var e;try{if(a.length!==b.length)return false;for(e=0;e<a.length;e+=1)if(a[e]!==b[e])return false}catch(c){return false}return true}function e(a,b,e){(typeof b!=="string"||typeof e!=="string")&&runtime.log("WARN: shouldBe() expects string arguments");var c,d;try{d=eval(b)}catch(f){c=f}a=eval(e);c?i(b+" should be "+a+". Threw exception "+c):(a===0?d===a&&1/d===1/a:d===a||(typeof a==="number"&&isNaN(a)?typeof d===
-"number"&&isNaN(d):Object.prototype.toString.call(a)===Object.prototype.toString.call([])&&k(d,a)))?runtime.log("pass",b+" is "+e):typeof d===typeof a?i(b+" should be "+a+". Was "+(d===0&&1/d<0?"-0":String(d))+"."):i(b+" should be "+a+" (of type "+typeof a+"). Was "+d+" (of type "+typeof d+").")}var g=0;this.shouldBeNull=function(a,b){e(a,b,"null")};this.shouldBeNonNull=function(a,b){var e,c;try{c=eval(b)}catch(d){e=d}e?i(b+" should be non-null. Threw exception "+e):c!==null?runtime.log("pass",b+
-" is non-null."):i(b+" should be non-null. Was "+c)};this.shouldBe=e;this.countFailedTests=function(){return g}};
-core.UnitTester=function(){var i=0,k={};this.runTests=function(e,g){function a(e){if(e.length===0)k[b]=f,i+=c.countFailedTests(),g();else{p=e[0];var j=Runtime.getFunctionName(p);runtime.log("Running "+j);l=c.countFailedTests();d.setUp();p(function(){d.tearDown();f[j]=l===c.countFailedTests();a(e.slice(1))})}}var b=Runtime.getFunctionName(e),h,c=new core.UnitTestRunner,d=new e(c),f={},j,p,m,l;if(b.hasOwnProperty(k))runtime.log("Test "+b+" has already run.");else{runtime.log("Running "+b+": "+d.description());
-m=d.tests();for(j=0;j<m.length;j+=1)p=m[j],h=Runtime.getFunctionName(p),runtime.log("Running "+h),l=c.countFailedTests(),d.setUp(),p(),d.tearDown(),f[h]=l===c.countFailedTests();a(d.asyncTests())}};this.countFailedTests=function(){return i};this.results=function(){return k}};
+core.UnitTestRunner=function(){function g(a){k+=1;runtime.log("fail",a)}function m(a,c){var b;try{if(a.length!==c.length)return!1;for(b=0;b<a.length;b+=1)if(a[b]!==c[b])return!1}catch(d){return!1}return!0}function e(a,c,b){("string"!==typeof c||"string"!==typeof b)&&runtime.log("WARN: shouldBe() expects string arguments");var d,e;try{e=eval(c)}catch(f){d=f}a=eval(b);d?g(c+" should be "+a+". Threw exception "+d):(0===a?e===a&&1/e===1/a:e===a||("number"===typeof a&&isNaN(a)?"number"===typeof e&&isNaN(e):
+Object.prototype.toString.call(a)===Object.prototype.toString.call([])&&m(e,a)))?runtime.log("pass",c+" is "+b):typeof e===typeof a?g(c+" should be "+a+". Was "+(0===e&&0>1/e?"-0":""+e)+"."):g(c+" should be "+a+" (of type "+typeof a+"). Was "+e+" (of type "+typeof e+").")}var k=0;this.shouldBeNull=function(a,c){e(a,c,"null")};this.shouldBeNonNull=function(a,c){var b,d;try{d=eval(c)}catch(e){b=e}b?g(c+" should be non-null. Threw exception "+b):null!==d?runtime.log("pass",c+" is non-null."):g(c+" should be non-null. Was "+
+d)};this.shouldBe=e;this.countFailedTests=function(){return k}};
+core.UnitTester=function(){var g=0,m={};this.runTests=function(e,k){function a(b){if(0===b.length)m[c]=f,g+=d.countFailedTests(),k();else{i=b[0];var e=Runtime.getFunctionName(i);runtime.log("Running "+e);n=d.countFailedTests();o.setUp();i(function(){o.tearDown();f[e]=n===d.countFailedTests();a(b.slice(1))})}}var c=Runtime.getFunctionName(e),b,d=new core.UnitTestRunner,o=new e(d),f={},h,i,j,n;if(c.hasOwnProperty(m))runtime.log("Test "+c+" has already run.");else{runtime.log("Running "+c+": "+o.description());
+j=o.tests();for(h=0;h<j.length;h+=1)i=j[h],b=Runtime.getFunctionName(i),runtime.log("Running "+b),n=d.countFailedTests(),o.setUp(),i(),o.tearDown(),f[b]=n===d.countFailedTests();a(o.asyncTests())}};this.countFailedTests=function(){return g};this.results=function(){return m}};
// Input 9
-core.PointWalker=function(i){function k(a){for(var c=-1;a;)a=a.previousSibling,c+=1;return c}var e=i,g=null,a=i&&i.firstChild,b=0;this.setPoint=function(h,c){e=h;b=c;if(e.nodeType===3)g=a=null;else{for(a=e.firstChild;c;)c-=1,a=a.nextSibling;g=a?a.previousSibling:e.lastChild}};this.stepForward=function(){var h;if(e.nodeType===3&&(h=typeof e.nodeValue.length==="number"?e.nodeValue.length:e.nodeValue.length(),b<h))return b+=1,true;if(a)return a.nodeType===1?(e=a,g=null,a=e.firstChild,b=0):a.nodeType===
-3?(e=a,a=g=null,b=0):(g=a,a=a.nextSibling,b+=1),true;return e!==i?(g=e,a=g.nextSibling,e=e.parentNode,b=k(g)+1,true):false};this.stepBackward=function(){if(e.nodeType===3&&b>0)return b-=1,true;if(g)return g.nodeType===1?(e=g,g=e.lastChild,a=null,b=k(g)+1):g.nodeType===3?(e=g,a=g=null,b=typeof e.nodeValue.length==="number"?e.nodeValue.length:e.nodeValue.length()):(a=g,g=g.previousSibling,b-=1),true;return e!==i?(a=e,g=a.previousSibling,e=e.parentNode,b=k(a),true):false};this.node=function(){return e};
-this.position=function(){return b};this.precedingSibling=function(){return g};this.followingSibling=function(){return a}};
+core.PointWalker=function(g){function m(a){for(var c=-1;a;)a=a.previousSibling,c+=1;return c}var e=g,k=null,a=g&&g.firstChild,c=0;this.setPoint=function(b,d){e=b;c=d;if(3===e.nodeType)k=a=null;else{for(a=e.firstChild;d;)d-=1,a=a.nextSibling;k=a?a.previousSibling:e.lastChild}};this.stepForward=function(){var b;if(3===e.nodeType&&(b="number"===typeof e.nodeValue.length?e.nodeValue.length:e.nodeValue.length(),c<b))return c+=1,!0;if(a)return 1===a.nodeType?(e=a,k=null,a=e.firstChild,c=0):3===a.nodeType?
+(e=a,a=k=null,c=0):(k=a,a=a.nextSibling,c+=1),!0;return e!==g?(k=e,a=k.nextSibling,e=e.parentNode,c=m(k)+1,!0):!1};this.stepBackward=function(){if(3===e.nodeType&&0<c)return c-=1,!0;if(k)return 1===k.nodeType?(e=k,k=e.lastChild,a=null,c=m(k)+1):3===k.nodeType?(e=k,a=k=null,c="number"===typeof e.nodeValue.length?e.nodeValue.length:e.nodeValue.length()):(a=k,k=k.previousSibling,c-=1),!0;return e!==g?(a=e,k=a.previousSibling,e=e.parentNode,c=m(a),!0):!1};this.node=function(){return e};this.position=
+function(){return c};this.precedingSibling=function(){return k};this.followingSibling=function(){return a}};
// Input 10
-core.Async=function(){this.forEach=function(i,k,e){function g(a){h!==b&&(a?(h=b,e(a)):(h+=1,h===b&&e(null)))}var a,b=i.length,h=0;for(a=0;a<b;a+=1)k(i[a],g)}};
+core.Async=function(){this.forEach=function(g,m,e){function k(a){b!==c&&(a?(b=c,e(a)):(b+=1,b===c&&e(null)))}var a,c=g.length,b=0;for(a=0;a<c;a+=1)m(g[a],k)}};
// Input 11
-runtime.loadClass("core.RawInflate");runtime.loadClass("core.ByteArray");runtime.loadClass("core.ByteArrayWriter");
-core.Zip=function(i,k){function e(a){var c=[0,1996959894,3993919788,2567524794,124634137,1886057615,3915621685,2657392035,249268274,2044508324,3772115230,2547177864,162941995,2125561021,3887607047,2428444049,498536548,1789927666,4089016648,2227061214,450548861,1843258603,4107580753,2211677639,325883990,1684777152,4251122042,2321926636,335633487,1661365465,4195302755,2366115317,997073096,1281953886,3579855332,2724688242,1006888145,1258607687,3524101629,2768942443,901097722,1119000684,3686517206,2898065728,
+runtime.loadClass("core.RawInflate");runtime.loadClass("core.ByteArray");runtime.loadClass("core.ByteArrayWriter");runtime.loadClass("core.Base64");
+core.Zip=function(g,m){function e(a){var c=[0,1996959894,3993919788,2567524794,124634137,1886057615,3915621685,2657392035,249268274,2044508324,3772115230,2547177864,162941995,2125561021,3887607047,2428444049,498536548,1789927666,4089016648,2227061214,450548861,1843258603,4107580753,2211677639,325883990,1684777152,4251122042,2321926636,335633487,1661365465,4195302755,2366115317,997073096,1281953886,3579855332,2724688242,1006888145,1258607687,3524101629,2768942443,901097722,1119000684,3686517206,2898065728,
853044451,1172266101,3705015759,2882616665,651767980,1373503546,3369554304,3218104598,565507253,1454621731,3485111705,3099436303,671266974,1594198024,3322730930,2970347812,795835527,1483230225,3244367275,3060149565,1994146192,31158534,2563907772,4023717930,1907459465,112637215,2680153253,3904427059,2013776290,251722036,2517215374,3775830040,2137656763,141376813,2439277719,3865271297,1802195444,476864866,2238001368,4066508878,1812370925,453092731,2181625025,4111451223,1706088902,314042704,2344532202,
4240017532,1658658271,366619977,2362670323,4224994405,1303535960,984961486,2747007092,3569037538,1256170817,1037604311,2765210733,3554079995,1131014506,879679996,2909243462,3663771856,1141124467,855842277,2852801631,3708648649,1342533948,654459306,3188396048,3373015174,1466479909,544179635,3110523913,3462522015,1591671054,702138776,2966460450,3352799412,1504918807,783551873,3082640443,3233442989,3988292384,2596254646,62317068,1957810842,3939845945,2647816111,81470997,1943803523,3814918930,2489596804,
225274430,2053790376,3826175755,2466906013,167816743,2097651377,4027552580,2265490386,503444072,1762050814,4150417245,2154129355,426522225,1852507879,4275313526,2312317920,282753626,1742555852,4189708143,2394877945,397917763,1622183637,3604390888,2714866558,953729732,1340076626,3518719985,2797360999,1068828381,1219638859,3624741850,2936675148,906185462,1090812512,3747672003,2825379669,829329135,1181335161,3412177804,3160834842,628085408,1382605366,3423369109,3138078467,570562233,1426400815,3317316542,
2998733608,733239954,1555261956,3268935591,3050360625,752459403,1541320221,2607071920,3965973030,1969922972,40735498,2617837225,3943577151,1913087877,83908371,2512341634,3803740692,2075208622,213261112,2463272603,3855990285,2094854071,198958881,2262029012,4057260610,1759359992,534414190,2176718541,4139329115,1873836001,414664567,2282248934,4279200368,1711684554,285281116,2405801727,4167216745,1634467795,376229701,2685067896,3608007406,1308918612,956543938,2808555105,3495958263,1231636301,1047427035,
-2932959818,3654703836,1088359270,936918E3,2847714899,3736837829,1202900863,817233897,3183342108,3401237130,1404277552,615818150,3134207493,3453421203,1423857449,601450431,3009837614,3294710456,1567103746,711928724,3020668471,3272380065,1510334235,755167117],b=0,f,d=a.length,e=0,e=0;b^=-1;for(f=0;f<d;f+=1)e=(b^a[f])&255,e=c[e],b=b>>>8^e;return b^-1}function g(a){return new Date((a>>25&127)+1980,(a>>21&15)-1,a>>16&31,a>>11&15,a>>5&63,(a&31)<<1)}function a(a){var c=a.getFullYear();return c<1980?0:c-
-1980<<25|a.getMonth()+1<<21|a.getDate()<<16|a.getHours()<<11|a.getMinutes()<<5|a.getSeconds()>>1}function b(a,c){var b,f,d,e,j,h,i,m=this;this.load=function(c){if(m.data!==void 0)c(null,m.data);else{var d=j+34+b+f+256;d+i>p&&(d=p-i);runtime.read(a,i,d,function(b,f){if(b)c(b,f);else a:{var d=f,g=new core.ByteArray(d),o=g.readUInt32LE(),r;if(o!==67324752)c("File entry signature is wrong."+o.toString()+" "+d.length.toString(),null);else{g.pos+=22;o=g.readUInt16LE();r=g.readUInt16LE();g.pos+=o+r;if(e){d=
-d.slice(g.pos,g.pos+j);if(j!==d.length){c("The amount of compressed bytes read was "+d.length.toString()+" instead of "+j.toString()+" for "+m.filename+" in "+a+".",null);break a}d=l(d,h)}else d=d.slice(g.pos,g.pos+h);h!==d.length?c("The amount of bytes read was "+d.length.toString()+" instead of "+h.toString()+" for "+m.filename+" in "+a+".",null):(m.data=d,c(null,d))}}})}};this.set=function(a,c,b,f){m.filename=a;m.data=c;m.compressed=b;m.date=f};this.error=null;if(c)c.readUInt32LE()!==33639248?
-this.error="Central directory entry has wrong signature at position "+(c.pos-4).toString()+' for file "'+a+'": '+c.data.length.toString():(c.pos+=6,e=c.readUInt16LE(),this.date=g(c.readUInt32LE()),c.readUInt32LE(),j=c.readUInt32LE(),h=c.readUInt32LE(),b=c.readUInt16LE(),f=c.readUInt16LE(),d=c.readUInt16LE(),c.pos+=8,i=c.readUInt32LE(),this.filename=runtime.byteArrayToString(c.data.slice(c.pos,c.pos+b),"utf8"),c.pos+=b+f+d)}function h(a,c){if(a.length!==22)c("Central directory length should be 22.",
-u);else{var f=new core.ByteArray(a),d;d=f.readUInt32LE();d!==101010256?c("Central directory signature is wrong: "+d.toString(),u):f.readUInt16LE()!==0?c("Zip files with non-zero disk numbers are not supported.",u):f.readUInt16LE()!==0?c("Zip files with non-zero disk numbers are not supported.",u):(d=f.readUInt16LE(),m=f.readUInt16LE(),d!==m?c("Number of entries is inconsistent.",u):(d=f.readUInt32LE(),f=f.readUInt16LE(),f=p-22-d,runtime.read(i,f,p-f,function(a,f){a:{var d=new core.ByteArray(f),e,
-g;j=[];for(e=0;e<m;e+=1){g=new b(i,d);if(g.error){c(g.error,u);break a}j[j.length]=g}c(null,u)}})))}}function c(c){var b=new core.ByteArrayWriter("utf8"),f=0;b.appendArray([80,75,3,4,20,0,0,0,0,0]);if(c.data)f=c.data.length;b.appendUInt32LE(a(c.date));b.appendUInt32LE(e(c.data));b.appendUInt32LE(f);b.appendUInt32LE(f);b.appendUInt16LE(c.filename.length);b.appendUInt16LE(0);b.appendString(c.filename);c.data&&b.appendByteArray(c.data);return b}function d(c,b){var f=new core.ByteArrayWriter("utf8"),
-d=0;f.appendArray([80,75,1,2,20,0,20,0,0,0,0,0]);if(c.data)d=c.data.length;f.appendUInt32LE(a(c.date));f.appendUInt32LE(e(c.data));f.appendUInt32LE(d);f.appendUInt32LE(d);f.appendUInt16LE(c.filename.length);f.appendArray([0,0,0,0,0,0,0,0,0,0,0,0]);f.appendUInt32LE(b);f.appendString(c.filename);return f}function f(a,c){if(a===j.length)c(null);else{var b=j[a];b.data!==void 0?f(a+1,c):b.load(function(b){b?c(b):f(a+1,c)})}}var j,p,m,l=(new core.RawInflate).inflate,u=this;this.load=function(a,c){var b=
-null,f,d;for(d=0;d<j.length;d+=1)if(f=j[d],f.filename===a){b=f;break}b?b.data?c(null,b.data):b.load(c):c(a+" not found.",null)};this.save=function(a,c,f,d){var e,g;for(e=0;e<j.length;e+=1)if(g=j[e],g.filename===a){g.set(a,c,f,d);return}g=new b(i);g.set(a,c,f,d);j.push(g)};this.write=function(a){f(0,function(b){if(b)a(b);else{var b=new core.ByteArrayWriter("utf8"),f,e,g,h=[0];for(f=0;f<j.length;f+=1)b.appendByteArrayWriter(c(j[f])),h.push(b.getLength());g=b.getLength();for(f=0;f<j.length;f+=1)e=j[f],
-b.appendByteArrayWriter(d(e,h[f]));f=b.getLength()-g;b.appendArray([80,75,5,6,0,0,0,0]);b.appendUInt16LE(j.length);b.appendUInt16LE(j.length);b.appendUInt32LE(f);b.appendUInt32LE(g);b.appendArray([0,0]);runtime.writeFile(i,b.getByteArray(),a)}})};this.getEntries=function(){return j.slice()};p=-1;k===null?j=[]:runtime.getFileSize(i,function(a){p=a;p<0?k("File '"+i+"' cannot be read.",u):runtime.read(i,p-22,22,function(a,c){a||k===null?k(a,u):h(c,k)})})};
+2932959818,3654703836,1088359270,936918E3,2847714899,3736837829,1202900863,817233897,3183342108,3401237130,1404277552,615818150,3134207493,3453421203,1423857449,601450431,3009837614,3294710456,1567103746,711928724,3020668471,3272380065,1510334235,755167117],b,f,d=a.length,e=0,e=0;b=-1;for(f=0;f<d;f+=1)e=(b^a[f])&255,e=c[e],b=b>>>8^e;return b^-1}function k(a){return new Date((a>>25&127)+1980,(a>>21&15)-1,a>>16&31,a>>11&15,a>>5&63,(a&31)<<1)}function a(a){var c=a.getFullYear();return 1980>c?0:c-1980<<
+25|a.getMonth()+1<<21|a.getDate()<<16|a.getHours()<<11|a.getMinutes()<<5|a.getSeconds()>>1}function c(a,c){var b,f,d,e,j,h,l,g=this;this.load=function(c){if(void 0!==g.data)c(null,g.data);else{var d=j+34+b+f+256;d+l>n&&(d=n-l);runtime.read(a,l,d,function(b,f){if(b)c(b,f);else a:{var d=f,l=new core.ByteArray(d),i=l.readUInt32LE(),q;if(67324752!==i)c("File entry signature is wrong."+i.toString()+" "+d.length.toString(),null);else{l.pos+=22;i=l.readUInt16LE();q=l.readUInt16LE();l.pos+=i+q;if(e){d=d.slice(l.pos,
+l.pos+j);if(j!==d.length){c("The amount of compressed bytes read was "+d.length.toString()+" instead of "+j.toString()+" for "+g.filename+" in "+a+".",null);break a}d=p(d,h)}else d=d.slice(l.pos,l.pos+h);h!==d.length?c("The amount of bytes read was "+d.length.toString()+" instead of "+h.toString()+" for "+g.filename+" in "+a+".",null):(g.data=d,c(null,d))}}})}};this.set=function(a,c,b,d){g.filename=a;g.data=c;g.compressed=b;g.date=d};this.error=null;c&&(33639248!==c.readUInt32LE()?this.error="Central directory entry has wrong signature at position "+
+(c.pos-4).toString()+' for file "'+a+'": '+c.data.length.toString():(c.pos+=6,e=c.readUInt16LE(),this.date=k(c.readUInt32LE()),c.readUInt32LE(),j=c.readUInt32LE(),h=c.readUInt32LE(),b=c.readUInt16LE(),f=c.readUInt16LE(),d=c.readUInt16LE(),c.pos+=8,l=c.readUInt32LE(),this.filename=runtime.byteArrayToString(c.data.slice(c.pos,c.pos+b),"utf8"),c.pos+=b+f+d))}function b(a,b){if(22!==a.length)b("Central directory length should be 22.",t);else{var d=new core.ByteArray(a),f;f=d.readUInt32LE();101010256!==
+f?b("Central directory signature is wrong: "+f.toString(),t):0!==d.readUInt16LE()?b("Zip files with non-zero disk numbers are not supported.",t):0!==d.readUInt16LE()?b("Zip files with non-zero disk numbers are not supported.",t):(f=d.readUInt16LE(),x=d.readUInt16LE(),f!==x?b("Number of entries is inconsistent.",t):(f=d.readUInt32LE(),d=d.readUInt16LE(),d=n-22-f,runtime.read(g,d,n-d,function(a,d){a:{var f=new core.ByteArray(d),e,l;j=[];for(e=0;e<x;e+=1){l=new c(g,f);if(l.error){b(l.error,t);break a}j[j.length]=
+l}b(null,t)}})))}}function d(a,c){var b=null,d,f;for(f=0;f<j.length;f+=1)if(d=j[f],d.filename===a){b=d;break}b?b.data?c(null,b.data):b.load(c):c(a+" not found.",null)}function o(a,c){d(a,function(a,b){if(a)return c(a,null);b=runtime.byteArrayToString(b,"utf8");c(null,b)})}function f(c){var b=new core.ByteArrayWriter("utf8"),d=0;b.appendArray([80,75,3,4,20,0,0,0,0,0]);c.data&&(d=c.data.length);b.appendUInt32LE(a(c.date));b.appendUInt32LE(e(c.data));b.appendUInt32LE(d);b.appendUInt32LE(d);b.appendUInt16LE(c.filename.length);
+b.appendUInt16LE(0);b.appendString(c.filename);c.data&&b.appendByteArray(c.data);return b}function h(c,b){var d=new core.ByteArrayWriter("utf8"),f=0;d.appendArray([80,75,1,2,20,0,20,0,0,0,0,0]);c.data&&(f=c.data.length);d.appendUInt32LE(a(c.date));d.appendUInt32LE(e(c.data));d.appendUInt32LE(f);d.appendUInt32LE(f);d.appendUInt16LE(c.filename.length);d.appendArray([0,0,0,0,0,0,0,0,0,0,0,0]);d.appendUInt32LE(b);d.appendString(c.filename);return d}function i(a,c){if(a===j.length)c(null);else{var b=j[a];
+void 0!==b.data?i(a+1,c):b.load(function(b){b?c(b):i(a+1,c)})}}var j,n,x,p=(new core.RawInflate).inflate,t=this,r=new core.Base64;this.load=d;this.save=function(a,b,d,f){var e,h;for(e=0;e<j.length;e+=1)if(h=j[e],h.filename===a){h.set(a,b,d,f);return}h=new c(g);h.set(a,b,d,f);j.push(h)};this.write=function(a){i(0,function(c){if(c)a(c);else{var c=new core.ByteArrayWriter("utf8"),b,d,e,i=[0];for(b=0;b<j.length;b+=1)c.appendByteArrayWriter(f(j[b])),i.push(c.getLength());e=c.getLength();for(b=0;b<j.length;b+=
+1)d=j[b],c.appendByteArrayWriter(h(d,i[b]));b=c.getLength()-e;c.appendArray([80,75,5,6,0,0,0,0]);c.appendUInt16LE(j.length);c.appendUInt16LE(j.length);c.appendUInt32LE(b);c.appendUInt32LE(e);c.appendArray([0,0]);runtime.writeFile(g,c.getByteArray(),a)}})};this.loadContentXmlAsFragments=function(a,c){o(a,function(a,b){if(a)return c.rootElementReady(a);c.rootElementReady(null,b,!0)})};this.loadAsString=o;this.loadAsDOM=function(a,c){o(a,function(a,b){a?c(a,null):(b=(new DOMParser).parseFromString(b,
+"text/xml"),c(null,b))})};this.loadAsDataURL=function(a,c,b){d(a,function(a,d){if(a)return b(a,null);var f=0,e;c||(c=80===d[1]&&78===d[2]&&71===d[3]?"image/png":255===d[0]&&216===d[1]&&255===d[2]?"image/jpeg":71===d[0]&&73===d[1]&&70===d[2]?"image/gif":"");for(e="data:"+c+";base64,";f<d.length;)e+=r.convertUTF8ArrayToBase64(d.slice(f,Math.min(f+45E3,d.length))),f+=45E3;b(null,e)})};this.getEntries=function(){return j.slice()};n=-1;null===m?j=[]:runtime.getFileSize(g,function(a){n=a;0>n?m("File '"+
+g+"' cannot be read.",t):runtime.read(g,n-22,22,function(a,c){a||null===m?m(a,t):b(c,m)})})};
// Input 12
xmldom.LSSerializerFilter=function(){};
// Input 13
-typeof Object.create!=="function"&&(Object.create=function(i){var k=function(){};k.prototype=i;return new k});
-xmldom.LSSerializer=function(){function i(e,g){var a="",b=Object.create(e),h=k.filter?k.filter.acceptNode(g):1,c;if(h===1){c="";var d=g.attributes,f,j,p,m="",l;if(d){if(b[g.namespaceURI]!==g.prefix)b[g.namespaceURI]=g.prefix;c+="<"+g.nodeName;f=d.length;for(j=0;j<f;j+=1)if(p=d.item(j),p.namespaceURI!=="http://www.w3.org/2000/xmlns/"&&(l=k.filter?k.filter.acceptNode(p):1,l===1)){if(p.namespaceURI){l=p.prefix;var u=p.namespaceURI;b.hasOwnProperty(u)?l=b[u]+":":(b[u]!==l&&(b[u]=l),l+=":")}else l="";
-m+=" "+(l+p.localName+'="'+p.nodeValue+'"')}for(j in b)b.hasOwnProperty(j)&&((l=b[j])?l!=="xmlns"&&(c+=" xmlns:"+b[j]+'="'+j+'"'):c+=' xmlns="'+j+'"');c+=m+">"}a+=c}if(h===1||h===3){for(c=g.firstChild;c;)a+=i(b,c),c=c.nextSibling;g.nodeValue&&(a+=g.nodeValue)}h===1&&(b="",g.nodeType===1&&(b+="</"+g.nodeName+">"),a+=b);return a}var k=this;this.filter=null;this.writeToString=function(e,g){if(!e)return"";var a;if(g){a=g;var b={},h;for(h in a)a.hasOwnProperty(h)&&(b[a[h]]=h);a=b}else a={};return i(a,
+"function"!==typeof Object.create&&(Object.create=function(g){var m=function(){};m.prototype=g;return new m});
+xmldom.LSSerializer=function(){function g(e,k){var a="",c=Object.create(e),b=m.filter?m.filter.acceptNode(k):1,d;if(1===b){d="";var o=k.attributes,f,h,i,j="",n;if(o){c[k.namespaceURI]!==k.prefix&&(c[k.namespaceURI]=k.prefix);d+="<"+k.nodeName;f=o.length;for(h=0;h<f;h+=1)if(i=o.item(h),"http://www.w3.org/2000/xmlns/"!==i.namespaceURI&&(n=m.filter?m.filter.acceptNode(i):1,1===n)){if(i.namespaceURI){n=i.prefix;var x=i.namespaceURI;c.hasOwnProperty(x)?n=c[x]+":":(c[x]!==n&&(c[x]=n),n+=":")}else n="";
+j+=" "+(n+i.localName+'="'+i.nodeValue+'"')}for(h in c)c.hasOwnProperty(h)&&((n=c[h])?"xmlns"!==n&&(d+=" xmlns:"+c[h]+'="'+h+'"'):d+=' xmlns="'+h+'"');d+=j+">"}a+=d}if(1===b||3===b){for(d=k.firstChild;d;)a+=g(c,d),d=d.nextSibling;k.nodeValue&&(a+=k.nodeValue)}1===b&&(c="",1===k.nodeType&&(c+="</"+k.nodeName+">"),a+=c);return a}var m=this;this.filter=null;this.writeToString=function(e,k){if(!e)return"";var a;if(k){a=k;var c={},b;for(b in a)a.hasOwnProperty(b)&&(c[a[b]]=b);a=c}else a={};return g(a,
e)}};
// Input 14
-xmldom.RelaxNGParser=function(){function i(a,c){this.message=function(){c&&(a+=c.nodeType===1?" Element ":" Node ",a+=c.nodeName,c.nodeValue&&(a+=" with value '"+c.nodeValue+"'"),a+=".");return a}}function k(a){if(a.e.length<=2)return a;var c={name:a.name,e:a.e.slice(0,2)};return k({name:a.name,e:[c].concat(a.e.slice(2))})}function e(a){var a=a.split(":",2),b="",d;a.length===1?a=["",a[0]]:b=a[0];for(d in c)c[d]===b&&(a[0]=d);return a}function g(a,c){var j;var f;for(var b=0,d,h,i=a.name;a.e&&b<a.e.length;)if(d=
-a.e[b],d.name==="ref"){h=c[d.a.name];if(!h)throw d.a.name+" was not defined.";d=a.e.slice(b+1);a.e=a.e.slice(0,b);a.e=a.e.concat(h.e);a.e=a.e.concat(d)}else b+=1,g(d,c);d=a.e;if(i==="choice"&&(!d||!d[1]||d[1].name==="empty"))!d||!d[0]||d[0].name==="empty"?(delete a.e,a.name="empty"):(d[1]=d[0],d[0]={name:"empty"});if(i==="group"||i==="interleave")if(d[0].name==="empty")d[1].name==="empty"?(delete a.e,a.name="empty"):(i=a.name=d[1].name,a.names=d[1].names,f=a.e=d[1].e,d=f);else if(d[1].name==="empty")i=
-a.name=d[0].name,a.names=d[0].names,j=a.e=d[0].e,d=j;if(i==="oneOrMore"&&d[0].name==="empty")delete a.e,a.name="empty";if(i==="attribute"){h=a.names?a.names.length:0;for(var k,q=a.localnames=[h],r=a.namespaces=[h],b=0;b<h;b+=1)k=e(a.names[b]),r[b]=k[0],q[b]=k[1]}if(i==="interleave")if(d[0].name==="interleave")d[1].name==="interleave"?a.e=d[0].e.concat(d[1].e):a.e=[d[1]].concat(d[0].e);else if(d[1].name==="interleave")a.e=[d[0]].concat(d[1].e)}function a(c,b){for(var d=0,e;c.e&&d<c.e.length;)e=c.e[d],
-e.name==="elementref"?(e.id=e.id||0,c.e[d]=b[e.id]):e.name!=="element"&&a(e,b),d+=1}var b=this,h,c={"http://www.w3.org/XML/1998/namespace":"xml"},d;d=function(a,b,g){var h=[],i,u,n=a.localName,q=[];i=a.attributes;var r=n,y=q,C={},s,v;for(s=0;s<i.length;s+=1)if(v=i.item(s),v.namespaceURI){if(v.namespaceURI==="http://www.w3.org/2000/xmlns/")c[v.value]=v.localName}else{v.localName==="name"&&(r==="element"||r==="attribute")&&y.push(v.value);if(v.localName==="name"||v.localName==="combine"||v.localName===
-"type"){var E=v,B;B=v.value;B=B.replace(/^\s\s*/,"");for(var z=/\s/,G=B.length-1;z.test(B.charAt(G));)G-=1;B=B.slice(0,G+1);E.value=B}C[v.localName]=v.value}i=C;i.combine=i.combine||void 0;a=a.firstChild;r=h;y=q;for(C="";a;){if(a.nodeType===1&&a.namespaceURI==="http://relaxng.org/ns/structure/1.0"){if(s=d(a,b,r))s.name==="name"?y.push(c[s.a.ns]+":"+s.text):s.name==="choice"&&s.names&&s.names.length&&(y=y.concat(s.names),delete s.names),r.push(s)}else a.nodeType===3&&(C+=a.nodeValue);a=a.nextSibling}a=
-C;n!=="value"&&n!=="param"&&(a=/^\s*([\s\S]*\S)?\s*$/.exec(a)[1]);if(n==="value"&&i.type===void 0)i.type="token",i.datatypeLibrary="";if((n==="attribute"||n==="element")&&i.name!==void 0)u=e(i.name),h=[{name:"name",text:u[1],a:{ns:u[0]}}].concat(h),delete i.name;if(n==="name"||n==="nsName"||n==="value"){if(i.ns===void 0)i.ns=""}else delete i.ns;if(n==="name")u=e(a),i.ns=u[0],a=u[1];if(h.length>1&&(n==="define"||n==="oneOrMore"||n==="zeroOrMore"||n==="optional"||n==="list"||n==="mixed"))h=[{name:"group",
-e:k({name:"group",e:h}).e}];h.length>2&&n==="element"&&(h=[h[0]].concat({name:"group",e:k({name:"group",e:h.slice(1)}).e}));h.length===1&&n==="attribute"&&h.push({name:"text",text:a});if(h.length===1&&(n==="choice"||n==="group"||n==="interleave"))n=h[0].name,q=h[0].names,i=h[0].a,a=h[0].text,h=h[0].e;else if(h.length>2&&(n==="choice"||n==="group"||n==="interleave"))h=k({name:n,e:h}).e;n==="mixed"&&(n="interleave",h=[h[0],{name:"text"}]);n==="optional"&&(n="choice",h=[h[0],{name:"empty"}]);n==="zeroOrMore"&&
-(n="choice",h=[{name:"oneOrMore",e:[h[0]]},{name:"empty"}]);if(n==="define"&&i.combine){a:{r=i.combine;y=i.name;C=h;for(s=0;g&&s<g.length;s+=1)if(v=g[s],v.name==="define"&&v.a&&v.a.name===y){v.e=[{name:r,e:v.e.concat(C)}];g=v;break a}g=null}if(g)return}g={name:n};if(h&&h.length>0)g.e=h;for(u in i)if(i.hasOwnProperty(u)){g.a=i;break}if(a!==void 0)g.text=a;if(q&&q.length>0)g.names=q;if(n==="element")g.id=b.length,b.push(g),g={name:"elementref",id:g.id};return g};this.parseRelaxNGDOM=function(f,e){var k=
-[],m=d(f&&f.documentElement,k,void 0),l,u,n={};for(l=0;l<m.e.length;l+=1)u=m.e[l],u.name==="define"?n[u.a.name]=u:u.name==="start"&&(h=u);if(!h)return[new i("No Relax NG start element was found.")];g(h,n);for(l in n)n.hasOwnProperty(l)&&g(n[l],n);for(l=0;l<k.length;l+=1)g(k[l],n);if(e)b.rootPattern=e(h.e[0],k);a(h,k);for(l=0;l<k.length;l+=1)a(k[l],k);b.start=h;b.elements=k;b.nsmap=c;return null}};
+xmldom.RelaxNGParser=function(){function g(a,c){this.message=function(){c&&(a+=1===c.nodeType?" Element ":" Node ",a+=c.nodeName,c.nodeValue&&(a+=" with value '"+c.nodeValue+"'"),a+=".");return a}}function m(a){if(2>=a.e.length)return a;var c={name:a.name,e:a.e.slice(0,2)};return m({name:a.name,e:[c].concat(a.e.slice(2))})}function e(a){var a=a.split(":",2),c="",b;1===a.length?a=["",a[0]]:c=a[0];for(b in d)d[b]===c&&(a[0]=b);return a}function k(a,c){for(var b=0,d,g,o=a.name;a.e&&b<a.e.length;)if(d=
+a.e[b],"ref"===d.name){g=c[d.a.name];if(!g)throw d.a.name+" was not defined.";d=a.e.slice(b+1);a.e=a.e.slice(0,b);a.e=a.e.concat(g.e);a.e=a.e.concat(d)}else b+=1,k(d,c);d=a.e;if("choice"===o&&(!d||!d[1]||"empty"===d[1].name))!d||!d[0]||"empty"===d[0].name?(delete a.e,a.name="empty"):(d[1]=d[0],d[0]={name:"empty"});if("group"===o||"interleave"===o)"empty"===d[0].name?"empty"===d[1].name?(delete a.e,a.name="empty"):(o=a.name=d[1].name,a.names=d[1].names,d=a.e=d[1].e):"empty"===d[1].name&&(o=a.name=
+d[0].name,a.names=d[0].names,d=a.e=d[0].e);"oneOrMore"===o&&"empty"===d[0].name&&(delete a.e,a.name="empty");if("attribute"===o){g=a.names?a.names.length:0;for(var p,t=a.localnames=[g],r=a.namespaces=[g],b=0;b<g;b+=1)p=e(a.names[b]),r[b]=p[0],t[b]=p[1]}"interleave"===o&&("interleave"===d[0].name?"interleave"===d[1].name?a.e=d[0].e.concat(d[1].e):a.e=[d[1]].concat(d[0].e):"interleave"===d[1].name&&(a.e=[d[0]].concat(d[1].e)))}function a(c,d){for(var b=0,e;c.e&&b<c.e.length;)e=c.e[b],"elementref"===
+e.name?(e.id=e.id||0,c.e[b]=d[e.id]):"element"!==e.name&&a(e,d),b+=1}var c=this,b,d={"http://www.w3.org/XML/1998/namespace":"xml"},o;o=function(a,c,b){var g=[],n,k,p=a.localName,t=[];n=a.attributes;var r=p,z=t,s={},q,u;for(q=0;q<n.length;q+=1)if(u=n.item(q),u.namespaceURI)"http://www.w3.org/2000/xmlns/"===u.namespaceURI&&(d[u.value]=u.localName);else{"name"===u.localName&&("element"===r||"attribute"===r)&&z.push(u.value);if("name"===u.localName||"combine"===u.localName||"type"===u.localName){var E=
+u,C;C=u.value;C=C.replace(/^\s\s*/,"");for(var w=/\s/,G=C.length-1;w.test(C.charAt(G));)G-=1;C=C.slice(0,G+1);E.value=C}s[u.localName]=u.value}n=s;n.combine=n.combine||void 0;a=a.firstChild;r=g;z=t;for(s="";a;){if(1===a.nodeType&&"http://relaxng.org/ns/structure/1.0"===a.namespaceURI){if(q=o(a,c,r))"name"===q.name?z.push(d[q.a.ns]+":"+q.text):"choice"===q.name&&q.names&&q.names.length&&(z=z.concat(q.names),delete q.names),r.push(q)}else 3===a.nodeType&&(s+=a.nodeValue);a=a.nextSibling}a=s;"value"!==
+p&&"param"!==p&&(a=/^\s*([\s\S]*\S)?\s*$/.exec(a)[1]);"value"===p&&void 0===n.type&&(n.type="token",n.datatypeLibrary="");if(("attribute"===p||"element"===p)&&void 0!==n.name)k=e(n.name),g=[{name:"name",text:k[1],a:{ns:k[0]}}].concat(g),delete n.name;"name"===p||"nsName"===p||"value"===p?void 0===n.ns&&(n.ns=""):delete n.ns;"name"===p&&(k=e(a),n.ns=k[0],a=k[1]);if(1<g.length&&("define"===p||"oneOrMore"===p||"zeroOrMore"===p||"optional"===p||"list"===p||"mixed"===p))g=[{name:"group",e:m({name:"group",
+e:g}).e}];2<g.length&&"element"===p&&(g=[g[0]].concat({name:"group",e:m({name:"group",e:g.slice(1)}).e}));1===g.length&&"attribute"===p&&g.push({name:"text",text:a});if(1===g.length&&("choice"===p||"group"===p||"interleave"===p))p=g[0].name,t=g[0].names,n=g[0].a,a=g[0].text,g=g[0].e;else if(2<g.length&&("choice"===p||"group"===p||"interleave"===p))g=m({name:p,e:g}).e;"mixed"===p&&(p="interleave",g=[g[0],{name:"text"}]);"optional"===p&&(p="choice",g=[g[0],{name:"empty"}]);"zeroOrMore"===p&&(p="choice",
+g=[{name:"oneOrMore",e:[g[0]]},{name:"empty"}]);if("define"===p&&n.combine){a:{r=n.combine;z=n.name;s=g;for(q=0;b&&q<b.length;q+=1)if(u=b[q],"define"===u.name&&u.a&&u.a.name===z){u.e=[{name:r,e:u.e.concat(s)}];b=u;break a}b=null}if(b)return}b={name:p};g&&0<g.length&&(b.e=g);for(k in n)if(n.hasOwnProperty(k)){b.a=n;break}void 0!==a&&(b.text=a);t&&0<t.length&&(b.names=t);"element"===p&&(b.id=c.length,c.push(b),b={name:"elementref",id:b.id});return b};this.parseRelaxNGDOM=function(f,e){var i=[],j=o(f&&
+f.documentElement,i,void 0),n,m,p={};for(n=0;n<j.e.length;n+=1)m=j.e[n],"define"===m.name?p[m.a.name]=m:"start"===m.name&&(b=m);if(!b)return[new g("No Relax NG start element was found.")];k(b,p);for(n in p)p.hasOwnProperty(n)&&k(p[n],p);for(n=0;n<i.length;n+=1)k(i[n],p);e&&(c.rootPattern=e(b.e[0],i));a(b,i);for(n=0;n<i.length;n+=1)a(i[n],i);c.start=b;c.elements=i;c.nsmap=d;return null}};
// Input 15
runtime.loadClass("xmldom.RelaxNGParser");
-xmldom.RelaxNG=function(){function i(a){return function(){var c;return function(){c===void 0&&(c=a());return c}}()}function k(a,c){return function(){var b={},d=0;return function(f){var e=f.hash||f.toString(),g;g=b[e];if(g!==void 0)return g;b[e]=g=c(f);g.hash=a+d.toString();d+=1;return g}}()}function e(a){return function(){var c={};return function(b){var d,f;f=c[b.localName];if(f===void 0)c[b.localName]=f={};else if(d=f[b.namespaceURI],d!==void 0)return d;return f[b.namespaceURI]=d=a(b)}}()}function g(a,
-c,b){return function(){var d={},f=0;return function(e,g){var h=c&&c(e,g),j,i;if(h!==void 0)return h;h=e.hash||e.toString();j=g.hash||g.toString();i=d[h];if(i===void 0)d[h]=i={};else if(h=i[j],h!==void 0)return h;i[j]=h=b(e,g);h.hash=a+f.toString();f+=1;return h}}()}function a(c,b){b.p1.type==="choice"?a(c,b.p1):c[b.p1.hash]=b.p1;b.p2.type==="choice"?a(c,b.p2):c[b.p2.hash]=b.p2}function b(a,c){return{type:"element",nc:a,nullable:false,textDeriv:function(){return s},startTagOpenDeriv:function(b){return a.contains(b)?
-l(c,v):s},attDeriv:function(){return s},startTagCloseDeriv:function(){return this}}}function h(){return{type:"list",nullable:false,hash:"list",textDeriv:function(){return v}}}function c(a,b,d,e){if(b===s)return s;if(e>=d.length)return b;e===0&&(e=0);for(var g=d.item(e);g.namespaceURI===f;){e+=1;if(e>=d.length)return b;g=d.item(e)}return g=c(a,b.attDeriv(a,d.item(e)),d,e+1)}function d(a,c,b){b.e[0].a?(a.push(b.e[0].text),c.push(b.e[0].a.ns)):d(a,c,b.e[0]);b.e[1].a?(a.push(b.e[1].text),c.push(b.e[1].a.ns)):
-d(a,c,b.e[1])}var f="http://www.w3.org/2000/xmlns/",j,p,m,l,u,n,q,r,y,C,s={type:"notAllowed",nullable:false,hash:"notAllowed",textDeriv:function(){return s},startTagOpenDeriv:function(){return s},attDeriv:function(){return s},startTagCloseDeriv:function(){return s},endTagDeriv:function(){return s}},v={type:"empty",nullable:true,hash:"empty",textDeriv:function(){return s},startTagOpenDeriv:function(){return s},attDeriv:function(){return s},startTagCloseDeriv:function(){return v},endTagDeriv:function(){return s}},
-E={type:"text",nullable:true,hash:"text",textDeriv:function(){return E},startTagOpenDeriv:function(){return s},attDeriv:function(){return s},startTagCloseDeriv:function(){return E},endTagDeriv:function(){return s}},B,z,G;j=g("choice",function(a,c){if(a===s)return c;if(c===s)return a;if(a===c)return a},function(c,b){var d={},f;a(d,{p1:c,p2:b});b=c=void 0;for(f in d)d.hasOwnProperty(f)&&(c===void 0?c=d[f]:b=b===void 0?d[f]:j(b,d[f]));return function(a,c){return{type:"choice",p1:a,p2:c,nullable:a.nullable||
-c.nullable,textDeriv:function(b,d){return j(a.textDeriv(b,d),c.textDeriv(b,d))},startTagOpenDeriv:e(function(b){return j(a.startTagOpenDeriv(b),c.startTagOpenDeriv(b))}),attDeriv:function(b,d){return j(a.attDeriv(b,d),c.attDeriv(b,d))},startTagCloseDeriv:i(function(){return j(a.startTagCloseDeriv(),c.startTagCloseDeriv())}),endTagDeriv:i(function(){return j(a.endTagDeriv(),c.endTagDeriv())})}}(c,b)});p=function(a,c,b){return function(){var d={},f=0;return function(e,g){var h=c&&c(e,g),j,i;if(h!==
-void 0)return h;h=e.hash||e.toString();j=g.hash||g.toString();h<j&&(i=h,h=j,j=i,i=e,e=g,g=i);i=d[h];if(i===void 0)d[h]=i={};else if(h=i[j],h!==void 0)return h;i[j]=h=b(e,g);h.hash=a+f.toString();f+=1;return h}}()}("interleave",function(a,c){if(a===s||c===s)return s;if(a===v)return c;if(c===v)return a},function(a,c){return{type:"interleave",p1:a,p2:c,nullable:a.nullable&&c.nullable,textDeriv:function(b,d){return j(p(a.textDeriv(b,d),c),p(a,c.textDeriv(b,d)))},startTagOpenDeriv:e(function(b){return j(B(function(a){return p(a,
-c)},a.startTagOpenDeriv(b)),B(function(c){return p(a,c)},c.startTagOpenDeriv(b)))}),attDeriv:function(b,d){return j(p(a.attDeriv(b,d),c),p(a,c.attDeriv(b,d)))},startTagCloseDeriv:i(function(){return p(a.startTagCloseDeriv(),c.startTagCloseDeriv())})}});m=g("group",function(a,c){if(a===s||c===s)return s;if(a===v)return c;if(c===v)return a},function(a,c){return{type:"group",p1:a,p2:c,nullable:a.nullable&&c.nullable,textDeriv:function(b,d){var f=m(a.textDeriv(b,d),c);return a.nullable?j(f,c.textDeriv(b,
-d)):f},startTagOpenDeriv:function(b){var d=B(function(a){return m(a,c)},a.startTagOpenDeriv(b));return a.nullable?j(d,c.startTagOpenDeriv(b)):d},attDeriv:function(b,d){return j(m(a.attDeriv(b,d),c),m(a,c.attDeriv(b,d)))},startTagCloseDeriv:i(function(){return m(a.startTagCloseDeriv(),c.startTagCloseDeriv())})}});l=g("after",function(a,c){if(a===s||c===s)return s},function(a,c){return{type:"after",p1:a,p2:c,nullable:false,textDeriv:function(b,d){return l(a.textDeriv(b,d),c)},startTagOpenDeriv:e(function(b){return B(function(a){return l(a,
-c)},a.startTagOpenDeriv(b))}),attDeriv:function(b,d){return l(a.attDeriv(b,d),c)},startTagCloseDeriv:i(function(){return l(a.startTagCloseDeriv(),c)}),endTagDeriv:i(function(){return a.nullable?c:s})}});u=k("oneormore",function(a){return a===s?s:{type:"oneOrMore",p:a,nullable:a.nullable,textDeriv:function(c,b){return m(a.textDeriv(c,b),j(this,v))},startTagOpenDeriv:function(c){var b=this;return B(function(a){return m(a,j(b,v))},a.startTagOpenDeriv(c))},attDeriv:function(c,b){return m(a.attDeriv(c,
-b),j(this,v))},startTagCloseDeriv:i(function(){return u(a.startTagCloseDeriv())})}});q=g("attribute",void 0,function(a,c){return{type:"attribute",nullable:false,nc:a,p:c,attDeriv:function(b,d){return a.contains(d)&&(c.nullable&&/^\s+$/.test(d.nodeValue)||c.textDeriv(b,d.nodeValue).nullable)?v:s},startTagCloseDeriv:function(){return s}}});n=k("value",function(a){return{type:"value",nullable:false,value:a,textDeriv:function(c,b){return b===a?v:s},attDeriv:function(){return s},startTagCloseDeriv:function(){return this}}});
-y=k("data",function(a){return{type:"data",nullable:false,dataType:a,textDeriv:function(){return v},attDeriv:function(){return s},startTagCloseDeriv:function(){return this}}});B=function x(a,c){if(c.type==="after")return l(c.p1,a(c.p2));else if(c.type==="choice")return j(x(a,c.p1),x(a,c.p2));return c};z=function(a,b,d){for(var f=d.currentNode,b=b.startTagOpenDeriv(f),b=c(a,b,f.attributes,0),e=b=b.startTagCloseDeriv(),f=d.currentNode,b=d.firstChild(),g=0,h=[];b;)b.nodeType===1?h.push(b):b.nodeType===
-3&&!/^\s*$/.test(b.nodeValue)&&(h.push(b.nodeValue),g+=1),b=d.nextSibling();h.length===0&&(h=[""]);g=e;for(e=0;g!==s&&e<h.length;e+=1)b=h[e],typeof b==="string"?g=/^\s*$/.test(b)?j(g,g.textDeriv(a,b)):g.textDeriv(a,b):(d.currentNode=b,g=z(a,g,d));d.currentNode=f;return b=g.endTagDeriv()};r=function(a){var c,b,f;if(a.name==="name")return c=a.text,b=a.a.ns,{name:c,ns:b,hash:"{"+b+"}"+c,contains:function(a){return a.namespaceURI===b&&a.localName===c}};else if(a.name==="choice"){c=[];b=[];d(c,b,a);a=
-"";for(f=0;f<c.length;f+=1)a+="{"+b[f]+"}"+c[f]+",";return{hash:a,contains:function(a){var d;for(d=0;d<c.length;d+=1)if(c[d]===a.localName&&b[d]===a.namespaceURI)return true;return false}}}return{hash:"anyName",contains:function(){return true}}};C=function t(a,c){var d,f;if(a.name==="elementref"){d=a.id||0;a=c[d];if(a.name!==void 0){var e=a;d=c[e.id]={hash:"element"+e.id.toString()};e=b(r(e.e[0]),C(e.e[1],c));for(f in e)e.hasOwnProperty(f)&&(d[f]=e[f]);f=d}else f=a;return f}switch(a.name){case "empty":return v;
-case "notAllowed":return s;case "text":return E;case "choice":return j(t(a.e[0],c),t(a.e[1],c));case "interleave":d=t(a.e[0],c);for(f=1;f<a.e.length;f+=1)d=p(d,t(a.e[f],c));return d;case "group":return m(t(a.e[0],c),t(a.e[1],c));case "oneOrMore":return u(t(a.e[0],c));case "attribute":return q(r(a.e[0]),t(a.e[1],c));case "value":return n(a.text);case "data":return d=a.a&&a.a.type,d===void 0&&(d=""),y(d);case "list":return h()}throw"No support for "+a.name;};this.makePattern=function(a,c){var b={},
-d;for(d in c)c.hasOwnProperty(d)&&(b[d]=c[d]);return d=C(a,b)};this.validate=function(a,c){var b;a.currentNode=a.root;b=z(null,G,a);b.nullable?c(null):(runtime.log("Error in Relax NG validation: "+b),c(["Error in Relax NG validation: "+b]))};this.init=function(a){G=a}};
+xmldom.RelaxNG=function(){function g(a){return function(){var c;return function(){void 0===c&&(c=a());return c}}()}function m(a,c){return function(){var b={},d=0;return function(f){var e=f.hash||f.toString(),g;g=b[e];if(void 0!==g)return g;b[e]=g=c(f);g.hash=a+d.toString();d+=1;return g}}()}function e(a){return function(){var c={};return function(b){var d,f;f=c[b.localName];if(void 0===f)c[b.localName]=f={};else if(d=f[b.namespaceURI],void 0!==d)return d;return f[b.namespaceURI]=d=a(b)}}()}function k(a,
+c,b){return function(){var d={},f=0;return function(e,g){var h=c&&c(e,g),j,i;if(void 0!==h)return h;h=e.hash||e.toString();j=g.hash||g.toString();i=d[h];if(void 0===i)d[h]=i={};else if(h=i[j],void 0!==h)return h;i[j]=h=b(e,g);h.hash=a+f.toString();f+=1;return h}}()}function a(c,b){"choice"===b.p1.type?a(c,b.p1):c[b.p1.hash]=b.p1;"choice"===b.p2.type?a(c,b.p2):c[b.p2.hash]=b.p2}function c(a,c){return{type:"element",nc:a,nullable:!1,textDeriv:function(){return q},startTagOpenDeriv:function(b){return a.contains(b)?
+n(c,u):q},attDeriv:function(){return q},startTagCloseDeriv:function(){return this}}}function b(){return{type:"list",nullable:!1,hash:"list",textDeriv:function(){return u}}}function d(a,c,b,e){if(c===q)return q;if(e>=b.length)return c;0===e&&(e=0);for(var g=b.item(e);g.namespaceURI===f;){e+=1;if(e>=b.length)return c;g=b.item(e)}return g=d(a,c.attDeriv(a,b.item(e)),b,e+1)}function o(a,c,b){b.e[0].a?(a.push(b.e[0].text),c.push(b.e[0].a.ns)):o(a,c,b.e[0]);b.e[1].a?(a.push(b.e[1].text),c.push(b.e[1].a.ns)):
+o(a,c,b.e[1])}var f="http://www.w3.org/2000/xmlns/",h,i,j,n,x,p,t,r,z,s,q={type:"notAllowed",nullable:!1,hash:"notAllowed",textDeriv:function(){return q},startTagOpenDeriv:function(){return q},attDeriv:function(){return q},startTagCloseDeriv:function(){return q},endTagDeriv:function(){return q}},u={type:"empty",nullable:!0,hash:"empty",textDeriv:function(){return q},startTagOpenDeriv:function(){return q},attDeriv:function(){return q},startTagCloseDeriv:function(){return u},endTagDeriv:function(){return q}},
+E={type:"text",nullable:!0,hash:"text",textDeriv:function(){return E},startTagOpenDeriv:function(){return q},attDeriv:function(){return q},startTagCloseDeriv:function(){return E},endTagDeriv:function(){return q}},C,w,G;h=k("choice",function(a,b){if(a===q)return b;if(b===q||a===b)return a},function(b,c){var d={},f;a(d,{p1:b,p2:c});c=b=void 0;for(f in d)d.hasOwnProperty(f)&&(void 0===b?b=d[f]:c=void 0===c?d[f]:h(c,d[f]));return function(a,b){return{type:"choice",p1:a,p2:b,nullable:a.nullable||b.nullable,
+textDeriv:function(c,d){return h(a.textDeriv(c,d),b.textDeriv(c,d))},startTagOpenDeriv:e(function(c){return h(a.startTagOpenDeriv(c),b.startTagOpenDeriv(c))}),attDeriv:function(c,d){return h(a.attDeriv(c,d),b.attDeriv(c,d))},startTagCloseDeriv:g(function(){return h(a.startTagCloseDeriv(),b.startTagCloseDeriv())}),endTagDeriv:g(function(){return h(a.endTagDeriv(),b.endTagDeriv())})}}(b,c)});i=function(a,b,c){return function(){var d={},f=0;return function(e,g){var h=b&&b(e,g),j,i;if(void 0!==h)return h;
+h=e.hash||e.toString();j=g.hash||g.toString();h<j&&(i=h,h=j,j=i,i=e,e=g,g=i);i=d[h];if(void 0===i)d[h]=i={};else if(h=i[j],void 0!==h)return h;i[j]=h=c(e,g);h.hash=a+f.toString();f+=1;return h}}()}("interleave",function(a,b){if(a===q||b===q)return q;if(a===u)return b;if(b===u)return a},function(a,b){return{type:"interleave",p1:a,p2:b,nullable:a.nullable&&b.nullable,textDeriv:function(c,d){return h(i(a.textDeriv(c,d),b),i(a,b.textDeriv(c,d)))},startTagOpenDeriv:e(function(c){return h(C(function(a){return i(a,
+b)},a.startTagOpenDeriv(c)),C(function(b){return i(a,b)},b.startTagOpenDeriv(c)))}),attDeriv:function(c,d){return h(i(a.attDeriv(c,d),b),i(a,b.attDeriv(c,d)))},startTagCloseDeriv:g(function(){return i(a.startTagCloseDeriv(),b.startTagCloseDeriv())})}});j=k("group",function(a,b){if(a===q||b===q)return q;if(a===u)return b;if(b===u)return a},function(a,b){return{type:"group",p1:a,p2:b,nullable:a.nullable&&b.nullable,textDeriv:function(c,d){var f=j(a.textDeriv(c,d),b);return a.nullable?h(f,b.textDeriv(c,
+d)):f},startTagOpenDeriv:function(c){var d=C(function(a){return j(a,b)},a.startTagOpenDeriv(c));return a.nullable?h(d,b.startTagOpenDeriv(c)):d},attDeriv:function(c,d){return h(j(a.attDeriv(c,d),b),j(a,b.attDeriv(c,d)))},startTagCloseDeriv:g(function(){return j(a.startTagCloseDeriv(),b.startTagCloseDeriv())})}});n=k("after",function(a,b){if(a===q||b===q)return q},function(a,b){return{type:"after",p1:a,p2:b,nullable:!1,textDeriv:function(c,d){return n(a.textDeriv(c,d),b)},startTagOpenDeriv:e(function(c){return C(function(a){return n(a,
+b)},a.startTagOpenDeriv(c))}),attDeriv:function(c,d){return n(a.attDeriv(c,d),b)},startTagCloseDeriv:g(function(){return n(a.startTagCloseDeriv(),b)}),endTagDeriv:g(function(){return a.nullable?b:q})}});x=m("oneormore",function(a){return a===q?q:{type:"oneOrMore",p:a,nullable:a.nullable,textDeriv:function(b,c){return j(a.textDeriv(b,c),h(this,u))},startTagOpenDeriv:function(b){var c=this;return C(function(a){return j(a,h(c,u))},a.startTagOpenDeriv(b))},attDeriv:function(b,c){return j(a.attDeriv(b,
+c),h(this,u))},startTagCloseDeriv:g(function(){return x(a.startTagCloseDeriv())})}});t=k("attribute",void 0,function(a,b){return{type:"attribute",nullable:!1,nc:a,p:b,attDeriv:function(c,d){return a.contains(d)&&(b.nullable&&/^\s+$/.test(d.nodeValue)||b.textDeriv(c,d.nodeValue).nullable)?u:q},startTagCloseDeriv:function(){return q}}});p=m("value",function(a){return{type:"value",nullable:!1,value:a,textDeriv:function(b,c){return c===a?u:q},attDeriv:function(){return q},startTagCloseDeriv:function(){return this}}});
+z=m("data",function(a){return{type:"data",nullable:!1,dataType:a,textDeriv:function(){return u},attDeriv:function(){return q},startTagCloseDeriv:function(){return this}}});C=function v(a,b){return"after"===b.type?n(b.p1,a(b.p2)):"choice"===b.type?h(v(a,b.p1),v(a,b.p2)):b};w=function(a,b,c){for(var f=c.currentNode,b=b.startTagOpenDeriv(f),b=d(a,b,f.attributes,0),e=b=b.startTagCloseDeriv(),f=c.currentNode,b=c.firstChild(),g=[],j;b;)1===b.nodeType?g.push(b):3===b.nodeType&&!/^\s*$/.test(b.nodeValue)&&
+g.push(b.nodeValue),b=c.nextSibling();0===g.length&&(g=[""]);j=e;for(e=0;j!==q&&e<g.length;e+=1)b=g[e],"string"===typeof b?j=/^\s*$/.test(b)?h(j,j.textDeriv(a,b)):j.textDeriv(a,b):(c.currentNode=b,j=w(a,j,c));c.currentNode=f;return b=j.endTagDeriv()};r=function(a){var b,c,d;if("name"===a.name)return b=a.text,c=a.a.ns,{name:b,ns:c,hash:"{"+c+"}"+b,contains:function(a){return a.namespaceURI===c&&a.localName===b}};if("choice"===a.name){b=[];c=[];o(b,c,a);a="";for(d=0;d<b.length;d+=1)a+="{"+c[d]+"}"+
+b[d]+",";return{hash:a,contains:function(a){var d;for(d=0;d<b.length;d+=1)if(b[d]===a.localName&&c[d]===a.namespaceURI)return!0;return!1}}}return{hash:"anyName",contains:function(){return!0}}};s=function y(a,d){var f,e;if("elementref"===a.name){f=a.id||0;a=d[f];if(void 0!==a.name){var g=a;f=d[g.id]={hash:"element"+g.id.toString()};g=c(r(g.e[0]),s(g.e[1],d));for(e in g)g.hasOwnProperty(e)&&(f[e]=g[e]);e=f}else e=a;return e}switch(a.name){case "empty":return u;case "notAllowed":return q;case "text":return E;
+case "choice":return h(y(a.e[0],d),y(a.e[1],d));case "interleave":f=y(a.e[0],d);for(e=1;e<a.e.length;e+=1)f=i(f,y(a.e[e],d));return f;case "group":return j(y(a.e[0],d),y(a.e[1],d));case "oneOrMore":return x(y(a.e[0],d));case "attribute":return t(r(a.e[0]),y(a.e[1],d));case "value":return p(a.text);case "data":return f=a.a&&a.a.type,void 0===f&&(f=""),z(f);case "list":return b()}throw"No support for "+a.name;};this.makePattern=function(a,b){var c={},d;for(d in b)b.hasOwnProperty(d)&&(c[d]=b[d]);return d=
+s(a,c)};this.validate=function(a,b){var c;a.currentNode=a.root;c=w(null,G,a);c.nullable?b(null):(runtime.log("Error in Relax NG validation: "+c),b(["Error in Relax NG validation: "+c]))};this.init=function(a){G=a}};
// Input 16
runtime.loadClass("xmldom.RelaxNGParser");
-xmldom.RelaxNG2=function(){function i(a,b){this.message=function(){b&&(a+=b.nodeType===1?" Element ":" Node ",a+=b.nodeName,b.nodeValue&&(a+=" with value '"+b.nodeValue+"'"),a+=".");return a}}function k(c,b,f,e){return c.name==="empty"?null:a(c,b,f,e)}function e(a,d){if(a.e.length!==2)throw"Element with wrong # of elements: "+a.e.length;h+=1;for(var f=d.currentNode,e=f?f.nodeType:0,g=null;e>1;){if(e!==8&&(e!==3||!/^\s+$/.test(d.currentNode.nodeValue)))return h-=1,[new i("Not allowed node of type "+
-e+".")];e=(f=d.nextSibling())?f.nodeType:0}if(!f)return h-=1,[new i("Missing element "+a.names)];if(a.names&&a.names.indexOf(b[f.namespaceURI]+":"+f.localName)===-1)return h-=1,[new i("Found "+f.nodeName+" instead of "+a.names+".",f)];if(d.firstChild()){for(g=k(a.e[1],d,f);d.nextSibling();)if(e=d.currentNode.nodeType,(!d.currentNode||!(d.currentNode.nodeType===3&&/^\s+$/.test(d.currentNode.nodeValue)))&&e!==8)return h-=1,[new i("Spurious content.",d.currentNode)];if(d.parentNode()!==f)return h-=1,
-[new i("Implementation error.")]}else g=k(a.e[1],d,f);h-=1;d.nextSibling();return g}var g,a,b,h=0;a=function(b,d,f,g){var h=b.name,m=null;if(h==="text")a:{for(var l=(b=d.currentNode)?b.nodeType:0;b!==f&&l!==3;){if(l===1){m=[new i("Element not allowed here.",b)];break a}l=(b=d.nextSibling())?b.nodeType:0}d.nextSibling();m=null}else if(h==="data")m=null;else if(h==="value")g!==b.text&&(m=[new i("Wrong value, should be '"+b.text+"', not '"+g+"'",f)]);else if(h==="list")m=null;else if(h==="attribute")a:{if(b.e.length!==
-2)throw"Attribute with wrong # of elements: "+b.e.length;h=b.localnames.length;for(m=0;m<h;m+=1){g=f.getAttributeNS(b.namespaces[m],b.localnames[m]);g===""&&!f.hasAttributeNS(b.namespaces[m],b.localnames[m])&&(g=void 0);if(l!==void 0&&g!==void 0){m=[new i("Attribute defined too often.",f)];break a}l=g}m=l===void 0?[new i("Attribute not found: "+b.names,f)]:k(b.e[1],d,f,l)}else if(h==="element")m=e(b,d,f);else if(h==="oneOrMore"){g=0;do l=d.currentNode,h=a(b.e[0],d,f),g+=1;while(!h&&l!==d.currentNode);
-g>1?(d.currentNode=l,m=null):m=h}else if(h==="choice"){if(b.e.length!==2)throw"Choice with wrong # of options: "+b.e.length;l=d.currentNode;if(b.e[0].name==="empty"){if(h=a(b.e[1],d,f,g))d.currentNode=l;m=null}else{if(h=k(b.e[0],d,f,g))d.currentNode=l,h=a(b.e[1],d,f,g);m=h}}else if(h==="group"){if(b.e.length!==2)throw"Group with wrong # of members: "+b.e.length;m=a(b.e[0],d,f)||a(b.e[1],d,f)}else if(h==="interleave")a:{for(var l=b.e.length,g=[l],u=l,n,q,r,y;u>0;){n=0;q=d.currentNode;for(m=0;m<l;m+=
-1)if(r=d.currentNode,g[m]!==true&&g[m]!==r)y=b.e[m],(h=a(y,d,f))?(d.currentNode=r,g[m]===void 0&&(g[m]=false)):r===d.currentNode||y.name==="oneOrMore"||y.name==="choice"&&(y.e[0].name==="oneOrMore"||y.e[1].name==="oneOrMore")?(n+=1,g[m]=r):(n+=1,g[m]=true);if(q===d.currentNode&&n===u)break;if(n===0){for(m=0;m<l;m+=1)if(g[m]===false){m=[new i("Interleave does not match.",f)];break a}break}for(m=u=0;m<l;m+=1)g[m]!==true&&(u+=1)}m=null}else throw h+" not allowed in nonEmptyPattern.";return m};this.validate=
-function(a,b){a.currentNode=a.root;var f=k(g.e[0],a,a.root);b(f)};this.init=function(a,d){g=a;b=d}};
+xmldom.RelaxNG2=function(){function g(a,c){this.message=function(){c&&(a+=1===c.nodeType?" Element ":" Node ",a+=c.nodeName,c.nodeValue&&(a+=" with value '"+c.nodeValue+"'"),a+=".");return a}}function m(b,c,e,f){return"empty"===b.name?null:a(b,c,e,f)}function e(a,d){if(2!==a.e.length)throw"Element with wrong # of elements: "+a.e.length;for(var e=d.currentNode,f=e?e.nodeType:0,h=null;1<f;){if(8!==f&&(3!==f||!/^\s+$/.test(d.currentNode.nodeValue)))return[new g("Not allowed node of type "+f+".")];f=
+(e=d.nextSibling())?e.nodeType:0}if(!e)return[new g("Missing element "+a.names)];if(a.names&&-1===a.names.indexOf(c[e.namespaceURI]+":"+e.localName))return[new g("Found "+e.nodeName+" instead of "+a.names+".",e)];if(d.firstChild()){for(h=m(a.e[1],d,e);d.nextSibling();)if(f=d.currentNode.nodeType,(!d.currentNode||!(3===d.currentNode.nodeType&&/^\s+$/.test(d.currentNode.nodeValue)))&&8!==f)return[new g("Spurious content.",d.currentNode)];if(d.parentNode()!==e)return[new g("Implementation error.")]}else h=
+m(a.e[1],d,e);d.nextSibling();return h}var k,a,c;a=function(b,c,o,f){var h=b.name,i=null;if("text"===h)a:{for(var j=(b=c.currentNode)?b.nodeType:0;b!==o&&3!==j;){if(1===j){i=[new g("Element not allowed here.",b)];break a}j=(b=c.nextSibling())?b.nodeType:0}c.nextSibling();i=null}else if("data"===h)i=null;else if("value"===h)f!==b.text&&(i=[new g("Wrong value, should be '"+b.text+"', not '"+f+"'",o)]);else if("list"===h)i=null;else if("attribute"===h)a:{if(2!==b.e.length)throw"Attribute with wrong # of elements: "+
+b.e.length;h=b.localnames.length;for(i=0;i<h;i+=1){f=o.getAttributeNS(b.namespaces[i],b.localnames[i]);""===f&&!o.hasAttributeNS(b.namespaces[i],b.localnames[i])&&(f=void 0);if(void 0!==j&&void 0!==f){i=[new g("Attribute defined too often.",o)];break a}j=f}i=void 0===j?[new g("Attribute not found: "+b.names,o)]:m(b.e[1],c,o,j)}else if("element"===h)i=e(b,c,o);else if("oneOrMore"===h){f=0;do j=c.currentNode,h=a(b.e[0],c,o),f+=1;while(!h&&j!==c.currentNode);1<f?(c.currentNode=j,i=null):i=h}else if("choice"===
+h){if(2!==b.e.length)throw"Choice with wrong # of options: "+b.e.length;j=c.currentNode;if("empty"===b.e[0].name){if(h=a(b.e[1],c,o,f))c.currentNode=j;i=null}else{if(h=m(b.e[0],c,o,f))c.currentNode=j,h=a(b.e[1],c,o,f);i=h}}else if("group"===h){if(2!==b.e.length)throw"Group with wrong # of members: "+b.e.length;i=a(b.e[0],c,o)||a(b.e[1],c,o)}else if("interleave"===h)a:{for(var j=b.e.length,f=[j],n=j,k,p,t,r;0<n;){k=0;p=c.currentNode;for(i=0;i<j;i+=1)t=c.currentNode,!0!==f[i]&&f[i]!==t&&(r=b.e[i],(h=
+a(r,c,o))?(c.currentNode=t,void 0===f[i]&&(f[i]=!1)):t===c.currentNode||"oneOrMore"===r.name||"choice"===r.name&&("oneOrMore"===r.e[0].name||"oneOrMore"===r.e[1].name)?(k+=1,f[i]=t):(k+=1,f[i]=!0));if(p===c.currentNode&&k===n)break;if(0===k){for(i=0;i<j;i+=1)if(!1===f[i]){i=[new g("Interleave does not match.",o)];break a}break}for(i=n=0;i<j;i+=1)!0!==f[i]&&(n+=1)}i=null}else throw h+" not allowed in nonEmptyPattern.";return i};this.validate=function(a,c){a.currentNode=a.root;var e=m(k.e[0],a,a.root);
+c(e)};this.init=function(a,d){k=a;c=d}};
// Input 17
xmldom.OperationalTransformInterface=function(){};xmldom.OperationalTransformInterface.prototype.retain=function(){};xmldom.OperationalTransformInterface.prototype.insertCharacters=function(){};xmldom.OperationalTransformInterface.prototype.insertElementStart=function(){};xmldom.OperationalTransformInterface.prototype.insertElementEnd=function(){};xmldom.OperationalTransformInterface.prototype.deleteCharacters=function(){};xmldom.OperationalTransformInterface.prototype.deleteElementStart=function(){};
xmldom.OperationalTransformInterface.prototype.deleteElementEnd=function(){};xmldom.OperationalTransformInterface.prototype.replaceAttributes=function(){};xmldom.OperationalTransformInterface.prototype.updateAttributes=function(){};
// Input 18
-xmldom.OperationalTransformDOM=function(){this.retain=function(){};this.insertCharacters=function(){};this.insertElementStart=function(){};this.insertElementEnd=function(){};this.deleteCharacters=function(){};this.deleteElementStart=function(){};this.deleteElementEnd=function(){};this.replaceAttributes=function(){};this.updateAttributes=function(){};this.atEnd=function(){return true}};
+xmldom.OperationalTransformDOM=function(){this.retain=function(){};this.insertCharacters=function(){};this.insertElementStart=function(){};this.insertElementEnd=function(){};this.deleteCharacters=function(){};this.deleteElementStart=function(){};this.deleteElementEnd=function(){};this.replaceAttributes=function(){};this.updateAttributes=function(){};this.atEnd=function(){return!0}};
// Input 19
-xmldom.XPath=function(){function i(i,e,g){i=i.ownerDocument.evaluate(e,i,g,XPathResult.UNORDERED_NODE_ITERATOR_TYPE,null);e=[];for(g=i.iterateNext();g!==null;)g.nodeType===1&&e.push(g),g=i.iterateNext();return e}xmldom.XPath=function(){this.getODFElementsWithXPath=i};return xmldom.XPath}();
+xmldom.XPath=function(){function g(a,c,b){return-1!==a&&(a<c||-1===c)&&(a<b||-1===b)}function m(a){for(var c=[],b=0,d=a.length,f;b<d;){var e=a,h=d,o=c,k="",u=[],m=e.indexOf("[",b),C=e.indexOf("/",b),w=e.indexOf("=",b);g(C,m,w)?(k=e.substring(b,C),b=C+1):g(m,C,w)?(k=e.substring(b,m),b=i(e,m,u)):g(w,C,m)?(k=e.substring(b,w),b=w):(k=e.substring(b,h),b=h);o.push({location:k,predicates:u});if(b<d&&"="===a[b]){f=a.substring(b+1,d);if(2<f.length&&("'"===f[0]||'"'===f[0]))f=f.slice(1,f.length-1);else try{f=
+parseInt(f,10)}catch(G){}b=d}}return{steps:c,value:f}}function e(){}function k(){var a,c=!1;this.setNode=function(c){a=c};this.reset=function(){c=!1};this.next=function(){var b=c?null:a;c=!0;return b}}function a(a,c,b){this.reset=function(){a.reset()};this.next=function(){for(var d=a.next();d&&!(d=d.getAttributeNodeNS(c,b));)d=a.next();return d}}function c(a,c){var b=a.next(),d=null;this.reset=function(){a.reset();b=a.next();d=null};this.next=function(){for(;b;){if(d)if(c&&d.firstChild)d=d.firstChild;
+else{for(;!d.nextSibling&&d!==b;)d=d.parentNode;d===b?b=a.next():d=d.nextSibling}else{do(d=b.firstChild)||(b=a.next());while(b&&!d)}if(d&&1===d.nodeType)return d}return null}}function b(a,b){this.reset=function(){a.reset()};this.next=function(){for(var c=a.next();c&&!b(c);)c=a.next();return c}}function d(a,c,d){var c=c.split(":",2),f=d(c[0]),e=c[1];return new b(a,function(a){return a.localName===e&&a.namespaceURI===f})}function o(a,c,d){var f=new k,e=h(f,c,d),g=c.value;return void 0===g?new b(a,function(a){f.setNode(a);
+e.reset();return e.next()}):new b(a,function(a){f.setNode(a);e.reset();return(a=e.next())&&a.nodeValue===g})}function f(a,c,b){var d=a.ownerDocument,f=[],f=new k;f.setNode(a);a=m(c);f=h(f,a,b);a=[];for(b=f.next();b;)a.push(b),b=f.next();return f=a}var h,i;i=function(a,c,b){for(var d=c,f=a.length,e=0;d<f;)"]"===a[d]?(e-=1,0>=e&&b.push(m(a.substring(c,d)))):"["===a[d]&&(0>=e&&(c=d+1),e+=1),d+=1;return d};e.prototype.next=function(){};e.prototype.reset=function(){};h=function(b,f,e){var g,h,i,k;for(g=
+0;g<f.steps.length;g+=1){i=f.steps[g];h=i.location;""===h?b=new c(b,!1):"@"===h[0]?(k=h.slice(1).split(":",2),b=new a(b,e(k[0]),k[1])):"."!==h&&(b=new c(b,!1),-1!==h.indexOf(":")&&(b=d(b,h,e)));for(h=0;h<i.predicates.length;h+=1)k=i.predicates[h],b=o(b,k,e)}return b};xmldom.XPath=function(){this.getODFElementsWithXPath=f};return xmldom.XPath}();
// Input 20
-odf.StyleInfo=function(){function i(e,g){for(var a=k[e.localName],b=a&&a[e.namespaceURI],h=b?b.length:0,c,d,f,a=0;a<h;a+=1)if(c=e.getAttributeNS(b[a].ns,b[a].localname))d=b[a].keygroup,(f=g[d])||(f=g[d]={}),f[c]=1;for(a=e.firstChild;a;)a.nodeType===1&&(b=a,i(b,g)),a=a.nextSibling}var k;this.UsedKeysList=function(e){var g={};this.uses=function(a){var b=a.localName,e=a.getAttributeNS("urn:oasis:names:tc:opendocument:xmlns:drawing:1.0","name")||a.getAttributeNS("urn:oasis:names:tc:opendocument:xmlns:style:1.0",
-"name"),a=b==="style"?a.getAttributeNS("urn:oasis:names:tc:opendocument:xmlns:style:1.0","family"):a.namespaceURI==="urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0"?"data":b;return(a=g[a])?a[e]>0:false};i(e,g)};this.canElementHaveStyle=function(e,g){var a=k[g.localName];return(a=a&&a[g.namespaceURI])&&a.length>0};k=function(e){var j;var g,a,b,h,c,d={},f;for(g in e)if(e.hasOwnProperty(g)){b=e[g];c=b.length;for(a=0;a<c;a+=1)h=b[a],f=d[h.en]=d[h.en]||{},j=f[h.ens]=f[h.ens]||[],f=j,f.push({ns:h.ans,
-localname:h.a,keygroup:g})}return d}({text:[{ens:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",en:"tab-stop",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"leader-text-style"},{ens:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",en:"drop-cap",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"notes-configuration",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"citation-body-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",
+odf.StyleInfo=function(){function g(e,k){for(var a=m[e.localName],c=a&&a[e.namespaceURI],b=c?c.length:0,d,o,f,a=0;a<b;a+=1)if(d=e.getAttributeNS(c[a].ns,c[a].localname))o=c[a].keygroup,(f=k[o])||(f=k[o]={}),f[d]=1;for(a=e.firstChild;a;)1===a.nodeType&&(c=a,g(c,k)),a=a.nextSibling}var m;this.UsedKeysList=function(e){var k={};this.uses=function(a){var c=a.localName,b=a.getAttributeNS("urn:oasis:names:tc:opendocument:xmlns:drawing:1.0","name")||a.getAttributeNS("urn:oasis:names:tc:opendocument:xmlns:style:1.0",
+"name"),a="style"===c?a.getAttributeNS("urn:oasis:names:tc:opendocument:xmlns:style:1.0","family"):"urn:oasis:names:tc:opendocument:xmlns:datastyle:1.0"===a.namespaceURI?"data":c;return(a=k[a])?0<a[b]:!1};g(e,k)};this.canElementHaveStyle=function(e,g){var a=m[g.localName];return(a=a&&a[g.namespaceURI])&&0<a.length};m=function(e){var g,a,c,b,d,o={},f;for(g in e)if(e.hasOwnProperty(g)){c=e[g];d=c.length;for(a=0;a<d;a+=1)b=c[a],f=o[b.en]=o[b.en]||{},f=f[b.ens]=f[b.ens]||[],f.push({ns:b.ans,localname:b.a,
+keygroup:g})}return o}({text:[{ens:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",en:"tab-stop",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"leader-text-style"},{ens:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",en:"drop-cap",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"notes-configuration",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"citation-body-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",
en:"notes-configuration",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"citation-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"a",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"alphabetical-index",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"linenumbering-configuration",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",
a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"list-level-style-number",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"ruby-text",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"span",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"a",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",
a:"visited-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",en:"text-properties",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"text-line-through-text-style"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"alphabetical-index-source",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"main-entry-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"index-entry-bibliography",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},
{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"index-entry-chapter",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"index-entry-link-end",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"index-entry-link-start",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",
en:"index-entry-page-number",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"index-entry-span",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"index-entry-tab-stop",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"index-entry-text",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",
a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"index-title-template",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"list-level-style-bullet",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"outline-level-style",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"}],paragraph:[{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",
en:"caption",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"text-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"circle",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"text-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"connector",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"text-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"control",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",
a:"text-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"custom-shape",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"text-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"ellipse",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"text-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"frame",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"text-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",
en:"line",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"text-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"measure",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"text-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"path",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"text-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"polygon",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",
a:"text-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"polyline",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"text-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"rect",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"text-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"regular-polygon",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"text-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:office:1.0",
en:"annotation",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"text-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:form:1.0",en:"column",ans:"urn:oasis:names:tc:opendocument:xmlns:form:1.0",a:"text-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",en:"style",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"next-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",en:"body",ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",a:"paragraph-style-name"},
{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",en:"even-columns",ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",a:"paragraph-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",en:"even-rows",ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",a:"paragraph-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",en:"first-column",ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",a:"paragraph-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",
en:"first-row",ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",a:"paragraph-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",en:"last-column",ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",a:"paragraph-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",en:"last-row",ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",a:"paragraph-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",en:"odd-columns",ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",
a:"paragraph-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",en:"odd-rows",ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",a:"paragraph-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"notes-configuration",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"default-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"alphabetical-index-entry-template",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",
en:"bibliography-entry-template",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"h",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"illustration-index-entry-template",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"index-source-style",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",
a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"object-index-entry-template",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"p",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"table-index-entry-template",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",
en:"table-of-content-entry-template",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"table-index-entry-template",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"user-index-entry-template",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",en:"page-layout-properties",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",
a:"register-truth-ref-style-name"}],chart:[{ens:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",en:"axis",ans:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",en:"chart",ans:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",en:"data-label",ans:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",
en:"data-point",ans:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",en:"equation",ans:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",en:"error-indicator",ans:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",en:"floor",ans:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",a:"style-name"},
{ens:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",en:"footer",ans:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",en:"grid",ans:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",en:"legend",ans:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",en:"mean-value",ans:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",
a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",en:"plot-area",ans:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",en:"regression-curve",ans:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",en:"series",ans:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",en:"stock-gain-marker",
ans:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",en:"stock-loss-marker",ans:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",en:"stock-range-line",ans:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",en:"subtitle",ans:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",a:"style-name"},
{ens:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",en:"title",ans:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",en:"wall",ans:"urn:oasis:names:tc:opendocument:xmlns:chart:1.0",a:"style-name"}],section:[{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"alphabetical-index",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"bibliography",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",
a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"illustration-index",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"index-title",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"object-index",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"section",
ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"table-of-content",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"table-index",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"user-index",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"}],ruby:[{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",
en:"ruby",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"}],table:[{ens:"urn:oasis:names:tc:opendocument:xmlns:database:1.0",en:"query",ans:"urn:oasis:names:tc:opendocument:xmlns:database:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:database:1.0",en:"table-representation",ans:"urn:oasis:names:tc:opendocument:xmlns:database:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",en:"background",ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",
a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",en:"table",ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",a:"style-name"}],"table-column":[{ens:"urn:oasis:names:tc:opendocument:xmlns:database:1.0",en:"column",ans:"urn:oasis:names:tc:opendocument:xmlns:database:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",en:"table-column",ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",a:"style-name"}],"table-row":[{ens:"urn:oasis:names:tc:opendocument:xmlns:database:1.0",
en:"query",ans:"urn:oasis:names:tc:opendocument:xmlns:database:1.0",a:"default-row-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:database:1.0",en:"table-representation",ans:"urn:oasis:names:tc:opendocument:xmlns:database:1.0",a:"default-row-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",en:"table-row",ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",a:"style-name"}],"table-cell":[{ens:"urn:oasis:names:tc:opendocument:xmlns:database:1.0",en:"column",ans:"urn:oasis:names:tc:opendocument:xmlns:database:1.0",
a:"default-cell-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",en:"table-column",ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",a:"default-cell-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",en:"table-row",ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",a:"default-cell-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",en:"body",ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",
en:"covered-table-cell",ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",en:"even-columns",ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",en:"covered-table-cell",ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",en:"even-columns",ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",
a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",en:"even-rows",ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",en:"first-column",ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",en:"first-row",ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",en:"last-column",
ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",en:"last-row",ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",en:"odd-columns",ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",en:"odd-rows",ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",
en:"table-cell",ans:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",a:"style-name"}],graphic:[{ens:"urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0",en:"cube",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0",en:"extrude",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0",en:"rotate",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"style-name"},
{ens:"urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0",en:"scene",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0",en:"sphere",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"caption",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"circle",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",
a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"connector",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"control",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"custom-shape",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",
en:"ellipse",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"frame",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"g",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"line",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",
en:"measure",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"page-thumbnail",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"path",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"polygon",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"style-name"},
{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"polyline",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"rect",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"regular-polygon",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:office:1.0",en:"annotation",
ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"style-name"}],presentation:[{ens:"urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0",en:"cube",ans:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0",en:"extrude",ans:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0",en:"rotate",ans:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",a:"style-name"},
{ens:"urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0",en:"scene",ans:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:dr3d:1.0",en:"sphere",ans:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"caption",ans:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"circle",
ans:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"connector",ans:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"control",ans:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"custom-shape",ans:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",
a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"ellipse",ans:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"frame",ans:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"g",ans:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",
en:"line",ans:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"measure",ans:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"page-thumbnail",ans:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"path",ans:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",
a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"polygon",ans:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"polyline",ans:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"rect",ans:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",
en:"regular-polygon",ans:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:office:1.0",en:"annotation",ans:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",a:"style-name"}],"drawing-page":[{ens:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",en:"page",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",en:"notes",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",
a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",en:"handout-master",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",en:"master-page",ans:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",a:"style-name"}],"list-style":[{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"list",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",
en:"numbered-paragraph",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"list-item",ans:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",a:"style-override"},{ens:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",en:"style",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"list-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",en:"style",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},
{ens:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",en:"style",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"percentage-data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",en:"date-time-decl",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"creation-date",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",
en:"creation-time",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"database-display",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"date",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"editing-duration",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",
a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"expression",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"meta-field",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"modification-date",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",
en:"modification-time",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"print-date",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"print-time",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"table-formula",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",
a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"time",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"user-defined",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"user-field-get",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",
en:"user-field-input",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"variable-get",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"variable-input",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"variable-set",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",
a:"data-style-name"}],data:[{ens:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",en:"style",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",en:"style",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"percentage-data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",en:"date-time-decl",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",
en:"creation-date",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"creation-time",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"database-display",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"date",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",
a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"editing-duration",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"expression",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"meta-field",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",
en:"modification-date",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"modification-time",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"print-date",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"print-time",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",
a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"table-formula",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"time",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"user-defined",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",
en:"user-field-get",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"user-field-input",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"variable-get",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"variable-input",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",
a:"data-style-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:text:1.0",en:"variable-set",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"data-style-name"}],"page-layout":[{ens:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",en:"notes",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"page-layout-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",en:"handout-master",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"page-layout-name"},{ens:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",
en:"master-page",ans:"urn:oasis:names:tc:opendocument:xmlns:style:1.0",a:"page-layout-name"}]})};
// Input 21
-odf.Style2CSS=function(){function i(a,b){var c={},d,f,e;if(!b)return c;for(d=b.firstChild;d;){d.namespaceURI===p&&d.localName==="style"?e=d.getAttributeNS(p,"family"):d.namespaceURI===m&&d.localName==="list-style"&&(e="list");if(f=e&&d.getAttributeNS&&d.getAttributeNS(p,"name"))c[e]||(c[e]={}),c[e][f]=d;d=d.nextSibling}return c}function k(a,b){if(!b||!a)return null;if(a[b])return a[b];var c,d;for(c in a)if(a.hasOwnProperty(c)&&(d=k(a[c].derivedStyles,b)))return d;return null}function e(a,b,c){var d=
-b[a],f,g;if(d)if(f=d.getAttributeNS(p,"parent-style-name"),g=null,f&&(g=k(c,f),!g&&b[f]&&(e(f,b,c),g=b[f],b[f]=null)),g){if(!g.derivedStyles)g.derivedStyles={};g.derivedStyles[a]=d}else c[a]=d}function g(a,b){for(var c in a)a.hasOwnProperty(c)&&(e(c,a,b),a[c]=null)}function a(a,b){var c=u[a],d;if(c===null)return null;d="["+c+'|style-name="'+b+'"]';c==="presentation"&&(c="draw",d='[presentation|style-name="'+b+'"]');return c+"|"+n[a].join(d+","+c+"|")+d}function b(c,d,f){var e=[],g,h;e.push(a(c,d));
-for(g in f.derivedStyles)if(f.derivedStyles.hasOwnProperty(g))for(h in d=b(c,g,f.derivedStyles[g]),d)d.hasOwnProperty(h)&&e.push(d[h]);return e}function h(a,b,c){if(!a)return null;for(a=a.firstChild;a;){if(a.namespaceURI===b&&a.localName===c)return b=a;a=a.nextSibling}return null}function c(a,b){var c="",d,f;for(d in b)b.hasOwnProperty(d)&&(d=b[d],(f=a.getAttributeNS(d[0],d[1]))&&(c+=d[2]+":"+f+";"));return c}function d(a,b,c,d){for(var b='text|list[text|style-name="'+b+'"]',c=c.getAttributeNS(m,
-"level"),f="",c=c&&parseInt(c,10);c>1;)b+=" > text|list-item > text|list",c-=1;b+=" > list-item:before";try{a.insertRule(b+"{"+d+"}",a.cssRules.length)}catch(e){throw e;}}function f(a,e,g,i){if(e==="list")for(var k=i.firstChild,l,n;k;){if(k.namespaceURI===m)if(l=k,k.localName==="list-level-style-number"){n=l;var t=n.getAttributeNS(p,"num-format"),u=n.getAttributeNS(p,"num-suffix"),L="",L={1:"decimal",a:"lower-latin",A:"upper-latin",i:"lower-roman",I:"upper-roman"},Q="",Q=n.getAttributeNS(p,"num-prefix")||
-"";Q+=L.hasOwnProperty(t)?" counter(list, "+L[t]+")":t?"'"+t+"';":" ''";u&&(Q+=" '"+u+"'");n=L="content: "+Q+";";d(a,g,l,n)}else k.localName==="list-level-style-image"?(n="content: none;",d(a,g,l,n)):k.localName==="list-level-style-bullet"&&(n="content: '"+l.getAttributeNS(m,"bullet-char")+"';",d(a,g,l,n));k=k.nextSibling}else{g=b(e,g,i).join(",");l="";if(k=h(i,p,"text-properties")){n="";n+=c(k,q);t=k.getAttributeNS(p,"text-underline-style");t==="solid"&&(n+="text-decoration: underline;");if(t=k.getAttributeNS(p,
-"font-name"))(t='"'+t+'"')&&(n+="font-family: "+t+";");l+=n}if(k=h(i,p,"paragraph-properties")){n=k;k="";k+=c(n,y);n=n.getElementsByTagNameNS(p,"background-image");if(n.length>0&&(t=n.item(0).getAttributeNS(j,"href")))k+="background-image: url('odfkit:"+t+"');",n=n.item(0),k+=c(n,r);l+=k}if(k=h(i,p,"graphic-properties"))n="",n+=c(k,C),l+=n;if(k=h(i,p,"table-cell-properties"))n="",n+=c(k,s),l+=n;if(l.length!==0)try{a.insertRule(g+"{"+l+"}",a.cssRules.length)}catch(R){throw R;}}for(var X in i.derivedStyles)i.derivedStyles.hasOwnProperty(X)&&
-f(a,e,X,i.derivedStyles[X])}var j="http://www.w3.org/1999/xlink",p="urn:oasis:names:tc:opendocument:xmlns:style:1.0",m="urn:oasis:names:tc:opendocument:xmlns:text:1.0",l={draw:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",fo:"urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",office:"urn:oasis:names:tc:opendocument:xmlns:office:1.0",presentation:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",style:p,svg:"urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0",table:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",
-text:m,xlink:j},u={graphic:"draw",paragraph:"text",presentation:"presentation",ruby:"text",section:"text",table:"table","table-cell":"table","table-column":"table","table-row":"table",text:"text",list:"text"},n={graphic:"circle,connected,control,custom-shape,ellipse,frame,g,line,measure,page,page-thumbnail,path,polygon,polyline,rect,regular-polygon".split(","),paragraph:"alphabetical-index-entry-template,h,illustration-index-entry-template,index-source-style,object-index-entry-template,p,table-index-entry-template,table-of-content-entry-template,user-index-entry-template".split(","),
+odf.Style2CSS=function(){function g(a,b){var c={},d,f,e;if(!b)return c;for(d=b.firstChild;d;){d.namespaceURI===i&&"style"===d.localName?e=d.getAttributeNS(i,"family"):d.namespaceURI===j&&"list-style"===d.localName&&(e="list");if(f=e&&d.getAttributeNS&&d.getAttributeNS(i,"name"))c[e]||(c[e]={}),c[e][f]=d;d=d.nextSibling}return c}function m(a,b){if(!b||!a)return null;if(a[b])return a[b];var c,d;for(c in a)if(a.hasOwnProperty(c)&&(d=m(a[c].derivedStyles,b)))return d;return null}function e(a,b,c){var d=
+b[a],f,g;d&&(f=d.getAttributeNS(i,"parent-style-name"),g=null,f&&(g=m(c,f),!g&&b[f]&&(e(f,b,c),g=b[f],b[f]=null)),g?(g.derivedStyles||(g.derivedStyles={}),g.derivedStyles[a]=d):c[a]=d)}function k(a,b){for(var c in a)a.hasOwnProperty(c)&&(e(c,a,b),a[c]=null)}function a(a,b){var c=x[a],d;if(null===c)return null;d="["+c+'|style-name="'+b+'"]';"presentation"===c&&(c="draw",d='[presentation|style-name="'+b+'"]');return c+"|"+p[a].join(d+","+c+"|")+d}function c(b,d,f){var e=[],g,h;e.push(a(b,d));for(g in f.derivedStyles)if(f.derivedStyles.hasOwnProperty(g))for(h in d=
+c(b,g,f.derivedStyles[g]),d)d.hasOwnProperty(h)&&e.push(d[h]);return e}function b(a,b,c){if(!a)return null;for(a=a.firstChild;a;){if(a.namespaceURI===b&&a.localName===c)return b=a;a=a.nextSibling}return null}function d(a,b){var c="",d,f;for(d in b)b.hasOwnProperty(d)&&(d=b[d],(f=a.getAttributeNS(d[0],d[1]))&&(c+=d[2]+":"+f+";"));return c}function o(a,b,c,d){b='text|list[text|style-name="'+b+'"]';for(c=(c=c.getAttributeNS(j,"level"))&&parseInt(c,10);1<c;)b+=" > text|list-item > text|list",c-=1;try{a.insertRule(b+
+" > list-item:before{"+d+"}",a.cssRules.length)}catch(f){throw f;}}function f(a,e,g,k){if("list"===e)for(var n=k.firstChild,l,m;n;){if(n.namespaceURI===j)if(l=n,"list-level-style-number"===n.localName){m=l;var p=m.getAttributeNS(i,"num-format"),x=m.getAttributeNS(i,"num-suffix"),M="",M={1:"decimal",a:"lower-latin",A:"upper-latin",i:"lower-roman",I:"upper-roman"},F="",F=m.getAttributeNS(i,"num-prefix")||"",F=M.hasOwnProperty(p)?F+(" counter(list, "+M[p]+")"):p?F+("'"+p+"';"):F+" ''";x&&(F+=" '"+x+
+"'");m=M="content: "+F+";";o(a,g,l,m)}else"list-level-style-image"===n.localName?(m="content: none;",o(a,g,l,m)):"list-level-style-bullet"===n.localName&&(m="content: '"+l.getAttributeNS(j,"bullet-char")+"';",o(a,g,l,m));n=n.nextSibling}else{g=c(e,g,k).join(",");n="";if(l=b(k,i,"text-properties")){m=""+d(l,t);p=l.getAttributeNS(i,"text-underline-style");"solid"===p&&(m+="text-decoration: underline;");if(p=l.getAttributeNS(i,"font-name"))(p='"'+p+'"')&&(m+="font-family: "+p+";");n+=m}if(l=b(k,i,"paragraph-properties")){m=
+l;l=""+d(m,z);m=m.getElementsByTagNameNS(i,"background-image");if(0<m.length&&(p=m.item(0).getAttributeNS(h,"href")))l+="background-image: url('odfkit:"+p+"');",m=m.item(0),l+=d(m,r);n+=l}if(l=b(k,i,"graphic-properties"))l=""+d(l,s),n+=l;if(l=b(k,i,"table-cell-properties"))l=""+d(l,q),n+=l;if(0!==n.length)try{a.insertRule(g+"{"+n+"}",a.cssRules.length)}catch(R){throw R;}}for(var S in k.derivedStyles)k.derivedStyles.hasOwnProperty(S)&&f(a,e,S,k.derivedStyles[S])}var h="http://www.w3.org/1999/xlink",
+i="urn:oasis:names:tc:opendocument:xmlns:style:1.0",j="urn:oasis:names:tc:opendocument:xmlns:text:1.0",n={draw:"urn:oasis:names:tc:opendocument:xmlns:drawing:1.0",fo:"urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",office:"urn:oasis:names:tc:opendocument:xmlns:office:1.0",presentation:"urn:oasis:names:tc:opendocument:xmlns:presentation:1.0",style:i,svg:"urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0",table:"urn:oasis:names:tc:opendocument:xmlns:table:1.0",text:j,xlink:h},x=
+{graphic:"draw",paragraph:"text",presentation:"presentation",ruby:"text",section:"text",table:"table","table-cell":"table","table-column":"table","table-row":"table",text:"text",list:"text"},p={graphic:"circle,connected,control,custom-shape,ellipse,frame,g,line,measure,page,page-thumbnail,path,polygon,polyline,rect,regular-polygon".split(","),paragraph:"alphabetical-index-entry-template,h,illustration-index-entry-template,index-source-style,object-index-entry-template,p,table-index-entry-template,table-of-content-entry-template,user-index-entry-template".split(","),
presentation:"caption,circle,connector,control,custom-shape,ellipse,frame,g,line,measure,page-thumbnail,path,polygon,polyline,rect,regular-polygon".split(","),ruby:["ruby","ruby-text"],section:"alphabetical-index,bibliography,illustration-index,index-title,object-index,section,table-of-content,table-index,user-index".split(","),table:["background","table"],"table-cell":"body,covered-table-cell,even-columns,even-rows,first-column,first-row,last-column,last-row,odd-columns,odd-rows,table-cell".split(","),
-"table-column":["table-column"],"table-row":["table-row"],text:"a,index-entry-chapter,index-entry-link-end,index-entry-link-start,index-entry-page-number,index-entry-span,index-entry-tab-stop,index-entry-text,index-title-template,linenumbering-configuration,list-level-style-number,list-level-style-bullet,outline-level-style,span".split(","),list:["list-item"]},q=[["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","color","color"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
-"background-color","background-color"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","font-weight","font-weight"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","font-style","font-style"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","font-size","font-size"]],r=[[p,"repeat","background-repeat"]],y=[["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","background-color","background-color"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
+"table-column":["table-column"],"table-row":["table-row"],text:"a,index-entry-chapter,index-entry-link-end,index-entry-link-start,index-entry-page-number,index-entry-span,index-entry-tab-stop,index-entry-text,index-title-template,linenumbering-configuration,list-level-style-number,list-level-style-bullet,outline-level-style,span".split(","),list:["list-item"]},t=[["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","color","color"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
+"background-color","background-color"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","font-weight","font-weight"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","font-style","font-style"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","font-size","font-size"]],r=[[i,"repeat","background-repeat"]],z=[["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","background-color","background-color"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
"text-align","text-align"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","padding-left","padding-left"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","padding-right","padding-right"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","padding-top","padding-top"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","padding-bottom","padding-bottom"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","border-left","border-left"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
"border-right","border-right"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","border-top","border-top"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","border-bottom","border-bottom"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","margin-left","margin-left"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","margin-right","margin-right"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","margin-top","margin-top"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0",
-"margin-bottom","margin-bottom"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","border","border"]],C=[["urn:oasis:names:tc:opendocument:xmlns:drawing:1.0","fill-color","background-color"],["urn:oasis:names:tc:opendocument:xmlns:drawing:1.0","fill","background"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","min-height","min-height"],["urn:oasis:names:tc:opendocument:xmlns:drawing:1.0","stroke","border"],["urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0",
-"stroke-color","border-color"]],s=[["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","background-color","background-color"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","border-left","border-left"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","border-right","border-right"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","border-top","border-top"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","border-bottom","border-bottom"]];
-this.namespaces=l;this.namespaceResolver=function(a){return l[a]||null};this.namespaceResolver.lookupNamespaceURI=this.namespaceResolver;this.style2css=function(a,b,c){for(var d,e,h,j,r;a.cssRules.length;)a.deleteRule(a.cssRules.length-1);d=null;if(b)d=b.ownerDocument;if(c)d=c.ownerDocument;if(d){for(e in l)if(l.hasOwnProperty(e)){j="@namespace "+e+" url("+l[e]+");";try{a.insertRule(j,a.cssRules.length)}catch(q){}}b=i(d,b);d=i(d,c);for(r in u)if(u.hasOwnProperty(r))for(h in c={},g(b[r],c),g(d[r],
-c),c)c.hasOwnProperty(h)&&f(a,r,h,c[h])}}};
+"margin-bottom","margin-bottom"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","border","border"]],s=[["urn:oasis:names:tc:opendocument:xmlns:drawing:1.0","fill-color","background-color"],["urn:oasis:names:tc:opendocument:xmlns:drawing:1.0","fill","background"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","min-height","min-height"],["urn:oasis:names:tc:opendocument:xmlns:drawing:1.0","stroke","border"],["urn:oasis:names:tc:opendocument:xmlns:svg-compatible:1.0",
+"stroke-color","border-color"]],q=[["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","background-color","background-color"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","border-left","border-left"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","border-right","border-right"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","border-top","border-top"],["urn:oasis:names:tc:opendocument:xmlns:xsl-fo-compatible:1.0","border-bottom","border-bottom"]];
+this.namespaces=n;this.namespaceResolver=function(a){return n[a]||null};this.namespaceResolver.lookupNamespaceURI=this.namespaceResolver;this.style2css=function(a,b,c){for(var d,e,h,i,j;a.cssRules.length;)a.deleteRule(a.cssRules.length-1);d=null;b&&(d=b.ownerDocument);c&&(d=c.ownerDocument);if(d){for(e in n)if(n.hasOwnProperty(e)){i="@namespace "+e+" url("+n[e]+");";try{a.insertRule(i,a.cssRules.length)}catch(o){}}b=g(d,b);e=g(d,c);c={};for(j in x)if(x.hasOwnProperty(j))for(h in d=c[j]={},k(b[j],
+d),k(e[j],d),d)d.hasOwnProperty(h)&&f(a,j,h,d[h])}}};
// Input 22
runtime.loadClass("core.Base64");runtime.loadClass("xmldom.XPath");runtime.loadClass("odf.Style2CSS");
-odf.FontLoader=function(){function i(b,e,c,d,f){var g,k=0,m;for(m in b)b.hasOwnProperty(m)&&(k===c&&(g=m),k+=1);if(!g)return f();e.load(b[g].href,function(k,m){if(k)runtime.log(k);else{var n=d,n=document.styleSheets[0],q='@font-face { font-family: "'+g+'"; src: url(data:application/x-font-ttf;charset=binary;base64,'+a.convertUTF8ArrayToBase64(m)+') format("truetype"); }';try{n.insertRule(q,n.cssRules.length)}catch(r){runtime.log("Problem inserting rule in CSS: "+q)}}return i(b,e,c+1,d,f)})}function k(a,
-e,c){i(a,e,0,c,function(){})}var e=new odf.Style2CSS,g=new xmldom.XPath,a=new core.Base64;odf.FontLoader=function(){this.loadFonts=function(a,h,c){var d={},f,i,p;if(a){a=g.getODFElementsWithXPath(a,"style:font-face[svg:font-face-src]",e.namespaceResolver);for(f=0;f<a.length;f+=1)i=a[f],p=i.getAttributeNS(e.namespaces.style,"name"),i=g.getODFElementsWithXPath(i,"svg:font-face-src/svg:font-face-uri",e.namespaceResolver),i.length>0&&(i=i[0].getAttributeNS(e.namespaces.xlink,"href"),d[p]={href:i})}k(d,
-h,c)}};return odf.FontLoader}();
+odf.FontLoader=function(){function g(c,b,d,e,f){var h,i=0,j;for(j in c)c.hasOwnProperty(j)&&(i===d&&(h=j),i+=1);if(!h)return f();b.load(c[h].href,function(i,j){if(i)runtime.log(i);else{var k=e,k=document.styleSheets[0],m='@font-face { font-family: "'+h+'"; src: url(data:application/x-font-ttf;charset=binary;base64,'+a.convertUTF8ArrayToBase64(j)+') format("truetype"); }';try{k.insertRule(m,k.cssRules.length)}catch(r){runtime.log("Problem inserting rule in CSS: "+m)}}return g(c,b,d+1,e,f)})}function m(a,
+b,d){g(a,b,0,d,function(){})}var e=new odf.Style2CSS,k=new xmldom.XPath,a=new core.Base64;odf.FontLoader=function(){this.loadFonts=function(a,b,d){var g={},f,h,i;if(a){a=k.getODFElementsWithXPath(a,"style:font-face[svg:font-face-src]",e.namespaceResolver);for(f=0;f<a.length;f+=1)h=a[f],i=h.getAttributeNS(e.namespaces.style,"name"),h=k.getODFElementsWithXPath(h,"svg:font-face-src/svg:font-face-uri",e.namespaceResolver),0<h.length&&(h=h[0].getAttributeNS(e.namespaces.xlink,"href"),g[i]={href:h})}m(g,
+b,d)}};return odf.FontLoader}();
// Input 23
runtime.loadClass("core.Base64");runtime.loadClass("core.Zip");runtime.loadClass("xmldom.LSSerializer");runtime.loadClass("odf.StyleInfo");runtime.loadClass("odf.Style2CSS");runtime.loadClass("odf.FontLoader");
-odf.OdfContainer=function(){function i(a,b,c){for(a=a?a.firstChild:null;a;){if(a.localName===c&&a.namespaceURI===b)return a;a=a.nextSibling}return null}function k(a){var b,c=p.length;for(b=0;b<c;b+=1)if(a.namespaceURI===f&&a.localName===p[b])return b;return-1}function e(a,b){var d=a.automaticStyles,f;b&&(f=new c.UsedKeysList(b));this.acceptNode=function(a){if(a.namespaceURI==="http://www.w3.org/1999/xhtml")return 3;else if(f&&a.parentNode===d&&a.nodeType===1)return f.uses(a)?1:2;return 1}}function g(a,
-b){if(b){var c=k(b),d,f=a.firstChild;if(c!==-1){for(;f;){d=k(f);if(d!==-1&&d>c)break;f=f.nextSibling}a.insertBefore(b,f)}}}function a(a){this.OdfContainer=a}function b(a,b,c){var d=this,f;this.size=0;this.type=null;this.name=a;this.container=b;this.onchange=this.onreadystatechange=this.document=this.url=null;this.EMPTY=0;this.LOADING=1;this.DONE=2;this.state=this.EMPTY;this.load=function(){c.load(a,function(b,c){f=c;d.url=null;if(f){var e=0,g=u[a];g||(g=f[1]===80&&f[2]===78&&f[3]===71?"image/png":
-f[0]===255&&f[1]===216&&f[2]===255?"image/jpeg":f[0]===71&&f[1]===73&&f[2]===70?"image/gif":"");for(d.url="data:"+g+";base64,";e<f.length;)d.url+=m.convertUTF8ArrayToBase64(f.slice(e,Math.min(e+45E3,f.length))),e+=45E3}if(d.onchange)d.onchange(d);if(d.onstatereadychange)d.onstatereadychange(d)})};this.abort=function(){}}function h(){this.length=0;this.item=function(){}}var c=new odf.StyleInfo,d=new odf.Style2CSS,f="urn:oasis:names:tc:opendocument:xmlns:office:1.0",j="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0",
-p="meta,settings,scripts,font-face-decls,styles,automatic-styles,master-styles,body".split(","),m=new core.Base64,l=new odf.FontLoader,u={};a.prototype=new function(){};a.prototype.constructor=a;a.namespaceURI=f;a.localName="document";b.prototype.load=function(){};b.prototype.getUrl=function(){return this.data?"data:;base64,"+m.toBase64(this.data):null};odf.OdfContainer=function q(c,k){function m(a){for(var b=a.firstChild,c;b;)c=b.nextSibling,b.nodeType===1?m(b):b.nodeType===7&&a.removeChild(b),b=
-c}function s(a){var b=A.rootElement.ownerDocument,c;if(a){m(a.documentElement);try{c=b.importNode(a.documentElement,true)}catch(d){}}return c}function p(a){A.state=a;if(A.onchange)A.onchange(A);if(A.onstatereadychange)A.onstatereadychange(A)}function E(a){var a=s(a),b=A.rootElement;!a||a.localName!=="document-styles"||a.namespaceURI!==f?p(q.INVALID):(b.fontFaceDecls=i(a,f,"font-face-decls"),g(b,b.fontFaceDecls),b.styles=i(a,f,"styles"),g(b,b.styles),b.automaticStyles=i(a,f,"automatic-styles"),g(b,
-b.automaticStyles),b.masterStyles=i(a,f,"master-styles"),g(b,b.masterStyles),l.loadFonts(b.fontFaceDecls,J,null))}function B(a){var a=s(a),b,c,d;if(!a||a.localName!=="document-content"||a.namespaceURI!==f)p(q.INVALID);else{b=A.rootElement;c=i(a,f,"font-face-decls");if(b.fontFaceDecls&&c)for(d=c.firstChild;d;)b.fontFaceDecls.appendChild(d),d=c.firstChild;else if(c)b.fontFaceDecls=c,g(b,c);c=i(a,f,"automatic-styles");if(b.automaticStyles&&c)for(d=c.firstChild;d;)b.automaticStyles.appendChild(d),d=c.firstChild;
-else if(c)b.automaticStyles=c,g(b,c);b.body=i(a,f,"body");g(b,b.body)}}function z(a){var a=s(a),b;if(a&&!(a.localName!=="document-meta"||a.namespaceURI!==f))b=A.rootElement,b.meta=i(a,f,"meta"),g(b,b.meta)}function G(a){var a=s(a),b;if(a&&!(a.localName!=="document-settings"||a.namespaceURI!==f))b=A.rootElement,b.settings=i(a,f,"settings"),g(b,b.settings)}function o(a,b){J.load(a,function(a,c){if(a)b(a,null);else{var d=runtime.byteArrayToString(c,"utf8"),d=(new DOMParser).parseFromString(d,"text/xml");
-b(null,d)}})}function x(){o("styles.xml",function(a,b){E(b);A.state!==q.INVALID&&o("content.xml",function(a,b){B(b);A.state!==q.INVALID&&o("meta.xml",function(a,b){z(b);A.state!==q.INVALID&&o("settings.xml",function(a,b){b&&G(b);o("META-INF/manifest.xml",function(a,b){if(b){var c=s(b),d;if(c&&!(c.localName!=="manifest"||c.namespaceURI!==j)){d=A.rootElement;d.manifest=c;for(c=d.manifest.firstChild;c;)c.nodeType===1&&c.localName==="file-entry"&&c.namespaceURI===j&&(u[c.getAttributeNS(j,"full-path")]=
-c.getAttributeNS(j,"media-type")),c=c.nextSibling}}A.state!==q.INVALID&&p(q.DONE)})})})})})}function t(a,b){var c="",d;for(d in b)b.hasOwnProperty(d)&&(c+=" xmlns:"+d+'="'+b[d]+'"');return'<?xml version="1.0" encoding="UTF-8"?><office:'+a+" "+c+' office:version="1.2">'}function w(){var a=d.namespaces,b=new xmldom.LSSerializer,c=t("document-meta",a);b.filter=new e(A.rootElement);c+=b.writeToString(A.rootElement.meta,a);c+="</office:document-meta>";return c}function L(){var a=d.namespaces,b=new xmldom.LSSerializer,
-c=t("document-settings",a);b.filter=new e(A.rootElement);c+=b.writeToString(A.rootElement.settings,a);c+="</office:document-settings>";return c}function Q(){var a=d.namespaces,b=new xmldom.LSSerializer,c=t("document-styles",a);b.filter=new e(A.rootElement,A.rootElement.masterStyles);c+=b.writeToString(A.rootElement.fontFaceDecls,a);c+=b.writeToString(A.rootElement.styles,a);c+=b.writeToString(A.rootElement.automaticStyles,a);c+=b.writeToString(A.rootElement.masterStyles,a);c+="</office:document-styles>";
-return c}function R(){var a=d.namespaces,b=new xmldom.LSSerializer,c=t("document-content",a);b.filter=new e(A.rootElement,A.rootElement.body);c+=b.writeToString(A.rootElement.automaticStyles,a);c+=b.writeToString(A.rootElement.body,a);c+="</office:document-content>";return c}function X(a,b){runtime.loadXML(a,function(a,c){if(a)b(a);else{var d=s(c);!d||d.localName!=="document"||d.namespaceURI!==f?p(q.INVALID):(A.rootElement=d,d.fontFaceDecls=i(d,f,"font-face-decls"),d.styles=i(d,f,"styles"),d.automaticStyles=
-i(d,f,"automatic-styles"),d.masterStyles=i(d,f,"master-styles"),d.body=i(d,f,"body"),d.meta=i(d,f,"meta"),p(q.DONE))}})}var A=this,J=null;this.onstatereadychange=k;this.parts=this.rootElement=this.state=this.onchange=null;this.getPart=function(a){return new b(a,A,J)};this.save=function(a){var b;b=runtime.byteArrayFromString(L(),"utf8");J.save("settings.xml",b,true,new Date);b=runtime.byteArrayFromString(w(),"utf8");J.save("meta.xml",b,true,new Date);b=runtime.byteArrayFromString(Q(),"utf8");J.save("styles.xml",
-b,true,new Date);b=runtime.byteArrayFromString(R(),"utf8");J.save("content.xml",b,true,new Date);J.write(function(b){a(b)})};this.state=q.LOADING;this.rootElement=function(a){var b=document.createElementNS(a.namespaceURI,a.localName),c,a=new a;for(c in a)a.hasOwnProperty(c)&&(b[c]=a[c]);return b}(a);this.parts=new h(this);J=new core.Zip(c,function(a,b){J=b;a?X(c,function(b){if(a)J.error=a+"\n"+b,p(q.INVALID)}):x()})};odf.OdfContainer.EMPTY=0;odf.OdfContainer.LOADING=1;odf.OdfContainer.DONE=2;odf.OdfContainer.INVALID=
-3;odf.OdfContainer.SAVING=4;odf.OdfContainer.MODIFIED=5;odf.OdfContainer.getContainer=function(a){return new odf.OdfContainer(a,null)};return odf.OdfContainer}();
+odf.OdfContainer=function(){function g(a,b,c){for(a=a?a.firstChild:null;a;){if(a.localName===c&&a.namespaceURI===b)return a;a=a.nextSibling}return null}function m(a){var b,c=i.length;for(b=0;b<c;b+=1)if(a.namespaceURI===f&&a.localName===i[b])return b;return-1}function e(a,b){var c=a.automaticStyles,f;b&&(f=new d.UsedKeysList(b));this.acceptNode=function(a){return"http://www.w3.org/1999/xhtml"===a.namespaceURI?3:f&&a.parentNode===c&&1===a.nodeType?f.uses(a)?1:2:1}}function k(a,b){if(b){var c=m(b),
+d,f=a.firstChild;if(-1!==c){for(;f;){d=m(f);if(-1!==d&&d>c)break;f=f.nextSibling}a.insertBefore(b,f)}}}function a(a){this.OdfContainer=a}function c(a,b,c){var d=this;this.size=0;this.type=null;this.name=a;this.container=b;this.onchange=this.onreadystatechange=this.document=this.url=null;this.EMPTY=0;this.LOADING=1;this.DONE=2;this.state=this.EMPTY;this.load=function(){c.loadAsDataURL(a,x[a],function(a,b){d.url=b;if(d.onchange)d.onchange(d);if(d.onstatereadychange)d.onstatereadychange(d)})};this.abort=
+function(){}}function b(){this.length=0;this.item=function(){}}var d=new odf.StyleInfo,o=new odf.Style2CSS,f="urn:oasis:names:tc:opendocument:xmlns:office:1.0",h="urn:oasis:names:tc:opendocument:xmlns:manifest:1.0",i="meta,settings,scripts,font-face-decls,styles,automatic-styles,master-styles,body".split(","),j=new core.Base64,n=new odf.FontLoader,x={};a.prototype=new function(){};a.prototype.constructor=a;a.namespaceURI=f;a.localName="document";c.prototype.load=function(){};c.prototype.getUrl=function(){return this.data?
+"data:;base64,"+j.toBase64(this.data):null};odf.OdfContainer=function t(d,i){function j(a){for(var b=a.firstChild,c;b;)c=b.nextSibling,1===b.nodeType?j(b):7===b.nodeType&&a.removeChild(b),b=c}function q(a){var b=A.rootElement.ownerDocument,c;if(a){j(a.documentElement);try{c=b.importNode(a.documentElement,!0)}catch(d){}}return c}function m(a){A.state=a;if(A.onchange)A.onchange(A);if(A.onstatereadychange)A.onstatereadychange(A)}function E(a){var a=q(a),b=A.rootElement;!a||"document-styles"!==a.localName||
+a.namespaceURI!==f?m(t.INVALID):(b.fontFaceDecls=g(a,f,"font-face-decls"),k(b,b.fontFaceDecls),b.styles=g(a,f,"styles"),k(b,b.styles),b.automaticStyles=g(a,f,"automatic-styles"),k(b,b.automaticStyles),b.masterStyles=g(a,f,"master-styles"),k(b,b.masterStyles),n.loadFonts(b.fontFaceDecls,K,null))}function C(a){var a=q(a),b,c,d;if(!a||"document-content"!==a.localName||a.namespaceURI!==f)m(t.INVALID);else{b=A.rootElement;c=g(a,f,"font-face-decls");if(b.fontFaceDecls&&c)for(d=c.firstChild;d;)b.fontFaceDecls.appendChild(d),
+d=c.firstChild;else c&&(b.fontFaceDecls=c,k(b,c));c=g(a,f,"automatic-styles");if(b.automaticStyles&&c)for(d=c.firstChild;d;)b.automaticStyles.appendChild(d),d=c.firstChild;else c&&(b.automaticStyles=c,k(b,c));b.body=g(a,f,"body");k(b,b.body)}}function w(a){var a=q(a),b;if(a&&!("document-meta"!==a.localName||a.namespaceURI!==f))b=A.rootElement,b.meta=g(a,f,"meta"),k(b,b.meta)}function G(a){var a=q(a),b;if(a&&!("document-settings"!==a.localName||a.namespaceURI!==f))b=A.rootElement,b.settings=g(a,f,
+"settings"),k(b,b.settings)}function l(a,b){K.loadAsDOM(a,b)}function v(){l("styles.xml",function(a,b){E(b);A.state!==t.INVALID&&l("content.xml",function(a,b){C(b);A.state!==t.INVALID&&l("meta.xml",function(a,b){w(b);A.state!==t.INVALID&&l("settings.xml",function(a,b){b&&G(b);l("META-INF/manifest.xml",function(a,b){if(b){var c=q(b),d;if(c&&!("manifest"!==c.localName||c.namespaceURI!==h)){d=A.rootElement;d.manifest=c;for(c=d.manifest.firstChild;c;)1===c.nodeType&&"file-entry"===c.localName&&c.namespaceURI===
+h&&(x[c.getAttributeNS(h,"full-path")]=c.getAttributeNS(h,"media-type")),c=c.nextSibling}}A.state!==t.INVALID&&m(t.DONE)})})})})})}function y(a,b){var c="",d;for(d in b)b.hasOwnProperty(d)&&(c+=" xmlns:"+d+'="'+b[d]+'"');return'<?xml version="1.0" encoding="UTF-8"?><office:'+a+" "+c+' office:version="1.2">'}function B(){var a=o.namespaces,b=new xmldom.LSSerializer,c=y("document-meta",a);b.filter=new e(A.rootElement);c+=b.writeToString(A.rootElement.meta,a);return c+"</office:document-meta>"}function M(){var a=
+o.namespaces,b=new xmldom.LSSerializer,c=y("document-settings",a);b.filter=new e(A.rootElement);c+=b.writeToString(A.rootElement.settings,a);return c+"</office:document-settings>"}function F(){var a=o.namespaces,b=new xmldom.LSSerializer,c=y("document-styles",a);b.filter=new e(A.rootElement,A.rootElement.masterStyles);c+=b.writeToString(A.rootElement.fontFaceDecls,a);c+=b.writeToString(A.rootElement.styles,a);c+=b.writeToString(A.rootElement.automaticStyles,a);c+=b.writeToString(A.rootElement.masterStyles,
+a);return c+"</office:document-styles>"}function R(){var a=o.namespaces,b=new xmldom.LSSerializer,c=y("document-content",a);b.filter=new e(A.rootElement,A.rootElement.body);c+=b.writeToString(A.rootElement.automaticStyles,a);c+=b.writeToString(A.rootElement.body,a);return c+"</office:document-content>"}function S(a,b){runtime.loadXML(a,function(a,c){if(a)b(a);else{var d=q(c);!d||"document"!==d.localName||d.namespaceURI!==f?m(t.INVALID):(A.rootElement=d,d.fontFaceDecls=g(d,f,"font-face-decls"),d.styles=
+g(d,f,"styles"),d.automaticStyles=g(d,f,"automatic-styles"),d.masterStyles=g(d,f,"master-styles"),d.body=g(d,f,"body"),d.meta=g(d,f,"meta"),m(t.DONE))}})}var A=this,K=null;this.onstatereadychange=i;this.parts=this.rootElement=this.state=this.onchange=null;this.getPart=function(a){return new c(a,A,K)};this.save=function(a){var b;b=runtime.byteArrayFromString(M(),"utf8");K.save("settings.xml",b,!0,new Date);b=runtime.byteArrayFromString(B(),"utf8");K.save("meta.xml",b,!0,new Date);b=runtime.byteArrayFromString(F(),
+"utf8");K.save("styles.xml",b,!0,new Date);b=runtime.byteArrayFromString(R(),"utf8");K.save("content.xml",b,!0,new Date);K.write(function(b){a(b)})};this.state=t.LOADING;this.rootElement=function(a){var b=document.createElementNS(a.namespaceURI,a.localName),c,a=new a;for(c in a)a.hasOwnProperty(c)&&(b[c]=a[c]);return b}(a);this.parts=new b(this);K=new core.Zip(d,function(a,b){K=b;a?S(d,function(b){a&&(K.error=a+"\n"+b,m(t.INVALID))}):v()})};odf.OdfContainer.EMPTY=0;odf.OdfContainer.LOADING=1;odf.OdfContainer.DONE=
+2;odf.OdfContainer.INVALID=3;odf.OdfContainer.SAVING=4;odf.OdfContainer.MODIFIED=5;odf.OdfContainer.getContainer=function(a){return new odf.OdfContainer(a,null)};return odf.OdfContainer}();
// Input 24
-odf.Formatting=function(){function i(e){function g(a,e){for(var c=a&&a.firstChild;c&&e;)c=c.nextSibling,e-=1;return c}var a=g(e.startContainer,e.startOffset);g(e.endContainer,e.endOffset);this.next=function(){return a===null?a:null}}var k=new odf.StyleInfo;this.setOdfContainer=function(){};this.isCompletelyBold=function(){return false};this.getAlignment=function(e){this.getParagraphStyles(e)};this.getParagraphStyles=function(e){var g,a,b,h=[];for(g=0;g<e.length;g+=0){a=void 0;b=[];for(a=(new i(e[g])).next();a;)k.canElementHaveStyle("paragraph",
-a)&&b.push(a);for(a=0;a<b.length;a+=1)h.indexOf(b[a])===-1&&h.push(b[a])}return h};this.getTextStyles=function(){return[]}};
+odf.Formatting=function(){function g(e){function g(a,b){for(var d=a&&a.firstChild;d&&b;)d=d.nextSibling,b-=1;return d}var a=g(e.startContainer,e.startOffset);g(e.endContainer,e.endOffset);this.next=function(){return null===a?a:null}}var m=new odf.StyleInfo;this.setOdfContainer=function(){};this.isCompletelyBold=function(){return!1};this.getAlignment=function(e){this.getParagraphStyles(e)};this.getParagraphStyles=function(e){var k,a,c,b=[];for(k=0;k<e.length;k+=0){a=void 0;c=[];for(a=(new g(e[k])).next();a;)m.canElementHaveStyle("paragraph",
+a)&&c.push(a);for(a=0;a<c.length;a+=1)-1===b.indexOf(c[a])&&b.push(c[a])}return b};this.getTextStyles=function(){return[]}};
// Input 25
runtime.loadClass("odf.OdfContainer");runtime.loadClass("odf.Formatting");runtime.loadClass("xmldom.XPath");
-odf.OdfCanvas=function(){function i(a,b,c){a.addEventListener?a.addEventListener(b,c,false):a.attachEvent?a.attachEvent("on"+b,c):a["on"+b]=c}function k(a){function b(a,c){for(;c;){if(c===a)return true;c=c.parentNode}return false}function c(){var e=[],g=runtime.getWindow().getSelection(),h,i;for(h=0;h<g.rangeCount;h+=1)i=g.getRangeAt(h),i!==null&&b(a,i.startContainer)&&b(a,i.endContainer)&&e.push(i);if(e.length===d.length){for(g=0;g<e.length;g+=1)if(h=e[g],i=d[g],h=h===i?false:h===null||i===null?
-true:h.startContainer!==i.startContainer||h.startOffset!==i.startOffset||h.endContainer!==i.endContainer||h.endOffset!==i.endOffset,h)break;if(g===e.length)return}d=e;var g=Array(e.length),j,k=a.ownerDocument;for(h=0;h<e.length;h+=1)i=e[h],j=k.createRange(),j.setStart(i.startContainer,i.startOffset),j.setEnd(i.endContainer,i.endOffset),g[h]=j;d=g;g=f.length;for(e=0;e<g;e+=1)f[e](a,d)}var d=[],f=[];this.addListener=function(a,b){var c,d=f.length;for(c=0;c<d;c+=1)if(f[c]===b)return;f.push(b)};i(a,"mouseup",
-c);i(a,"keyup",c);i(a,"keydown",c)}function e(a){for(a=a.firstChild;a;){if(a.namespaceURI===f&&a.localName==="binary-data")return"data:image/png;base64,"+a.textContent;a=a.nextSibling}return""}function g(a,b,c,d){function f(b){b='draw|image[styleid="'+a+'"] {'+("background-image: url("+b+");")+"}";d.insertRule(b,d.cssRules.length)}c.setAttribute("styleid",a);var g=c.getAttributeNS(m,"href"),h;if(g)try{b.getPartUrl?(g=b.getPartUrl(g),f(g)):(h=b.getPart(g),h.onchange=function(a){f(a.url)},h.load())}catch(i){runtime.log("slight problem: "+
-i)}else g=e(c),f(g)}function a(a){var b=a.getElementsByTagName("style"),c=a.getElementsByTagName("head")[0],d="",f,b=b&&b.length>0?b[0].cloneNode(false):a.createElement("style");for(f in h)h.hasOwnProperty(f)&&f&&(d+="@namespace "+f+" url("+h[f]+");\n");b.appendChild(a.createTextNode(d));c.appendChild(b);return b}var b=new odf.Style2CSS,h=b.namespaces,c=h.draw,d=h.fo,f=h.office,j=h.svg,p=h.text,m=h.xlink,l=runtime.getWindow(),u=new xmldom.XPath,n={},q;odf.OdfCanvas=function(f){function e(a){function h(){for(var e=
-f;e.firstChild;)e.removeChild(e.firstChild);f.style.display="inline-block";f.style.background="white";e=a.rootElement;f.ownerDocument.importNode(e,true);E.setOdfContainer(a);var i=G;(new odf.Style2CSS).style2css(i.sheet,e.styles,e.automaticStyles);var i=o.sheet,k=a,q=e.body,l,m,s;m=[];for(l=q.firstChild;l&&l!==q;)if(l.namespaceURI===c&&(m[m.length]=l),l.firstChild)l=l.firstChild;else{for(;l&&l!==q&&!l.nextSibling;)l=l.parentNode;if(l&&l.nextSibling)l=l.nextSibling}for(s=0;s<m.length;s+=1){l=m[s];
-var v="frame"+String(s),y=i;l.setAttribute("styleid",v);var w=void 0,C=l.getAttributeNS(p,"anchor-type"),x=l.getAttributeNS(j,"x"),z=l.getAttributeNS(j,"y"),B=l.getAttributeNS(j,"width"),O=l.getAttributeNS(j,"height"),T=l.getAttributeNS(d,"min-height"),N=l.getAttributeNS(d,"min-width");if(C==="as-char")w="display: inline-block;";else if(C||x||z)w="position: absolute;";else if(B||O||T||N)w="display: block;";x&&(w+="left: "+x+";");z&&(w+="top: "+z+";");B&&(w+="width: "+B+";");O&&(w+="height: "+O+";");
-T&&(w+="min-height: "+T+";");N&&(w+="min-width: "+N+";");w&&(w="draw|"+l.localName+'[styleid="'+v+'"] {'+w+"}",y.insertRule(w,y.cssRules.length))}m=q.getElementsByTagNameNS(c,"image");for(s=0;s<m.length;s+=1)l=m.item(s),g("image"+String(s),k,l,i);s=u.getODFElementsWithXPath(q,".//*[*[@text:anchor-type='paragraph']]",b.namespaceResolver);for(q=0;q<s.length;q+=1)k=s[q],k.setAttributeNS&&k.setAttributeNS("urn:webodf","containsparagraphanchor",true);i.insertRule("office|presentation draw|page:nth-child(1n) { display:block; }",
-i.cssRules.length);i.insertRule("draw|page { background-color:#fff; }",i.cssRules.length);for(i=f;i.firstChild;)i.removeChild(i.firstChild);f.appendChild(e);if(n.hasOwnProperty("statereadychange")){e=n.statereadychange;for(i=0;i<e.length;i+=1)e[i](void 0)}}if(v===a)v.state===odf.OdfContainer.DONE?h():v.onchange=h}function h(){if(q){for(var a=q.ownerDocument.createDocumentFragment();q.firstChild;)a.insertBefore(q.firstChild,null);q.parentNode.replaceChild(a,q)}}var m=f.ownerDocument,v,E=new odf.Formatting,
-B=new k(f),z=a(m),G=a(m),o=a(m),x=false;this.odfContainer=function(){return v};this.slidevisibilitycss=function(){return z};this.load=this.load=function(a){f.innerHTML="loading "+a;v=new odf.OdfContainer(a,function(a){v=a;e(a)});v.onstatereadychange=e};this.save=function(a){h();v.save(a)};this.setEditable=function(a){(x=a)||h()};this.addListener=function(a,b){if(a==="selectionchange")B.addListener(a,b);else{var c=n[a];c===void 0&&(c=n[a]=[]);c.push(b)}};this.getFormatting=function(){return E};i(f,
-"click",function(a){for(var a=a||l.event,b=a.target,c=l.getSelection(),d=c.getRangeAt(0),f=d&&d.startContainer,e=d&&d.startOffset,g=d&&d.endContainer,i=d&&d.endOffset;b&&!((b.localName==="p"||b.localName==="h")&&b.namespaceURI===p);)b=b.parentNode;if(x&&b&&b.parentNode!==q)q?q.parentNode&&h():(q=b.ownerDocument.createElement("p"),q.style||(q=b.ownerDocument.createElementNS("http://www.w3.org/1999/xhtml","p")),q.style.margin="0px",q.style.padding="0px",q.style.border="0px",q.setAttribute("contenteditable",
-true)),b.parentNode.replaceChild(q,b),q.appendChild(b),q.focus(),d&&(c.removeAllRanges(),d=b.ownerDocument.createRange(),d.setStart(f,e),d.setEnd(g,i),c.addRange(d)),a.preventDefault?(a.preventDefault(),a.stopPropagation()):(a.returnValue=false,a.cancelBubble=true)})};return odf.OdfCanvas}();
+odf.OdfCanvas=function(){function g(a,b,c){a.addEventListener?a.addEventListener(b,c,!1):a.attachEvent?a.attachEvent("on"+b,c):a["on"+b]=c}function m(a){function b(a,c){for(;c;){if(c===a)return!0;c=c.parentNode}return!1}function c(){var e=[],g=runtime.getWindow().getSelection(),h,i;for(h=0;h<g.rangeCount;h+=1)i=g.getRangeAt(h),null!==i&&b(a,i.startContainer)&&b(a,i.endContainer)&&e.push(i);if(e.length===d.length){for(g=0;g<e.length&&!(h=e[g],i=d[g],h=h===i?!1:null===h||null===i?!0:h.startContainer!==
+i.startContainer||h.startOffset!==i.startOffset||h.endContainer!==i.endContainer||h.endOffset!==i.endOffset,h);g+=1);if(g===e.length)return}d=e;var g=[e.length],j,k=a.ownerDocument;for(h=0;h<e.length;h+=1)i=e[h],j=k.createRange(),j.setStart(i.startContainer,i.startOffset),j.setEnd(i.endContainer,i.endOffset),g[h]=j;d=g;g=f.length;for(e=0;e<g;e+=1)f[e](a,d)}var d=[],f=[];this.addListener=function(a,b){var c,d=f.length;for(c=0;c<d;c+=1)if(f[c]===b)return;f.push(b)};g(a,"mouseup",c);g(a,"keyup",c);g(a,
+"keydown",c)}function e(a){for(a=a.firstChild;a;){if(a.namespaceURI===h&&"binary-data"===a.localName)return"data:image/png;base64,"+a.textContent;a=a.nextSibling}return""}function k(a,b,c,d){function f(b){b='draw|image[styleid="'+a+'"] {'+("background-image: url("+b+");")+"}";d.insertRule(b,d.cssRules.length)}c.setAttribute("styleid",a);var g=c.getAttributeNS(n,"href"),h;if(g)try{b.getPartUrl?(g=b.getPartUrl(g),f(g)):(h=b.getPart(g),h.onchange=function(a){f(a.url)},h.load())}catch(i){runtime.log("slight problem: "+
+i)}else g=e(c),f(g)}function a(a,b,c){function d(a,b,c,f){z.addToQueue(function(){k(a,b,c,f)})}var f,e;f=b.getElementsByTagNameNS(o,"image");for(b=0;b<f.length;b+=1)e=f.item(b),d("image"+b,a,e,c)}function c(a){var b=a.getElementsByTagName("style"),c=a.getElementsByTagName("head")[0],f="",e,b=b&&0<b.length?b[0].cloneNode(!1):a.createElement("style");for(e in d)d.hasOwnProperty(e)&&e&&(f+="@namespace "+e+" url("+d[e]+");\n");b.appendChild(a.createTextNode(f));c.appendChild(b);return b}var b=new odf.Style2CSS,
+d=b.namespaces,o=d.draw,f=d.fo,h=d.office,i=d.svg,j=d.text,n=d.xlink,x=runtime.getWindow(),p=new xmldom.XPath,t={},r,z=new function(){function a(d){c=!0;runtime.setTimeout(function(){try{d()}catch(f){runtime.log(f)}c=!1;0<b.length&&a(b.pop())},10)}var b=[],c=!1;this.clearQueue=function(){b.length=0};this.addToQueue=function(d){if(0===b.length&&!c)return a(d);b.push(d)}};odf.OdfCanvas=function(d){function e(){var a=d.firstChild.firstChild;a&&(d.style.WebkitTransform="scale("+F+")",d.style.WebkitTransformOrigin=
+"left top",d.style.width=Math.round(F*a.offsetWidth)+"px",d.style.height=Math.round(F*a.offsetHeight)+"px")}function h(c){function g(){for(var h=d;h.firstChild;)h.removeChild(h.firstChild);d.style.display="inline-block";h=c.rootElement;d.ownerDocument.importNode(h,!0);G.setOdfContainer(c);var k=y;(new odf.Style2CSS).style2css(k.sheet,h.styles,h.automaticStyles);var k=c,m=B.sheet,l;l=h.body;var r,z,u;z=[];for(r=l.firstChild;r&&r!==l;)if(r.namespaceURI===o&&(z[z.length]=r),r.firstChild)r=r.firstChild;
+else{for(;r&&r!==l&&!r.nextSibling;)r=r.parentNode;r&&r.nextSibling&&(r=r.nextSibling)}for(u=0;u<z.length;u+=1){r=z[u];var w="frame"+u,x=m;r.setAttribute("styleid",w);var v=void 0,S=r.getAttributeNS(j,"anchor-type"),E=r.getAttributeNS(i,"x"),F=r.getAttributeNS(i,"y"),M=r.getAttributeNS(i,"width"),Q=r.getAttributeNS(i,"height"),$=r.getAttributeNS(f,"min-height"),W=r.getAttributeNS(f,"min-width");if("as-char"===S)v="display: inline-block;";else if(S||E||F)v="position: absolute;";else if(M||Q||$||W)v=
+"display: block;";E&&(v+="left: "+E+";");F&&(v+="top: "+F+";");M&&(v+="width: "+M+";");Q&&(v+="height: "+Q+";");$&&(v+="min-height: "+$+";");W&&(v+="min-width: "+W+";");v&&(v="draw|"+r.localName+'[styleid="'+w+'"] {'+v+"}",x.insertRule(v,x.cssRules.length))}u=p.getODFElementsWithXPath(l,".//*[*[@text:anchor-type='paragraph']]",b.namespaceResolver);for(z=0;z<u.length;z+=1)l=u[z],l.setAttributeNS&&l.setAttributeNS("urn:webodf","containsparagraphanchor",!0);m.insertRule("office|presentation draw|page:nth-child(1n) {display:block;}",
+m.cssRules.length);m.insertRule("draw|page { background-color:#fff; }",m.cssRules.length);for(l=d;l.firstChild;)l.removeChild(l.firstChild);l=n.createElement("div");l.style.display="inline-block";l.style.background="white";l.appendChild(h);d.appendChild(l);a(k,h.body,m);e();if(t.hasOwnProperty("statereadychange")){h=t.statereadychange;for(k=0;k<h.length;k+=1)h[k](void 0)}}w===c&&(w.state===odf.OdfContainer.DONE?g():w.onchange=g)}function k(){if(r){for(var a=r.ownerDocument.createDocumentFragment();r.firstChild;)a.insertBefore(r.firstChild,
+null);r.parentNode.replaceChild(a,r)}}var n=d.ownerDocument,w,G=new odf.Formatting,l=new m(d),v=c(n),y=c(n),B=c(n),M=!1,F=1;this.odfContainer=function(){return w};this.slidevisibilitycss=function(){return v};this.load=this.load=function(a){z.clearQueue();d.innerHTML="loading "+a;w=new odf.OdfContainer(a,function(a){w=a;h(a)});w.onstatereadychange=h};this.save=function(a){k();w.save(a)};this.setEditable=function(a){(M=a)||k()};this.addListener=function(a,b){if("selectionchange"===a)l.addListener(a,
+b);else{var c=t[a];void 0===c&&(c=t[a]=[]);b&&-1===c.indexOf(b)&&c.push(b)}};this.getFormatting=function(){return G};this.setZoomLevel=function(a){F=a;e()};this.getZoomLevel=function(){return F};this.fitToContainingElement=function(a,b){var c=d.offsetHeight/F;F=a/(d.offsetWidth/F);b/c<F&&(F=b/c);e()};this.fitToWidth=function(a){F=a/(d.offsetWidth/F);e()};this.fitToHeight=function(a){F=a/(d.offsetHeight/F);e()};g(d,"click",function(a){for(var a=a||x.event,b=a.target,c=x.getSelection(),d=0<c.rangeCount?
+c.getRangeAt(0):null,f=d&&d.startContainer,e=d&&d.startOffset,g=d&&d.endContainer,h=d&&d.endOffset;b&&!(("p"===b.localName||"h"===b.localName)&&b.namespaceURI===j);)b=b.parentNode;M&&b&&b.parentNode!==r&&(r?r.parentNode&&k():(r=b.ownerDocument.createElement("p"),r.style||(r=b.ownerDocument.createElementNS("http://www.w3.org/1999/xhtml","p")),r.style.margin="0px",r.style.padding="0px",r.style.border="0px",r.setAttribute("contenteditable",!0)),b.parentNode.replaceChild(r,b),r.appendChild(b),r.focus(),
+d&&(c.removeAllRanges(),d=b.ownerDocument.createRange(),d.setStart(f,e),d.setEnd(g,h),c.addRange(d)),a.preventDefault?(a.preventDefault(),a.stopPropagation()):(a.returnValue=!1,a.cancelBubble=!0))})};return odf.OdfCanvas}();
// Input 26
runtime.loadClass("xmldom.XPath");runtime.loadClass("odf.Style2CSS");
-gui.PresenterUI=function(){var i=new odf.Style2CSS,k=new xmldom.XPath,e=i.namespaceResolver;return function(g){var a=this;a.setInitialSlideMode=function(){a.startSlideMode("single")};a.keyDownHandler=function(b){if(!b.target.isContentEditable&&b.target.nodeName!=="input")switch(b.keyCode){case 84:a.toggleToolbar();break;case 37:case 8:a.prevSlide();break;case 39:case 32:a.nextSlide();break;case 36:a.firstSlide();break;case 35:a.lastSlide()}};a.root=function(){return a.odf_canvas.odfContainer().rootElement};
-a.firstSlide=function(){a.slideChange(function(){return 0})};a.lastSlide=function(){a.slideChange(function(a,e){return e-1})};a.nextSlide=function(){a.slideChange(function(a,e){return a+1<e?a+1:-1})};a.prevSlide=function(){a.slideChange(function(a){return a<1?-1:a-1})};a.slideChange=function(b){var e=a.getPages(a.odf_canvas.odfContainer().rootElement),c=-1,d=0;e.forEach(function(a){a=a[1];a.hasAttribute("slide_current")&&(c=d,a.removeAttribute("slide_current"));d+=1});b=b(c,e.length);b===-1&&(b=c);
-e[b][1].setAttribute("slide_current","1");document.getElementById("pagelist").selectedIndex=b;a.slide_mode==="cont"&&window.scrollBy(0,e[b][1].getBoundingClientRect().top-30)};a.selectSlide=function(b){a.slideChange(function(a,c){return b>=c?-1:b<0?-1:b})};a.scrollIntoContView=function(b){var e=a.getPages(a.odf_canvas.odfContainer().rootElement);e.length!==0&&window.scrollBy(0,e[b][1].getBoundingClientRect().top-30)};a.getPages=function(a){var a=a.getElementsByTagNameNS(e("draw"),"page"),g=[],c;for(c=
-0;c<a.length;c+=1)g.push([a[c].getAttribute("draw:name"),a[c]]);return g};a.fillPageList=function(b,e){for(var c=a.getPages(b),d,f,g;e.firstChild;)e.removeChild(e.firstChild);for(d=0;d<c.length;d+=1)f=document.createElement("option"),g=k.getODFElementsWithXPath(c[d][1],'./draw:frame[@presentation:class="title"]//draw:text-box/text:p',xmldom.XPath),g=g.length>0?g[0].textContent:c[d][0],f.textContent=d+1+": "+g,e.appendChild(f)};a.startSlideMode=function(b){var e=document.getElementById("pagelist"),
-c=a.odf_canvas.slidevisibilitycss().sheet;for(a.slide_mode=b;c.cssRules.length>0;)c.deleteRule(0);a.selectSlide(0);a.slide_mode==="single"?(c.insertRule("draw|page { position:fixed; left:0px;top:30px; z-index:1; }",0),c.insertRule("draw|page[slide_current] { z-index:2;}",1),c.insertRule("draw|page { -webkit-transform: scale(1);}",2),a.fitToWindow(),window.addEventListener("resize",a.fitToWindow,false)):a.slide_mode==="cont"&&window.removeEventListener("resize",a.fitToWindow,false);a.fillPageList(a.odf_canvas.odfContainer().rootElement,
-e)};a.toggleToolbar=function(){var b,e,c;b=a.odf_canvas.slidevisibilitycss().sheet;e=-1;for(c=0;c<b.cssRules.length;c+=1)if(b.cssRules[c].cssText.substring(0,8)===".toolbar"){e=c;break}e>-1?b.deleteRule(e):b.insertRule(".toolbar { position:fixed; left:0px;top:-200px; z-index:0; }",0)};a.fitToWindow=function(){var b=a.getPages(a.root()),e=(window.innerHeight-40)/b[0][1].clientHeight,b=(window.innerWidth-10)/b[0][1].clientWidth,e=e<b?e:b,b=a.odf_canvas.slidevisibilitycss().sheet;b.deleteRule(2);b.insertRule("draw|page { \n-moz-transform: scale("+
-e+"); \n-moz-transform-origin: 0% 0%; -webkit-transform-origin: 0% 0%; -webkit-transform: scale("+e+"); -o-transform-origin: 0% 0%; -o-transform: scale("+e+"); -ms-transform-origin: 0% 0%; -ms-transform: scale("+e+"); }",2)};a.load=function(b){a.odf_canvas.load(b)};a.odf_element=g;a.odf_canvas=new odf.OdfCanvas(a.odf_element);a.odf_canvas.addListener("statereadychange",a.setInitialSlideMode);a.slide_mode="undefined";document.addEventListener("keydown",a.keyDownHandler,false)}}();
+gui.PresenterUI=function(){var g=new odf.Style2CSS,m=new xmldom.XPath,e=g.namespaceResolver;return function(g){var a=this;a.setInitialSlideMode=function(){a.startSlideMode("single")};a.keyDownHandler=function(c){if(!(c.target.isContentEditable||"input"===c.target.nodeName))switch(c.keyCode){case 84:a.toggleToolbar();break;case 37:case 8:a.prevSlide();break;case 39:case 32:a.nextSlide();break;case 36:a.firstSlide();break;case 35:a.lastSlide()}};a.root=function(){return a.odf_canvas.odfContainer().rootElement};
+a.firstSlide=function(){a.slideChange(function(){return 0})};a.lastSlide=function(){a.slideChange(function(a,b){return b-1})};a.nextSlide=function(){a.slideChange(function(a,b){return a+1<b?a+1:-1})};a.prevSlide=function(){a.slideChange(function(a){return 1>a?-1:a-1})};a.slideChange=function(c){var b=a.getPages(a.odf_canvas.odfContainer().rootElement),d=-1,e=0;b.forEach(function(a){a=a[1];a.hasAttribute("slide_current")&&(d=e,a.removeAttribute("slide_current"));e+=1});c=c(d,b.length);-1===c&&(c=d);
+b[c][1].setAttribute("slide_current","1");document.getElementById("pagelist").selectedIndex=c;"cont"===a.slide_mode&&window.scrollBy(0,b[c][1].getBoundingClientRect().top-30)};a.selectSlide=function(c){a.slideChange(function(a,d){return c>=d||0>c?-1:c})};a.scrollIntoContView=function(c){var b=a.getPages(a.odf_canvas.odfContainer().rootElement);0!==b.length&&window.scrollBy(0,b[c][1].getBoundingClientRect().top-30)};a.getPages=function(a){var a=a.getElementsByTagNameNS(e("draw"),"page"),b=[],d;for(d=
+0;d<a.length;d+=1)b.push([a[d].getAttribute("draw:name"),a[d]]);return b};a.fillPageList=function(c,b){for(var d=a.getPages(c),e,f,g;b.firstChild;)b.removeChild(b.firstChild);for(e=0;e<d.length;e+=1)f=document.createElement("option"),g=m.getODFElementsWithXPath(d[e][1],'./draw:frame[@presentation:class="title"]//draw:text-box/text:p',xmldom.XPath),g=0<g.length?g[0].textContent:d[e][0],f.textContent=e+1+": "+g,b.appendChild(f)};a.startSlideMode=function(c){var b=document.getElementById("pagelist"),
+d=a.odf_canvas.slidevisibilitycss().sheet;for(a.slide_mode=c;0<d.cssRules.length;)d.deleteRule(0);a.selectSlide(0);"single"===a.slide_mode?(d.insertRule("draw|page { position:fixed; left:0px;top:30px; z-index:1; }",0),d.insertRule("draw|page[slide_current] { z-index:2;}",1),d.insertRule("draw|page { -webkit-transform: scale(1);}",2),a.fitToWindow(),window.addEventListener("resize",a.fitToWindow,!1)):"cont"===a.slide_mode&&window.removeEventListener("resize",a.fitToWindow,!1);a.fillPageList(a.odf_canvas.odfContainer().rootElement,
+b)};a.toggleToolbar=function(){var c,b,d;c=a.odf_canvas.slidevisibilitycss().sheet;b=-1;for(d=0;d<c.cssRules.length;d+=1)if(".toolbar"===c.cssRules[d].cssText.substring(0,8)){b=d;break}-1<b?c.deleteRule(b):c.insertRule(".toolbar { position:fixed; left:0px;top:-200px; z-index:0; }",0)};a.fitToWindow=function(){var c=a.getPages(a.root()),b=(window.innerHeight-40)/c[0][1].clientHeight,c=(window.innerWidth-10)/c[0][1].clientWidth,b=b<c?b:c,c=a.odf_canvas.slidevisibilitycss().sheet;c.deleteRule(2);c.insertRule("draw|page { \n-moz-transform: scale("+
+b+"); \n-moz-transform-origin: 0% 0%; -webkit-transform-origin: 0% 0%; -webkit-transform: scale("+b+"); -o-transform-origin: 0% 0%; -o-transform: scale("+b+"); -ms-transform-origin: 0% 0%; -ms-transform: scale("+b+"); }",2)};a.load=function(c){a.odf_canvas.load(c)};a.odf_element=g;a.odf_canvas=new odf.OdfCanvas(a.odf_element);a.odf_canvas.addListener("statereadychange",a.setInitialSlideMode);a.slide_mode="undefined";document.addEventListener("keydown",a.keyDownHandler,!1)}}();
// Input 27
-gui.Caret=function(i,k){k.ownerDocument.createElementNS("urn:webodf:names:cursor","cursor");this.updateToSelection=function(){i.rangeCount===1&&i.getRangeAt(0)}};
+gui.Caret=function(g,m){m.ownerDocument.createElementNS("urn:webodf:names:cursor","cursor");this.updateToSelection=function(){1===g.rangeCount&&g.getRangeAt(0)}};
// Input 28
runtime.loadClass("core.Cursor");
-gui.SelectionMover=function(i,k){function e(a,b){if(i.rangeCount!==0){var d=i.getRangeAt(0);if(d.startContainer&&d.startContainer.nodeType===1){k.setPoint(d.startContainer,d.startOffset);b();d=k.node();k.position();var f=[],e;for(e=0;e<i.rangeCount;e+=1)f[e]=i.getRangeAt(e);i.removeAllRanges();f.length===0&&(f[0]=d.ownerDocument.createRange());f[f.length-1].setStart(k.node(),k.position());for(e=0;e<f.length;e+=1)i.addRange(f[e])}}}function g(){b.updateToSelection();for(var a=b.getNode().getBoundingClientRect(),
-c=a.left,d=a.top,a=false,f=200;!a;){f-=1;b.remove();if(i.focusNode&&i.focusNode.nodeType===1){k.setPoint(i.focusNode,i.focusOffset);k.stepForward();var a=k.node(),e=k.position();i.collapse(a,e);b.updateToSelection()}a=b.getNode().getBoundingClientRect();a=a.top!==d&&a.left>c}}var a=k.node().ownerDocument,b=new core.Cursor(i,a);this.movePointForward=function(a){e(a,k.stepForward)};this.movePointBackward=function(a){e(a,k.stepBackward)};this.moveLineForward=function(a){i.modify?i.modify(a?"extend":
-"move","forward","line"):e(a,g)};this.moveLineBackward=function(a){i.modify?i.modify(a?"extend":"move","backward","line"):e(a,function(){})};return this};
+gui.SelectionMover=function(g,m){function e(a,c){if(0!==g.rangeCount){var e=g.getRangeAt(0);if(e.startContainer&&1===e.startContainer.nodeType){m.setPoint(e.startContainer,e.startOffset);c();e=m.node();m.position();var f=[],h;for(h=0;h<g.rangeCount;h+=1)f[h]=g.getRangeAt(h);g.removeAllRanges();0===f.length&&(f[0]=e.ownerDocument.createRange());f[f.length-1].setStart(m.node(),m.position());for(h=0;h<f.length;h+=1)g.addRange(f[h])}}}function k(){c.updateToSelection();for(var a=c.getNode().getBoundingClientRect(),
+d=a.left,e=a.top,a=!1;!a;){c.remove();if(g.focusNode&&1===g.focusNode.nodeType){m.setPoint(g.focusNode,g.focusOffset);m.stepForward();var a=m.node(),f=m.position();g.collapse(a,f);c.updateToSelection()}a=c.getNode().getBoundingClientRect();a=a.top!==e&&a.left>d}}var a=m.node().ownerDocument,c=new core.Cursor(g,a);this.movePointForward=function(a){e(a,m.stepForward)};this.movePointBackward=function(a){e(a,m.stepBackward)};this.moveLineForward=function(a){g.modify?g.modify(a?"extend":"move","forward",
+"line"):e(a,k)};this.moveLineBackward=function(a){g.modify?g.modify(a?"extend":"move","backward","line"):e(a,function(){})};return this};
// Input 29
runtime.loadClass("core.PointWalker");runtime.loadClass("core.Cursor");
-gui.XMLEdit=function(i,k){function e(a,b,c){a.addEventListener?a.addEventListener(b,c,false):a.attachEvent?a.attachEvent("on"+b,c):a["on"+b]=c}function g(a){a.preventDefault?a.preventDefault():a.returnValue=false}function a(){var a=i.ownerDocument.defaultView.getSelection();a&&!(a.rangeCount<=0)&&n&&(a=a.getRangeAt(0),n.setPoint(a.startContainer,a.startOffset))}function b(){var a=i.ownerDocument.defaultView.getSelection(),b,c;a.removeAllRanges();n&&n.node()&&(b=n.node(),c=b.ownerDocument.createRange(),
-c.setStart(b,n.position()),c.collapse(true),a.addRange(c))}function h(c){var d=c.charCode||c.keyCode;if(n=null,n&&d===37)a(),n.stepBackward(),b();else if(d>=16&&d<=20||d>=33&&d<=40)return;g(c)}function c(){}function d(a){i.ownerDocument.defaultView.getSelection().getRangeAt(0);g(a)}function f(a){for(var b=a.firstChild;b&&b!==a;)b.nodeType===1&&f(b),b=b.nextSibling||b.parentNode;var c,d,e,b=a.attributes;c="";for(e=b.length-1;e>=0;e-=1)d=b.item(e),c=c+" "+d.nodeName+'="'+d.nodeValue+'"';a.setAttribute("customns_name",
-a.nodeName);a.setAttribute("customns_atts",c);b=a.firstChild;for(d=/^\s*$/;b&&b!==a;)c=b,b=b.nextSibling||b.parentNode,c.nodeType===3&&d.test(c.nodeValue)&&c.parentNode.removeChild(c)}function j(a,b){for(var c=a.firstChild,d,e,f;c&&c!==a;){if(c.nodeType===1){j(c,b);d=c.attributes;for(f=d.length-1;f>=0;f-=1)if(e=d.item(f),e.namespaceURI==="http://www.w3.org/2000/xmlns/"&&!b[e.nodeValue])b[e.nodeValue]=e.localName}c=c.nextSibling||c.parentNode}}function p(){var a=i.ownerDocument.createElement("style"),
-b;b={};j(i,b);var c={},d,e,f=0;for(d in b)if(b.hasOwnProperty(d)&&d){e=b[d];if(!e||c.hasOwnProperty(e)||e==="xmlns"){do e="ns"+f,f+=1;while(c.hasOwnProperty(e));b[d]=e}c[e]=true}b="@namespace customns url(customns);\n";a.type="text/css";b+=m;a.appendChild(i.ownerDocument.createTextNode(b));k=k.parentNode.replaceChild(a,k)}var m,l,u,n=null;if(!i.id)i.id="xml"+String(Math.random()).substring(2);l="#"+i.id+" ";m=l+"*,"+l+":visited, "+l+":link {display:block; margin: 0px; margin-left: 10px; font-size: medium; color: black; background: white; font-variant: normal; font-weight: normal; font-style: normal; font-family: sans-serif; text-decoration: none; white-space: pre-wrap; height: auto; width: auto}\n"+
-l+":before {color: blue; content: '<' attr(customns_name) attr(customns_atts) '>';}\n"+l+":after {color: blue; content: '</' attr(customns_name) '>';}\n"+l+"{overflow: auto;}\n";(function(a){e(a,"click",d);e(a,"keydown",h);e(a,"keypress",c);e(a,"drop",g);e(a,"dragend",g);e(a,"beforepaste",g);e(a,"paste",g)})(i);this.updateCSS=p;this.setXML=function(a){a=a.documentElement||a;u=a=i.ownerDocument.importNode(a,true);for(f(a);i.lastChild;)i.removeChild(i.lastChild);i.appendChild(a);p();n=new core.PointWalker(a)};
-this.getXML=function(){return u}};
+gui.XMLEdit=function(g,m){function e(a,b,c){a.addEventListener?a.addEventListener(b,c,!1):a.attachEvent?a.attachEvent("on"+b,c):a["on"+b]=c}function k(a){a.preventDefault?a.preventDefault():a.returnValue=!1}function a(){var a=g.ownerDocument.defaultView.getSelection();a&&!(0>=a.rangeCount)&&p&&(a=a.getRangeAt(0),p.setPoint(a.startContainer,a.startOffset))}function c(){var a=g.ownerDocument.defaultView.getSelection(),b,c;a.removeAllRanges();p&&p.node()&&(b=p.node(),c=b.ownerDocument.createRange(),
+c.setStart(b,p.position()),c.collapse(!0),a.addRange(c))}function b(b){var d=b.charCode||b.keyCode;if(p=null,p&&37===d)a(),p.stepBackward(),c();else if(16<=d&&20>=d||33<=d&&40>=d)return;k(b)}function d(){}function o(a){g.ownerDocument.defaultView.getSelection().getRangeAt(0);k(a)}function f(a){for(var b=a.firstChild;b&&b!==a;)1===b.nodeType&&f(b),b=b.nextSibling||b.parentNode;var c,d,e,b=a.attributes;c="";for(e=b.length-1;0<=e;e-=1)d=b.item(e),c=c+" "+d.nodeName+'="'+d.nodeValue+'"';a.setAttribute("customns_name",
+a.nodeName);a.setAttribute("customns_atts",c);b=a.firstChild;for(d=/^\s*$/;b&&b!==a;)c=b,b=b.nextSibling||b.parentNode,3===c.nodeType&&d.test(c.nodeValue)&&c.parentNode.removeChild(c)}function h(a,b){for(var c=a.firstChild,d,e,f;c&&c!==a;){if(1===c.nodeType){h(c,b);d=c.attributes;for(f=d.length-1;0<=f;f-=1)e=d.item(f),"http://www.w3.org/2000/xmlns/"===e.namespaceURI&&!b[e.nodeValue]&&(b[e.nodeValue]=e.localName)}c=c.nextSibling||c.parentNode}}function i(){var a=g.ownerDocument.createElement("style"),
+b;b={};h(g,b);var c={},d,e,f=0;for(d in b)if(b.hasOwnProperty(d)&&d){e=b[d];if(!e||c.hasOwnProperty(e)||"xmlns"===e){do e="ns"+f,f+=1;while(c.hasOwnProperty(e));b[d]=e}c[e]=!0}a.type="text/css";b="@namespace customns url(customns);\n"+j;a.appendChild(g.ownerDocument.createTextNode(b));m=m.parentNode.replaceChild(a,m)}var j,n,x,p=null;g.id||(g.id="xml"+(""+Math.random()).substring(2));n="#"+g.id+" ";j=n+"*,"+n+":visited, "+n+":link {display:block; margin: 0px; margin-left: 10px; font-size: medium; color: black; background: white; font-variant: normal; font-weight: normal; font-style: normal; font-family: sans-serif; text-decoration: none; white-space: pre-wrap; height: auto; width: auto}\n"+
+n+":before {color: blue; content: '<' attr(customns_name) attr(customns_atts) '>';}\n"+n+":after {color: blue; content: '</' attr(customns_name) '>';}\n"+n+"{overflow: auto;}\n";(function(a){e(a,"click",o);e(a,"keydown",b);e(a,"keypress",d);e(a,"drop",k);e(a,"dragend",k);e(a,"beforepaste",k);e(a,"paste",k)})(g);this.updateCSS=i;this.setXML=function(a){a=a.documentElement||a;x=a=g.ownerDocument.importNode(a,!0);for(f(a);g.lastChild;)g.removeChild(g.lastChild);g.appendChild(a);i();p=new core.PointWalker(a)};
+this.getXML=function(){return x}};
+// Input 30
+(function(){return"core/Async.js,core/Base64.js,core/ByteArray.js,core/ByteArrayWriter.js,core/Cursor.js,core/JSLint.js,core/PointWalker.js,core/RawDeflate.js,core/RawInflate.js,core/UnitTester.js,core/Zip.js,gui/Caret.js,gui/SelectionMover.js,gui/XMLEdit.js,gui/PresenterUI.js,odf/FontLoader.js,odf/Formatting.js,odf/OdfCanvas.js,odf/OdfContainer.js,odf/Style2CSS.js,odf/StyleInfo.js,xmldom/LSSerializer.js,xmldom/LSSerializerFilter.js,xmldom/OperationalTransformDOM.js,xmldom/OperationalTransformInterface.js,xmldom/RelaxNG.js,xmldom/RelaxNG2.js,xmldom/RelaxNGParser.js,xmldom/XPath.js".split(",")})();

File Metadata

Mime Type
text/x-diff
Expires
Mon, Aug 17, 5:10 PM (1 d, 8 h)
Storage Engine
local-disk
Storage Format
Raw Data
Storage Handle
4c/3d/1f2631de37b099249fd0da50c5d0
Default Alt Text
(1 MB)

Event Timeline