diff --git a/plugins/calendar/.gitignore b/plugins/calendar/.gitignore index 93262bcd..7c2f14c7 100644 --- a/plugins/calendar/.gitignore +++ b/plugins/calendar/.gitignore @@ -4,4 +4,5 @@ *~ 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//SQL/.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 @@ -13,7 +13,7 @@ + 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 @@ -25,13 +25,13 @@ + 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) @@ -39,9 +39,10 @@ + 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 @@ -99,9 +99,9 @@ class calendar extends rcube_plugin // 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); @@ -205,7 +205,7 @@ class calendar extends rcube_plugin switch ($driver_name) { case "kolab": - $this->require_plugin('kolab_core'); + $this->require_plugin('libkolab'); default: $this->driver = new $driver_class($this); break; @@ -1069,6 +1069,11 @@ class calendar extends rcube_plugin $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; } @@ -1259,7 +1264,47 @@ class calendar extends rcube_plugin 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 */ @@ -1278,7 +1323,7 @@ class calendar extends rcube_plugin 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 . ';'; @@ -1534,12 +1579,8 @@ class calendar extends rcube_plugin } 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'])) { @@ -1550,16 +1591,30 @@ class calendar extends rcube_plugin 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"); @@ -1573,13 +1628,11 @@ class calendar extends rcube_plugin 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. @@ -1598,7 +1651,7 @@ class calendar extends rcube_plugin $disposition = !empty($_GET['_download']) ? 'attachment' : 'inline'; header("Content-Disposition: $disposition; filename=\"$filename\""); - echo $body; + echo $plugin['body']; } exit; @@ -1614,7 +1667,7 @@ class calendar extends rcube_plugin */ public function attachment_frame($attrib) { - $attachment = $_SESSION['calendar_attachment']; + $attachment = $GLOBALS['calendar_attachment']; $mimetype = strtolower($attachment['mimetype']); list($ctype_primary, $ctype_secondary) = explode('/', $mimetype); @@ -2167,15 +2220,15 @@ class calendar extends rcube_plugin $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); @@ -2312,8 +2365,8 @@ class calendar extends rcube_plugin $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) { @@ -2331,7 +2384,7 @@ class calendar extends rcube_plugin 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, 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 @@ -260,7 +260,7 @@ function rcube_calendar_ui(settings) 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); @@ -337,7 +337,7 @@ function rcube_calendar_ui(settings) 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(); @@ -363,9 +363,10 @@ function rcube_calendar_ui(settings) 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])); @@ -415,7 +416,7 @@ function rcube_calendar_ui(settings) $('#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() { @@ -2348,7 +2349,7 @@ function rcube_calendar_ui(settings) 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); @@ -2357,7 +2358,7 @@ function rcube_calendar_ui(settings) 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; @@ -2407,7 +2408,7 @@ function rcube_calendar_ui(settings) } }, 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); } }); @@ -2436,7 +2437,7 @@ function rcube_calendar_ui(settings) /* 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; 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 @@ -396,31 +396,13 @@ class database_driver extends calendar_driver */ 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; } 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 @@ -8,9 +8,12 @@ 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` ( 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 @@ -7,7 +7,7 @@ * @author Thomas Bruederli * @author Aleksander Machniak * - * Copyright (C) 2011, Kolab Systems AG + * Copyright (C) 2012, 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 @@ -26,6 +26,9 @@ 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; @@ -35,17 +38,10 @@ class kolab_calendar 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'); /** @@ -59,12 +55,11 @@ class kolab_calendar $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) { @@ -73,8 +68,8 @@ class kolab_calendar $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; } @@ -96,7 +91,7 @@ class kolab_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; } @@ -119,7 +114,7 @@ class kolab_calendar */ public function get_owner() { - return $this->storage->_folder->getOwner(); + return $this->storage->get_owner(); } @@ -130,10 +125,7 @@ class kolab_calendar */ 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(); } @@ -154,7 +146,8 @@ class kolab_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; } @@ -168,19 +161,11 @@ class kolab_calendar } /** - * 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; } @@ -189,17 +174,21 @@ class kolab_calendar */ 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]; } @@ -208,13 +197,21 @@ class kolab_calendar * @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); @@ -275,9 +272,9 @@ class kolab_calendar //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__, @@ -303,15 +300,15 @@ class kolab_calendar 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__, @@ -334,29 +331,15 @@ class kolab_calendar */ public function delete_event($event, $force = true) { - $deleted = false; + $deleted = $this->storage->delete($event['id'], $force); - if (!$force) { - // Get IMAP object ID - $imap_uid = $this->storage->_getStorageId($event['id']); - } - - $deleteme = $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; } @@ -369,63 +352,18 @@ class kolab_calendar */ 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; } @@ -471,308 +409,99 @@ class kolab_calendar /** * 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); + $record['sensitivity'] = intval($sensitivity_map[$record['sensitivity']]); - if (!empty($rec['_attachments'])) { - foreach ($rec['_attachments'] as $name => $attachment) { - // @TODO: 'type' and 'key' are the only supported (no 'size') - $attachments[] = array( - 'id' => $attachment['key'], - 'mimetype' => $attachment['type'], - 'name' => $name, - ); - } - } - - if ($rec['organizer']) { - $attendees[] = array( - 'role' => 'ORGANIZER', - 'name' => $rec['organizer']['display-name'], - 'email' => $rec['organizer']['smtp-address'], - 'status' => 'ACCEPTED', - ); - $_attendees .= $rec['organizer']['display-name'] . ' ' . $rec['organizer']['smtp-address'] . ' '; - } - - foreach ((array)$rec['attendee'] as $attendee) { - $attendees[] = array( - 'role' => $role_map[$attendee['role']], - 'name' => $attendee['display-name'], - 'email' => $attendee['smtp-address'], - 'status' => $status_map[$attendee['status']], - 'rsvp' => $attendee['request-response'], - ); - $_attendees .= $rec['organizer']['display-name'] . ' ' . $rec['organizer']['smtp-address'] . ' '; - } - // Roundcube only supports one category assignment - $categories = explode(',', $rec['categories']); - - return array( - 'id' => $rec['uid'], - 'uid' => $rec['uid'], - 'title' => $rec['summary'], - 'location' => $rec['location'], - 'description' => $rec['body'], - 'start' => $rec['start-date'], - 'end' => $rec['end-date'], - 'allday' => $allday, - 'recurrence' => $rrule, - 'alarms' => $alarm_value . $alarm_unit, - '_alarm' => intval($rec['alarm']), - 'categories' => $categories[0], - 'attachments' => $attachments, - 'attendees' => $attendees, - '_attendees' => $_attendees, - 'free_busy' => $rec['show-time-as'], - 'priority' => is_numeric($rec['priority']) ? intval($rec['priority']) : (isset($this->priority_map[$rec['priority']]) ? $this->priority_map[$rec['priority']] : 0), - 'sensitivity' => $sensitivity_map[$rec['sensitivity']], - 'changed' => $rec['last-modification-date'], - 'calendar' => $this->id, - ); + 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 @@ -7,7 +7,7 @@ * @author Thomas Bruederli * @author Aleksander Machniak * - * Copyright (C) 2011, Kolab Systems AG + * Copyright (C) 2012, 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 @@ -33,6 +33,7 @@ class kolab_driver extends calendar_driver public $freebusy = true; public $attachments = true; public $undelete = true; + public $alarm_types = array('DISPLAY','EMAIL'); public $categoriesimmutable = true; private $rc; @@ -64,7 +65,7 @@ class kolab_driver extends calendar_driver 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)) { @@ -134,7 +135,7 @@ class kolab_driver extends calendar_driver '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), ); } } @@ -160,11 +161,11 @@ class kolab_driver extends calendar_driver } // 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()); @@ -197,7 +198,7 @@ class kolab_driver extends calendar_driver } // 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()); @@ -226,13 +227,9 @@ class kolab_driver extends calendar_driver 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; } @@ -302,24 +299,24 @@ class kolab_driver extends calendar_driver // 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 } @@ -337,7 +334,7 @@ class kolab_driver extends calendar_driver { 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']]); @@ -346,7 +343,7 @@ class kolab_driver extends calendar_driver return true; } else - $this->last_error = rcube_kolab::$last_error; + $this->last_error = kolab_storage::$last_error; } return false; @@ -404,7 +401,6 @@ class kolab_driver extends calendar_driver } } - $GLOBALS['conf']['kolab']['no_triggering'] = true; $success = $storage->insert_event($event); if ($success) @@ -475,7 +471,6 @@ class kolab_driver extends calendar_driver $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']) { @@ -566,7 +561,6 @@ class kolab_driver extends calendar_driver return false; $fromcalendar = $storage; - $storage->storage->synchronize(); } } else @@ -583,7 +577,7 @@ class kolab_driver extends calendar_driver if (!empty($old['attachments'])) { foreach ($old['attachments'] as $idx => $att) { if ($att['id'] == $attachment) { - unset($old['attachments'][$idx]); + $old['attachments'][$idx]['_deleted'] = true; } } } @@ -596,12 +590,12 @@ class kolab_driver extends calendar_driver // 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'], ); } } @@ -618,8 +612,6 @@ class kolab_driver extends calendar_driver 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 @@ -629,7 +621,7 @@ class kolab_driver extends calendar_driver // 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); @@ -773,17 +765,19 @@ class kolab_driver extends calendar_driver $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']; } } } @@ -792,11 +786,13 @@ class kolab_driver extends calendar_driver 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; @@ -825,15 +821,24 @@ class kolab_driver extends calendar_driver */ 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 ); @@ -876,13 +881,14 @@ class kolab_driver extends calendar_driver /** * 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); } /** @@ -986,12 +992,11 @@ class kolab_driver extends calendar_driver 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', @@ -1048,7 +1053,7 @@ class kolab_driver extends calendar_driver // 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, ' » ', rcube_kolab::object_name($folder))) + $formfields['name']['value'] = Q(str_replace($delimiter, ' » ', kolab_storage::object_name($folder))) . $input_name->show($folder); } @@ -1065,7 +1070,7 @@ class kolab_driver extends calendar_driver $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 : ''), 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 @@ +_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 . "
\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 @@ -2,779 +2,10 @@ /** * 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 . "
\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 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 @@ + + * @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 + * @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('/(? $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('/(?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('/(?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('/(?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 + * @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 + * @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 + * @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 + * @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 + * @author Karsten Fourmont + * @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 + * @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 + * @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 @@ -124,7 +124,10 @@ class calendar_ical 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; 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 @@ -239,10 +239,11 @@ class calendar_itip 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, 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 @@ -450,17 +450,16 @@ class calendar_ui $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'])); @@ -475,8 +474,7 @@ class calendar_ui $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(); @@ -538,13 +536,10 @@ class calendar_ui $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, ''); @@ -555,8 +550,6 @@ class calendar_ui $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() . ' ' . $select_wday->show(); } @@ -664,13 +657,13 @@ class calendar_ui 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); 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 "//' $SRCDIR/String.php +echo "\n" +sed '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///' $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 @@ -4,7 +4,7 @@ http://pear.php.net/dtd/package-2.0 http://pear.php.net/dtd/package-2.0.xsd"> calendar - http://git.kolab.org/roundcube-plugins-kolab/ + http://git.kolab.org/roundcubemail-plugins-kolab/ Calendar plugin - @@ -64,10 +64,9 @@ - - - - + + + @@ -157,6 +156,7 @@ + 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 @@ -286,39 +286,45 @@ a.miniColors-trigger { #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; } @@ -502,8 +508,8 @@ td.topalign { .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, @@ -540,8 +546,6 @@ td.topalign { #edit-attendees-notify { margin: 0.3em 0; padding: 0.5em; - border: 1px solid #ffdf0e; - background-color: #fef893; } #edit-attendees-table { 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 @@ -26,7 +26,7 @@
- +
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 @@ -51,7 +51,7 @@ class kolab_addressbook extends rcube_plugin $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')); @@ -245,7 +245,7 @@ class kolab_addressbook extends rcube_plugin } // 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( @@ -264,7 +264,7 @@ class kolab_addressbook extends rcube_plugin 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; } @@ -289,12 +289,11 @@ class kolab_addressbook extends rcube_plugin // 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']); @@ -304,8 +303,9 @@ class kolab_addressbook extends rcube_plugin $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), ) ); */ @@ -481,7 +481,7 @@ class kolab_addressbook extends rcube_plugin if (!$plugin['abort']) { if ($oldfolder != $folder) - $result = rcube_kolab::folder_rename($oldfolder, $folder); + $result = kolab_storage::folder_rename($oldfolder, $folder); else $result = true; } @@ -497,7 +497,7 @@ class kolab_addressbook extends rcube_plugin $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']; @@ -545,7 +545,7 @@ class kolab_addressbook extends rcube_plugin $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, @@ -553,7 +553,7 @@ class kolab_addressbook extends rcube_plugin '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'); } @@ -574,12 +574,12 @@ class kolab_addressbook extends rcube_plugin { $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'); 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 @@ -5,7 +5,7 @@ * * @author Aleksander Machniak * - * Copyright (C) 2011, Kolab Systems AG + * Copyright (C) 2012, 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 @@ -144,8 +144,8 @@ class kolab_addressbook_ui 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(); @@ -156,7 +156,7 @@ class kolab_addressbook_ui ); if (!empty($options) && ($options['norename'] || $options['protected'])) { - $foldername = Q(str_replace($delimiter, ' » ', rcube_kolab::object_name($folder))); + $foldername = Q(str_replace($delimiter, ' » ', kolab_storage::object_name($folder))); } else { $foldername = new html_inputfield(array('name' => '_name', 'id' => '_name', 'size' => 30)); @@ -178,7 +178,7 @@ class kolab_addressbook_ui $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'), 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 @@ -4,7 +4,7 @@ * 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 * @author Aleksander Machniak @@ -46,30 +46,28 @@ class rcube_kolab_contacts extends rcube_addressbook '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 ); /** @@ -86,47 +84,14 @@ class rcube_kolab_contacts extends rcube_addressbook 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) @@ -136,9 +101,9 @@ class rcube_kolab_contacts extends rcube_addressbook } // 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) { @@ -147,17 +112,17 @@ class rcube_kolab_contacts extends rcube_addressbook } // 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; @@ -166,6 +131,8 @@ class rcube_kolab_contacts extends rcube_addressbook } } } + + $this->action = rcmail::get_instance()->action; } @@ -176,7 +143,7 @@ class rcube_kolab_contacts extends rcube_addressbook */ 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; } @@ -192,17 +159,6 @@ class rcube_kolab_contacts extends rcube_addressbook } - /** - * 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 * @@ -210,8 +166,8 @@ class rcube_kolab_contacts extends rcube_addressbook */ 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; @@ -270,8 +226,8 @@ class rcube_kolab_contacts extends rcube_addressbook $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 @@ -290,23 +246,38 @@ class rcube_kolab_contacts extends rcube_addressbook */ 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)) { @@ -348,8 +319,6 @@ class rcube_kolab_contacts extends rcube_addressbook */ 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; @@ -384,6 +353,25 @@ class rcube_kolab_contacts extends rcube_addressbook // 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()); @@ -408,17 +396,19 @@ class rcube_kolab_contacts extends rcube_addressbook } 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) { @@ -461,9 +451,17 @@ class rcube_kolab_contacts extends rcube_addressbook */ 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); } @@ -488,11 +486,21 @@ class rcube_kolab_contacts extends rcube_addressbook */ 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; @@ -512,7 +520,7 @@ class rcube_kolab_contacts extends rcube_addressbook foreach ((array)$this->groupmembers[$id] as $gid) { if ($group = $this->distlists[$gid]) - $out[$gid] = $group['last-name']; + $out[$gid] = $group['name']; } return $out; @@ -546,26 +554,21 @@ class rcube_kolab_contacts extends rcube_addressbook } if (!$existing) { - $this->_connect(); - // generate new Kolab contact item $object = $this->_from_rcube_contact($save_data); - $object['uid'] = $this->contactstorage->generateUID(); + $saved = $this->storagefolder->save($object, 'contact'); - $saved = $this->contactstorage->save($object); - - 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; } } @@ -586,22 +589,21 @@ class rcube_kolab_contacts extends rcube_addressbook 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 } } @@ -619,45 +621,38 @@ class rcube_kolab_contacts extends rcube_addressbook */ 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; } @@ -675,52 +670,22 @@ class rcube_kolab_contacts extends rcube_addressbook 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; } @@ -729,11 +694,8 @@ class rcube_kolab_contacts extends rcube_addressbook */ 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; } } @@ -760,22 +722,22 @@ class rcube_kolab_contacts extends rcube_addressbook $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); } @@ -795,13 +757,13 @@ class rcube_kolab_contacts extends rcube_addressbook $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 @@ -822,16 +784,16 @@ class rcube_kolab_contacts extends rcube_addressbook $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; } @@ -854,8 +816,7 @@ class rcube_kolab_contacts extends rcube_addressbook $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) @@ -865,28 +826,37 @@ class rcube_kolab_contacts extends rcube_addressbook $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; } @@ -921,13 +891,13 @@ class rcube_kolab_contacts extends rcube_addressbook // 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 { @@ -970,44 +940,16 @@ class rcube_kolab_contacts extends rcube_addressbook } /** - * Establishes a connection to the Kolab_Data object for accessing contact data + * Query storage layer and store records in private member var */ - private function _connect() - { - if (!isset($this->contactstorage)) { - $this->contactstorage = $this->storagefolder->getData(null); - } - } - - /** - * Establishes a connection to the Kolab_Data object for accessing groups data - */ - private function _connect_groups() - { - if (!isset($this->liststorage)) { - $this->liststorage = $this->storagefolder->getData('distributionlist'); - } - } - - /** - * Simply fetch all records and store them in private member vars - */ - private function _fetch_contacts() + 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']; } } } @@ -1055,178 +997,158 @@ class rcube_kolab_contacts extends rcube_addressbook /** * 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(), - ); + $record['ID'] = $this->_uid2id($record['uid']); - foreach ($this->fieldmap as $kolab => $rcube) { - if (strlen($record[$kolab])) - $out[$rcube] = $record[$kolab]; + 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(); + if (!$contact['uid'] && $contact['ID']) + $contact['uid'] = $this->_id2uid($contact['ID']); + else if (!$contact['uid'] && $old['uid']) + $contact['uid'] = $old['uid']; - foreach (array_flip($this->fieldmap) as $rcube => $kolab) { - if (isset($contact[$rcube])) - $object[$kolab] = is_array($contact[$rcube]) ? $contact[$rcube][0] : $contact[$rcube]; - else if ($values = $this->get_col_values($rcube, $contact, true)) - $object[$kolab] = is_array($values) ? $values[0] : $values; - } - - // format dates - if ($object['birthday'] && ($date = @strtotime($object['birthday']))) - $object['birthday'] = date('Y-m-d', $date); - if ($object['anniversary'] && ($date = @strtotime($object['anniversary']))) - $object['anniversary'] = date('Y-m-d', $date); - - $gendermap = array_flip($this->gender_map); - if (isset($object['gender'])) - $object['gender'] = $gendermap[$object['gender']]; - - $emails = $this->get_col_values('email', $contact, true); - $object['emails'] = join(', ', array_filter($emails)); - // overwrite 'email' field - $object['email'] = null; - - foreach ($this->get_col_values('phone', $contact) as $type => $values) { - if ($this->phonetypemap[$type]) - $type = $this->phonetypemap[$type]; - foreach ((array)$values as $phone) { - if (!empty($phone)) { - $object['phone-' . $type] = $phone; - $object['phone'][] = array('number' => $phone, 'type' => $type); + $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]); } - $object['address'] = array(); + foreach ($this->get_col_values('phone', $contact) as $type => $values) { + foreach ((array)$values as $phone) { + if (!empty($phone)) { + $contact['phone'][] = array('number' => $phone, 'type' => $type); + } + } + unset($contact['phone:'.$type]); + } + $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; + + // copy meta data (starting with _) from old object + foreach ((array)$old as $key => $val) { + if (!isset($contact[$key]) && $key[0] == '_') + $contact[$key] = $val; } - // save new photo as attachment - if ($contact['photo']) { - $attkey = 'photo.attachment'; - $object['_attachments'][$attkey] = array( - 'type' => rc_image_content_type($contact['photo']), - 'content' => preg_match('![^a-z0-9/=+-]!i', $contact['photo']) ? $contact['photo'] : base64_decode($contact['photo']), - ); - $object['picture'] = $attkey; - } - - return $object; + // 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 @@ -5,7 +5,8 @@ $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'; 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 @@ -5,7 +5,8 @@ $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'; 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 @@ -5,7 +5,8 @@ $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'; 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 @@ -4,7 +4,7 @@ http://pear.php.net/dtd/package-2.0 http://pear.php.net/dtd/package-2.0.xsd"> kolab_addressbook - http://git.kolab.org/roundcube-plugins-kolab/ + http://git.kolab.org/roundcubemail-plugins-kolab/ Kolab addressbook Sample plugin to add a new address book source with data from Kolab storage. 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="pagetitle" /> + + + + +

+ +
+ +
+ +
+
+ +
+
+ + + + + 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 @@ -4,7 +4,7 @@ http://pear.php.net/dtd/package-2.0 http://pear.php.net/dtd/package-2.0.xsd"> kolab_auth - http://git.kolab.org/roundcube-plugins-kolab/ + http://git.kolab.org/roundcubemail-plugins-kolab/ Kolab Authentication Authenticates on LDAP server, finds canonized authentication ID for IMAP 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 @@ -61,16 +61,9 @@ class kolab_config extends rcube_plugin 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(); 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 @@ -4,7 +4,7 @@ http://pear.php.net/dtd/package-2.0 http://pear.php.net/dtd/package-2.0.xsd"> kolab_config - http://git.kolab.org/roundcube-plugins-kolab/ + http://git.kolab.org/roundcubemail-plugins-kolab/ Kolab configuration storage Plugin to use Kolab server as a configuration storage. Provides an API to handle 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 @@ -4,7 +4,7 @@ http://pear.php.net/dtd/package-2.0 http://pear.php.net/dtd/package-2.0.xsd"> kolab_core - http://git.kolab.org/roundcube-plugins-kolab/ + http://git.kolab.org/roundcubemail-plugins-kolab/ Kolab API Plugin to setup a basic environment for interaction with a Kolab server. 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 @@ -30,8 +30,6 @@ class kolab_folders extends rcube_plugin public $mail_types = array('inbox', 'drafts', 'sentitems', 'outbox', 'wastebasket', 'junkemail'); private $rc; - const CTYPE_KEY = '/shared/vendor/kolab/folder-type'; - /** * Plugin initialization. @@ -40,6 +38,9 @@ class kolab_folders extends rcube_plugin { $this->rc = rcmail::get_instance(); + // load required plugin + $this->require_plugin('libkolab'); + // Folder listing hooks $this->add_hook('storage_folders', array($this, 'mailboxes_list')); @@ -57,68 +58,32 @@ class kolab_folders extends rcube_plugin */ function mailboxes_list($args) { + // infinite loop prevention + if ($this->is_processing) { + return $args; + } + if (!$this->metadata_support()) { return $args; } - $filter = $args['filter']; + $this->is_processing = true; - // all-folders request, use core method - if (!$filter) { + // get folders + $folders = kolab_storage::list_folders($args['root'], $args['name'], $args['filter'], $args['mode'] == 'LSUB'); + + $this->is_processing = false; + + if (!is_array($folders)) { return $args; } - // get folders types - $folderdata = $this->get_folder_type_list($args['root'].$args['name'], true); - - if (!is_array($folderdata)) { - return $args; + // Create default folders + if ($args['root'] == '' && $args['name'] = '*') { + $this->create_default_folders($folders, $args['filter']); } - $regexp = '/^' . preg_quote($filter, '/') . '(\..+)?$/'; - - // In some conditions we can skip LIST command (?) - if ($args['mode'] == 'LIST' && $filter != 'mail' - && $args['root'] == '' && $args['name'] == '*' - ) { - foreach ($folderdata as $folder => $type) { - if (!preg_match($regexp, $type)) { - unset($folderdata[$folder]); - } - } - $args['folders'] = array_keys($folderdata); - return $args; - } - - $storage = $this->rc->get_storage(); - - // Get folders list - if ($args['mode'] == 'LIST') { - if (!$storage->check_connection()) { - return $args; - } - $args['folders'] = $storage->conn->listMailboxes($args['root'], $args['name']); - } - else { - $args['folders'] = $this->list_subscribed($args['root'], $args['name']); - } - - // In case of an error, return empty list - if (!is_array($args['folders'])) { - $args['folders'] = array(); - return $args; - } - - // Filter folders list - foreach ($args['folders'] as $idx => $folder) { - $type = $folderdata[$folder]; - if ($filter == 'mail' && empty($type)) { - continue; - } - if (empty($type) || !preg_match($regexp, $type)) { - unset($args['folders'][$idx]); - } - } + $args['folders'] = $folders; return $args; } @@ -132,10 +97,11 @@ class kolab_folders extends rcube_plugin 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; @@ -146,7 +112,7 @@ class kolab_folders extends rcube_plugin 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'; @@ -266,8 +232,6 @@ class kolab_folders extends rcube_plugin { // Folder actions from folders list if (empty($args['record'])) { - // Just clear Horde folders cache and return - $this->clear_folders_cache(); return $args; } @@ -340,11 +304,6 @@ class kolab_folders extends rcube_plugin } } - // 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; @@ -355,7 +314,7 @@ class kolab_folders extends rcube_plugin /** * Checks if IMAP server supports any of METADATA, ANNOTATEMORE, ANNOTATEMORE2 * - * @return boolean + * @return boolean */ function metadata_support() { @@ -376,9 +335,9 @@ class kolab_folders extends rcube_plugin 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]); } /** @@ -393,112 +352,7 @@ class kolab_folders extends rcube_plugin { $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)); } /** @@ -511,7 +365,7 @@ class kolab_folders extends rcube_plugin 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; @@ -521,7 +375,8 @@ class kolab_folders extends rcube_plugin $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) { @@ -562,24 +417,24 @@ class kolab_folders extends rcube_plugin 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]; @@ -621,7 +476,7 @@ class kolab_folders extends rcube_plugin } // 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 @@ -653,16 +508,10 @@ class kolab_folders extends rcube_plugin $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 @@ -4,7 +4,7 @@ http://pear.php.net/dtd/package-2.0 http://pear.php.net/dtd/package-2.0.xsd"> kolab_folders - http://git.kolab.org/roundcube-plugins-kolab/ + http://git.kolab.org/roundcubemail-plugins-kolab/ Type-aware folder management/listing for Kolab The plugin extends folders handling with features of the Kolab Suite @@ -21,10 +21,10 @@ machniak@kolabsys.com yes
- 2011-11-01 + 2012-05-14 - 1.0 - 1.0 + 2.0 + 2.0 stable 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 @@ -26,7 +26,7 @@ class kolab_zpush extends rcube_plugin { public $task = 'settings'; public $urlbase; - + private $rc; private $ui; private $cache; @@ -34,7 +34,7 @@ class kolab_zpush extends rcube_plugin 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'; @@ -45,17 +45,18 @@ class kolab_zpush extends rcube_plugin 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'); + } } @@ -66,6 +67,8 @@ class kolab_zpush extends rcube_plugin { $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(); @@ -120,7 +123,7 @@ class kolab_zpush extends rcube_plugin $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'] || @@ -146,12 +149,12 @@ class kolab_zpush extends rcube_plugin // 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]) @@ -161,25 +164,24 @@ class kolab_zpush extends rcube_plugin $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]); @@ -237,20 +239,20 @@ class kolab_zpush extends rcube_plugin 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'); } @@ -264,9 +266,10 @@ class kolab_zpush extends rcube_plugin public function list_devices() { if (!isset($this->devices)) { + $this->init_imap(); $this->devices = (array)$this->root_meta['DEVICE']; } - + return $this->devices; } @@ -299,7 +302,7 @@ class kolab_zpush extends rcube_plugin } $this->folders_meta[$folder]['TYPE'] = !empty($foldertype[0]) ? $foldertype[0] : 'mail'; } - + // cache it! $this->cache->write('folders', $this->folders_meta); } @@ -317,7 +320,7 @@ class kolab_zpush extends rcube_plugin { if (!isset($this->folders_meta)) $this->list_folders(); - + return $this->folders_meta; } 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 @@ -67,18 +67,18 @@ class kolab_zpush_ui $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']); @@ -90,7 +90,7 @@ class kolab_zpush_ui { 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(); @@ -99,7 +99,7 @@ class kolab_zpush_ui 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)) @@ -111,14 +111,14 @@ class kolab_zpush_ui } $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'))) : ''); @@ -129,7 +129,7 @@ class kolab_zpush_ui $names = array(); foreach ($a_folders as $folder) { - $foldername = $origname = preg_replace('/^INBOX »\s+/', '', rcube_kolab::object_name($folder)); + $foldername = $origname = preg_replace('/^INBOX »\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--) { @@ -161,7 +161,7 @@ class kolab_zpush_ui $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))); } 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 @@ -4,7 +4,7 @@ http://pear.php.net/dtd/package-2.0 http://pear.php.net/dtd/package-2.0.xsd"> kolab_zpush - http://git.kolab.org/roundcube-plugins-kolab/ + http://git.kolab.org/roundcubemail-plugins-kolab/ Z-Push configuration utility for Kolab accounts @@ -13,10 +13,10 @@ bruederli@kolabsys.com yes - 2011-11-14 - + 2012-05-14 - 0.3 + 1.0 + 1.0 stable @@ -29,6 +29,7 @@ + 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/.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:///freebusy +$rcmail_config['kolab_freebusy_server'] = 'https:///'; + +// 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 @@ + + * + * Copyright (C) 2012, 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 . + */ + +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 @@ + + * + * Copyright (C) 2012, 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 . + */ + +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 @@ + + * + * Copyright (C) 2012, 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 . + */ + +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 @@ + + * + * Copyright (C) 2012, 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 . + */ + +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 @@ + + * + * Copyright (C) 2012, 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 . + */ + +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, ' » ', $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].' » ') === 0) { + $length = strlen($names[$i].' » '); + $prefix = substr($name, 0, $length); + $count = count(explode(' » ', $prefix)); + $name = str_repeat('  ', $count-1) . '» ' . 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 @@ + + * + * Copyright (C) 2012, 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 . + */ + +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('', '', '') + * @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 @@ + + * + * Copyright (C) 2012, 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 . + */ +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('', '', '') + * @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('!(.+)!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 @@ + + * + * Copyright (C) 2012, 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 . + */ + +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 @@ -26,7 +26,7 @@ */ class odfviewer extends rcube_plugin { - public $task = 'mail|logout'; + public $task = 'mail|calendar|logout'; private $tempdir = 'plugins/odfviewer/files/'; private $tempbase = 'plugins/odfviewer/files/'; @@ -56,10 +56,9 @@ class odfviewer extends rcube_plugin $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); @@ -75,20 +74,24 @@ class odfviewer extends rcube_plugin */ 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; } 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 @@ -34,161 +34,168 @@ 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;b128)+(j>2048);d=new e.ByteArray(g);for(f=g=0;f>>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=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;ad?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;ch?(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;fo.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>>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>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>>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;fa.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>>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>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;bf?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;bf?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;dc?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.lengthc;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>>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].fc32506? -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]&&fd){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<>=7;g<30;g++){da[g]=e<<7;for(c=0;c<1<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&&Gw&&(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&&eh.len)g=h.len;for(q=0;qj-p&&(g=j-p);for(q=0;q>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=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>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>7)])&255,W(g,b),j=ba[g],j!=0&&(c-=da[g],P(c,j))), -e>>=1;while(f16-c?(r|=a<>16-y,y+=c-16):(r|=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>>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<>16-z,z+=c-16):(r|=a<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=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]&&bf){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(2f;f++)b+=K[f].fc*(5+ga[f]); +b>>=3;if(ia>=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;fV;)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(0b||(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=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(){8b?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=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;db.len&&(j=b.len);for(q=0;qh-i&&(j=h-i);for(q=0;qg;g++){$[g]=e;for(f=0;f<1<g;g++){W[g]=e;for(f=0;f<1<>=7;30>g;g++){W[g]=e<<7;for(f=0;f<1<=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=l-u&&(w=ya(u),w>B&& +(w=B),3===w&&4096B&&!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:9j;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>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;o256?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;jo&&(j=o);for(k=1<0;)y[n++]=r+=x[m++];x=a;o=m=0;do if((r=x[m++])!=0)t[y[r]++]=o;while(++o0;){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;++rh&&v0)y[t]=o,p.b=w[t],p.e=16+r,p.t=n,r=(o&(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]>v;r>=1)o^=r;for(o^=r;(o&(1<>=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&&g286||q>30)return-1;for(d=0;dh)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;f257?-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;E0){if(c!=0)for(;f>0&&E0&&E0&&F1){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;gg&&(e=g);for(B=1<(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(++gp+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;++sl&&p>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]n[y]?16:15,t.n= +n[y++]):(t.e=d[n[y]-b],t.n=f[n[y++]-b]);h=1<>p;s>=1)g^=s;for(g^=s;(g&(1<>=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;16d;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;fd)p[f++]=e=d;else if(16==d){l(2);d=3+v(2);y(2);if(f+d>h)return-1;for(;0h)return-1;for(;0D;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(1f&&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;bf&&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;c1/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;h0)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>>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>>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;en?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"}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+=""),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"}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+=""),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&&b1&&(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&&s0)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=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=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=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();h1;){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;m1?(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=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;g0: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;a1;)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 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(00&&(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;fc)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'}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+="";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+="";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+=""; -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+="";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;bc)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''}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+""}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+""}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+""}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+""}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;g0?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=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;c0?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-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=ea?-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;dc}}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;hd}}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: '';}\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: '';}\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(",")})();