diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php index d5efb219..513087ea 100644 --- a/plugins/calendar/drivers/kolab/kolab_calendar.php +++ b/plugins/calendar/drivers/kolab/kolab_calendar.php @@ -1,517 +1,529 @@ * @author Aleksander Machniak * * 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_calendar { public $id; public $ready = false; public $readonly = true; public $attachments = true; public $alarms = false; public $categories = array(); public $storage; private $cal; private $events = array(); private $imap_folder = 'INBOX/Calendar'; private $search_fields = array('title', 'description', 'location', '_attendees'); private $sensitivity_map = array('public', 'private', 'confidential'); /** * Default constructor */ public function __construct($imap_folder, $calendar) { $this->cal = $calendar; if (strlen($imap_folder)) $this->imap_folder = $imap_folder; // ID is derrived from folder name $this->id = kolab_storage::folder_id($this->imap_folder); // fetch objects from the given IMAP folder $this->storage = kolab_storage::get_folder($this->imap_folder); $this->ready = $this->storage && !PEAR::isError($this->storage); // Set readonly and alarms flags according to folder permissions if ($this->ready) { if ($this->storage->get_namespace() == 'personal') { $this->readonly = false; $this->alarms = true; } else { $rights = $this->storage->get_myrights(); if ($rights && !PEAR::isError($rights)) { if (strpos($rights, 'i') !== false) $this->readonly = false; } } // user-specific alarms settings win $prefs = $this->cal->rc->config->get('kolab_calendars', array()); if (isset($prefs[$this->id]['showalarms'])) $this->alarms = $prefs[$this->id]['showalarms']; } } /** * Getter for a nice and human readable name for this calendar * See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference * * @return string Name of this calendar */ public function get_name() { $folder = kolab_storage::object_name($this->imap_folder, $this->namespace); return $folder; } /** * Getter for the IMAP folder name * * @return string Name of the IMAP folder */ public function get_realname() { return $this->imap_folder; } /** * Getter for the IMAP folder owner * * @return string Name of the folder owner */ public function get_owner() { return $this->storage->get_owner(); } /** * Getter for the name of the namespace to which the IMAP folder belongs * * @return string Name of the namespace (personal, other, shared) */ public function get_namespace() { return $this->storage->get_namespace(); } /** * Getter for the top-end calendar folder name (not the entire path) * * @return string Name of this calendar */ public function get_foldername() { $parts = explode('/', $this->imap_folder); return rcube_charset::convert(end($parts), 'UTF7-IMAP'); } /** * Return color to display this calendar */ public function get_color() { // color is defined in folder METADATA $metadata = $this->storage->get_metadata(array(kolab_storage::COLOR_KEY_PRIVATE, kolab_storage::COLOR_KEY_SHARED)); if (($color = $metadata[kolab_storage::COLOR_KEY_PRIVATE]) || ($color = $metadata[kolab_storage::COLOR_KEY_SHARED])) { return $color; } // calendar color is stored in user prefs (temporary solution) $prefs = $this->cal->rc->config->get('kolab_calendars', array()); if (!empty($prefs[$this->id]) && !empty($prefs[$this->id]['color'])) return $prefs[$this->id]['color']; return 'cc0000'; } /** * Return the corresponding kolab_storage_folder instance */ public function get_folder() { return $this->storage; } /** * Getter for a single event object */ public function get_event($id) { // 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 ($record = $this->storage->get_object($master_id)) $this->events[$master_id] = $this->_to_rcube_event($record); if (($master = $this->events[$master_id]) && $master['recurrence']) { $limit = clone $master['start']; $limit->add(new DateInterval('P10Y')); $this->_get_recurring_events($record, $master['start'], $limit, $id); } } return $this->events[$id]; } /** * @param integer Event's new start (unix timestamp) * @param integer Event's new end (unix timestamp) * @param string Search query (optional) * @param boolean 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, $query = array()) { // convert to DateTime for comparisons $start = new DateTime('@'.$start); $end = new DateTime('@'.$end); // query Kolab storage $query[] = array('dtstart', '<=', $end); $query[] = array('dtend', '>=', $start); if (!empty($search)) { $search = mb_strtolower($search); foreach (rcube_utils::normalize_string($search, true) as $word) { $query[] = array('words', 'LIKE', $word); } } $events = array(); foreach ((array)$this->storage->select($query) as $record) { $event = $this->_to_rcube_event($record); $this->events[$event['id']] = $event; // remember seen categories if ($event['categories']) $this->categories[$event['categories']]++; // filter events by search query if (!empty($search)) { $hit = false; foreach ($this->search_fields as $col) { $sval = is_array($col) ? $event[$col[0]][$col[1]] : $event[$col]; if (empty($sval)) continue; // do a simple substring matching (to be improved) $val = mb_strtolower($sval); if (strpos($val, $search) !== false) { $hit = true; break; } } if (!$hit) // skip this event if not match with search term continue; } // list events in requested time window if ($event['start'] <= $end && $event['end'] >= $start) { unset($event['_attendees']); $events[] = $event; } // resolve recurring events if ($record['recurrence'] && $virtual == 1) { $events = array_merge($events, $this->_get_recurring_events($record, $start, $end)); } } return $events; } /** * Create a new event record * * @see calendar_driver::new_event() * * @return mixed The created record ID on success, False on error */ public function insert_event($event) { if (!is_array($event)) return false; //generate new event from RC input $object = $this->_from_rcube_event($event); $saved = $this->storage->save($object, 'event'); if (!$saved) { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Error saving event object to Kolab server"), true, false); $saved = false; } else { $event['id'] = $event['uid']; $this->events[$event['uid']] = $this->_to_rcube_event($object); } return $saved; } /** * Update a specific event record * * @see calendar_driver::new_event() * @return boolean True on success, False on error */ public function update_event($event) { $updated = false; $old = $this->storage->get_object($event['id']); if (!$old || PEAR::isError($old)) return false; $old['recurrence'] = ''; # clear old field, could have been removed in new, too $object = $this->_from_rcube_event($event, $old); $saved = $this->storage->save($object, 'event', $event['id']); if (!$saved) { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Error saving event object to Kolab server"), true, false); } else { $updated = true; $this->events[$event['id']] = $this->_to_rcube_event($object); } return $updated; } /** * Delete an event record * * @see calendar_driver::remove_event() * @return boolean True on success, False on error */ public function delete_event($event, $force = true) { $deleted = $this->storage->delete($event['id'], $force); if (!$deleted) { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Error deleting event object from Kolab server"), true, false); } return $deleted; } /** * Restore deleted event record * * @see calendar_driver::undelete_event() * @return boolean True on success, False on error */ public function restore_event($event) { if ($this->storage->undelete($event['id'])) { return true; } else { rcube::raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Error undeleting the event object $uid from the Kolab server"), true, false); } return false; } /** * Create instances of a recurring event */ public function _get_recurring_events($event, $start, $end, $event_id = null) { - $recurrence = new kolab_date_recurrence($event); + $object = $event['_formatobj']; + if (!$object) { + $rec = $this->storage->get_object($event['id']); + $object = $rec['_formatobj']; + } + if (!is_object($object)) + return array(); + + $recurrence = new kolab_date_recurrence($object); $i = 0; $events = array(); while ($next_event = $recurrence->next_instance()) { $rec_start = $next_event['start']->format('U'); $rec_end = $next_event['end']->format('U'); $rec_id = $event['uid'] . '-' . ++$i; // add to output if in range if (($next_event['start'] <= $end && $next_event['end'] >= $start) || ($event_id && $rec_id == $event_id)) { $rec_event = $this->_to_rcube_event($next_event); $rec_event['id'] = $rec_id; $rec_event['recurrence_id'] = $event['uid']; $rec_event['_instance'] = $i; unset($rec_event['_attendees']); $events[] = $rec_event; if ($rec_id == $event_id) { $this->events[$rec_id] = $rec_event; break; } } else if ($next_event['start'] > $end) // stop loop if out of range break; + + // avoid endless recursion loops + if ($i > 1000) + break; } return $events; } /** * Convert from Kolab_Format to internal representation */ private function _to_rcube_event($record) { $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'] = clone $record['start']; $record['end']->add(new DateInterval('PT1H')); } if (!empty($record['_attachments'])) { foreach ($record['_attachments'] as $key => $attachment) { if ($attachment !== false) { if (!$attachment['name']) $attachment['name'] = $key; unset($attachment['path'], $attachment['content']); $attachments[] = $attachment; } } $record['attachments'] = $attachments; } $sensitivity_map = array_flip($this->sensitivity_map); $record['sensitivity'] = intval($sensitivity_map[$record['sensitivity']]); // Roundcube only supports one category assignment 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, $old = array()) { $object = &$event; // in kolab_storage attachments are indexed by content-id $object['_attachments'] = array(); if (is_array($event['attachments'])) { foreach ($event['attachments'] as $idx => $attachment) { $key = null; // Roundcube ID has nothing to do with the storage ID, remove it if ($attachment['content']) { unset($attachment['id']); } else { foreach ((array)$old['_attachments'] as $cid => $oldatt) { if ($attachment['id'] == $oldatt['id']) $key = $cid; } } // flagged for deletion => set to false if ($attachment['_deleted']) { $object['_attachments'][$key] = false; } // replace existing entry else if ($key) { $object['_attachments'][$key] = $attachment; } // append as new attachment else { $object['_attachments'][] = $attachment; } } unset($event['attachments']); } // 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 $event; } } diff --git a/plugins/kolab_config/kolab_config.php b/plugins/kolab_config/kolab_config.php index 4fe8a7f3..23188cf6 100644 --- a/plugins/kolab_config/kolab_config.php +++ b/plugins/kolab_config/kolab_config.php @@ -1,188 +1,188 @@ * @author Thomas Bruederli * * Copyright (C) 2011-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_config extends rcube_plugin { public $task = 'utils'; private $enabled; private $default; private $folders; private $dicts = array(); /** * Required startup method of a Roundcube plugin */ public function init() { $rcmail = rcube::get_instance(); // Register spellchecker dictionary handlers if (strtolower($rcmail->config->get('spellcheck_dictionary')) != 'shared') { $this->add_hook('spell_dictionary_save', array($this, 'dictionary_save')); $this->add_hook('spell_dictionary_get', array($this, 'dictionary_get')); } /* // Register addressbook saved searches handlers $this->add_hook('saved_search_create', array($this, 'saved_search_create')); $this->add_hook('saved_search_delete', array($this, 'saved_search_delete')); $this->add_hook('saved_search_list', array($this, 'saved_search_list')); $this->add_hook('saved_search_get', array($this, 'saved_search_get')); */ } /** * Initializes config object and dependencies */ private function load() { // nothing to be done here if (isset($this->folders)) return; $this->require_plugin('libkolab'); $this->folders = kolab_storage::get_folders('configuration'); foreach ($this->folders as $i => $folder) { if ($folder->default) { $this->default = $folder; break; } } // if no folder is set as default, choose the first one if (!$this->default) - $this->default = $this->folders[0]; + $this->default = reset($this->folders); // check if configuration folder exist if ($this->default && $this->default->name) { $this->enabled = true; } } /** * Saves spellcheck dictionary. * * @param array $args Hook arguments * * @return array Hook arguments */ public function dictionary_save($args) { $this->load(); if (!$this->enabled) { return $args; } $lang = $args['language']; $dict = $this->read_dictionary($lang, true); $dict['type'] = 'dictionary'; $dict['language'] = $args['language']; $dict['e'] = $args['dictionary']; if (empty($dict['e'])) { // Delete the object $this->default->delete($dict); } else { // Update the object $this->default->save($dict, 'configuration.dictionary', $dict['uid']); } $args['abort'] = true; return $args; } /** * Returns spellcheck dictionary. * * @param array $args Hook arguments * * @return array Hook arguments */ public function dictionary_get($args) { $this->load(); if (!$this->enabled) { return $args; } $lang = $args['language']; $dict = $this->read_dictionary($lang); if (!empty($dict)) { $args['dictionary'] = (array)$dict['e']; } $args['abort'] = true; return $args; } /** * Load dictionary config objects from Kolab storage * * @param string The language (2 chars) to load * @param boolean Only load objects from default folder * @return array Dictionary object as hash array */ private function read_dictionary($lang, $default = false) { if (isset($this->dicts[$lang])) return $this->dicts[$lang]; $query = array(array('type','=','configuration.dictionary'), array('tags','=',$lang)); foreach ($this->folders as $folder) { // we only want to read from default folder if ($default && !$folder->default) continue; foreach ((array)$folder->select($query) as $object) { if ($object['type'] == 'dictionary' && ($object['language'] == $lang || $object['language'] == 'XX')) { if (is_array($this->dicts[$lang])) $this->dicts[$lang]['e'] = array_merge((array)$this->dicts[$lang]['e'], $object['e']); else $this->dicts[$lang] = $object; // make sure the default object is cached if ($folder->default && $object['language'] != 'XX') { $object['e'] = $this->dicts[$lang]['e']; $this->dicts[$lang] = $object; } } } } return $this->dicts[$lang]; } } diff --git a/plugins/libkolab/config.inc.php.dist b/plugins/libkolab/config.inc.php.dist index fedf7936..cb446523 100644 --- a/plugins/libkolab/config.inc.php.dist +++ b/plugins/libkolab/config.inc.php.dist @@ -1,9 +1,10 @@ diff --git a/plugins/libkolab/lib/kolab_date_recurrence.php b/plugins/libkolab/lib/kolab_date_recurrence.php index 427f62ac..3aaa399f 100644 --- a/plugins/libkolab/lib/kolab_date_recurrence.php +++ b/plugins/libkolab/lib/kolab_date_recurrence.php @@ -1,171 +1,116 @@ * * 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_date_recurrence { - private $engine; - private $object; - private $next; - private $duration; - private $tz_offset = 0; - private $dst_start = 0; - private $allday = false; - private $hour = 0; + private /* EventCal */ $engine; + private /* kolab_format_xcal */ $object; + private /* DateTime */ $start; + private /* DateTime */ $next; + private /* cDateTime */ $cnext; + private /* DateInterval */ $duration; /** * Default constructor * * @param array The Kolab object to operate on */ function __construct($object) { + $data = $object->to_array(); + $this->object = $object; - $this->next = new Horde_Date($object['start'], kolab_format::$timezone->getName()); + $this->engine = $object->to_libcal(); + $this->start = $this->next = $data['start']; + $this->cnext = kolab_format::get_datetime($this->next); - if (is_object($object['start']) && is_object($object['end'])) - $this->duration = $object['start']->diff($object['end']); + if (is_object($data['start']) && is_object($data['end'])) + $this->duration = $data['start']->diff($data['end']); else - $this->duration = new DateInterval('PT' . ($object['end'] - $object['start']) . 'S'); - - // use (copied) Horde classes to compute recurring instances - // TODO: replace with something that has less than 6'000 lines of code - $this->engine = new Horde_Date_Recurrence($this->next); - $this->engine->fromRRule20($this->to_rrule($object['recurrence'])); // TODO: get that string directly from libkolabxml - - foreach ((array)$object['recurrence']['EXDATE'] as $exdate) - $this->engine->addException($exdate->format('Y'), $exdate->format('n'), $exdate->format('j')); - - $now = new DateTime('now', kolab_format::$timezone); - $this->tz_offset = $object['allday'] ? $now->getOffset() - date('Z') : 0; - $this->dst_start = $this->next->format('I'); - $this->allday = $object['allday']; - $this->hour = $this->next->hour; + $this->duration = new DateInterval('PT' . ($data['end'] - $data['start']) . 'S'); } /** * Get date/time of the next occurence of this event * * @param boolean Return a Unix timestamp instead of a DateTime object * @return mixed DateTime object/unix timestamp or False if recurrence ended */ public function next_start($timestamp = false) { $time = false; - if ($this->next && ($next = $this->engine->nextActiveRecurrence(array('year' => $this->next->year, 'month' => $this->next->month, 'mday' => $this->next->mday + 1, 'hour' => $this->next->hour, 'min' => $this->next->min, 'sec' => $this->next->sec)))) { - if ($this->allday) { - $next->hour = $this->hour; # fix time for all-day events - $next->min = 0; - } - if ($timestamp) { - # consider difference in daylight saving between base event and recurring instance - $dst_diff = ($this->dst_start - $next->format('I')) * 3600; - $time = $next->timestamp() - $this->tz_offset - $dst_diff; - } - else { - $time = $next->toDateTime(); + + if ($this->engine && $this->next) { + if (($cnext = new cDateTime($this->engine->getNextOccurence($this->cnext))) && $cnext->isValid()) { + $next = kolab_format::php_datetime($cnext); + $time = $timestamp ? $next->format('U') : $next; + $this->cnext = $cnext; + $this->next = $next; } - $this->next = $next; } return $time; } /** * Get the next recurring instance of this event * * @return mixed Array with event properties or False if recurrence ended */ public function next_instance() { if ($next_start = $this->next_start()) { $next_end = clone $next_start; $next_end->add($this->duration); - $next = $this->object; + $next = $this->object->to_array(); $next['recurrence_id'] = $next_start->format('Y-m-d'); $next['start'] = $next_start; $next['end'] = $next_end; unset($next['_formatobj']); return $next; } return false; } /** * Get the end date of the occurence of this recurrence cycle * * @param string Date limit (where infinite recurrences should abort) * @return mixed Timestamp with end date of the last event or False if recurrence exceeds limit */ public function end($limit = 'now +1 year') { - if ($this->object['recurrence']['UNTIL']) - return $this->object['recurrence']['UNTIL']->format('U'); - - $limit_time = strtotime($limit); - while ($next_start = $this->next_start(true)) { - if ($next_start > $limit_time) - break; - } - - if ($this->next) { - $next_end = $this->next->toDateTime(); - $next_end->add($this->duration); - return $next_end->format('U'); + $limit_dt = new DateTime($limit); + if ($this->engine && ($cend = $this->engine->getLastOccurrence()) && ($end_dt = kolab_format::php_datetime(new cDateTime($cend))) && $end_dt < $limit_dt) { + return $end_dt->format('U'); } return false; } - - /** - * Convert the internal structured data into a vcalendar RRULE 2.0 string - */ - private function to_rrule($recurrence) - { - if (is_string($recurrence)) - return $recurrence; - - $rrule = ''; - foreach ((array)$recurrence as $k => $val) { - $k = strtoupper($k); - switch ($k) { - case 'UNTIL': - $val = $val->format('Ymd\THis'); - break; - case 'EXDATE': - foreach ((array)$val as $i => $ex) - $val[$i] = $ex->format('Ymd\THis'); - $val = join(',', (array)$val); - break; - } - $rrule .= $k . '=' . $val . ';'; - } - - return $rrule; - } - } diff --git a/plugins/libkolab/lib/kolab_format.php b/plugins/libkolab/lib/kolab_format.php index 23246d32..a4147812 100644 --- a/plugins/libkolab/lib/kolab_format.php +++ b/plugins/libkolab/lib/kolab_format.php @@ -1,330 +1,419 @@ * * 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; public /*abstract*/ $CTYPE; + public /*abstract*/ $CTYPEv2; + protected /*abstract*/ $objclass; protected /*abstract*/ $read_func; protected /*abstract*/ $write_func; protected $obj; protected $data; protected $xmldata; + protected $xmlobject; protected $loaded = false; + protected $version = 3.0; - const VERSION = '3.0'; const KTYPE_PREFIX = 'application/x-vnd.kolab.'; + const PRODUCT_ID = 'Roundcube-libkolab-0.9'; /** - * Factory method to instantiate a kolab_format object of the given type + * Factory method to instantiate a kolab_format object of the given type and version * * @param string Object type to instantiate + * @param float Format version * @param string Cached xml data to initialize with * @return object kolab_format */ - public static function factory($type, $xmldata = null) + public static function factory($type, $version = 3.0, $xmldata = null) { if (!isset(self::$timezone)) self::$timezone = new DateTimeZone('UTC'); + if (!self::supports($version)) + return PEAR::raiseError("No support for Kolab format version " . $version); + $type = preg_replace('/configuration\.[a-z.]+$/', 'configuration', $type); $suffix = preg_replace('/[^a-z]+/', '', $type); $classname = 'kolab_format_' . $suffix; if (class_exists($classname)) - return new $classname($xmldata); + return new $classname($xmldata, $version); return PEAR::raiseError("Failed to load Kolab Format wrapper for type " . $type); } + /** + * Determine support for the given format version + * + * @param float Format version to check + * @return boolean True if supported, False otherwise + */ + public static function supports($version) + { + if ($version == 2.0) + return class_exists('kolabobject'); + // default is version 3 + return class_exists('kolabformat'); + } + /** * 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 Null, local time if False * @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) { // use timezone information from datetime of global setting if (!$tz && $tz !== false) { if ($datetime instanceof DateTime) $tz = $datetime->getTimezone(); 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 ?: null); if ($datetime instanceof 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 !== false) $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; } /** * Parse the X-Kolab-Type header from MIME messages and return the object type in short form * * @param string X-Kolab-Type header value * @return string Kolab object type (contact,event,task,note,etc.) */ public static function mime2object_type($x_kolab_type) { return preg_replace('/dictionary.[a-z.]+$/', 'dictionary', substr($x_kolab_type, strlen(self::KTYPE_PREFIX))); } + + /** + * Default constructor of all kolab_format_* objects + */ + public function __construct($xmldata = null, $version = null) + { + $this->obj = new $this->objclass; + $this->xmldata = $xmldata; + + if ($version) + $this->version = $version; + + // use libkolab module if available + if (class_exists('kolabobject')) + $this->xmlobject = new XMLObject(); + } + /** * 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) { rcube::raise_error(array( 'code' => 660, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, - 'message' => "kolabformat write $log: " . kolabformat::errorMessage(), + 'message' => "kolabformat $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->data['uid'] = $this->xmlobject ? $this->xmlobject->getSerializedUID() : 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; } } + /** + * Get constant value for libkolab's version parameter + * + * @param float Version value to convert + * @return int Constant value of either kolabobject::KolabV2 or kolabobject::KolabV3 or false if kolabobject module isn't available + */ + protected function libversion($v = null) + { + if (class_exists('kolabobject')) { + $version = $v ?: $this->version; + if ($version <= 2.0) + return kolabobject::KolabV2; + else + return kolabobject::KolabV3; + } + + return false; + } + + /** + * Determine the correct libkolab(xml) wrapper function for the given call + * depending on the available PHP modules + */ + protected function libfunc($func) + { + if (is_array($func) || strpos($func, '::')) + return $func; + else if (class_exists('kolabobject')) + return array($this->xmlobject, $func); + else + return 'kolabformat::' . $func; + } + /** * 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 + * @return boolean True on success, False on failure */ public function load($xml) { - $this->obj = call_user_func($this->read_func, $xml, false); + $read_func = $this->libfunc($this->read_func); + + if (is_array($read_func)) + $r = call_user_func($read_func, $xml, $this->libversion()); + else + $r = call_user_func($read_func, $xml, false); + + if (is_resource($r)) + $this->obj = new $this->objclass($r); + else if (is_a($r, $this->objclass)) + $this->obj = $r; + $this->loaded = !$this->format_errors(); } /** * Write object data to XML format * + * @param float Format version to write * @return string XML data */ - public function write() + public function write($version = null) { $this->init(); - $this->xmldata = call_user_func($this->write_func, $this->obj); + $write_func = $this->libfunc($this->write_func); + if (is_array($write_func)) + $this->xmldata = call_user_func($write_func, $this->obj, $this->libversion($version), self::PRODUCT_ID); + else + $this->xmldata = call_user_func($write_func, $this->obj, self::PRODUCT_ID); if (!$this->format_errors()) $this->update_uid(); else $this->xmldata = null; return $this->xmldata; } /** * Set properties to the kolabformat object * * @param array Object data as hash array */ abstract public function set(&$object); /** * */ abstract public function is_valid(); /** * 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_configuration.php b/plugins/libkolab/lib/kolab_format_configuration.php index 974fc453..918928b2 100644 --- a/plugins/libkolab/lib/kolab_format_configuration.php +++ b/plugins/libkolab/lib/kolab_format_configuration.php @@ -1,163 +1,159 @@ * * 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_configuration extends kolab_format { public $CTYPE = 'application/x-vnd.kolab.configuration'; + public $CTYPEv2 = 'application/x-vnd.kolab.configuration'; - protected $read_func = 'kolabformat::readConfiguration'; - protected $write_func = 'kolabformat::writeConfiguration'; + protected $objclass = 'Configuration'; + protected $read_func = 'readConfiguration'; + protected $write_func = 'writeConfiguration'; private $type_map = array( 'dictionary' => Configuration::TypeDictionary, 'category' => Configuration::TypeCategoryColor, ); - function __construct($xmldata = null) - { - $this->obj = new Configuration; - $this->xmldata = $xmldata; - } - /** * Set properties to the kolabformat object * * @param array Object data as hash array */ public function set(&$object) { $this->init(); // read type-specific properties switch ($object['type']) { case 'dictionary': $dict = new Dictionary($object['language']); $dict->setEntries(self::array2vector($object['e'])); $this->obj = new Configuration($dict); break; case 'category': // TODO: implement this $categories = new vectorcategorycolor; $this->obj = new Configuration($categories); break; default: return false; } // set some automatic values if missing if (!empty($object['uid'])) $this->obj->setUid($object['uid']); if (!empty($object['created'])) $this->obj->setCreated(self::get_datetime($object['created'])); // adjust content-type string - $this->CTYPE = 'application/x-vnd.kolab.configuration.' . $object['type']; + $this->CTYPE = $this->CTYPEv2 = 'application/x-vnd.kolab.configuration.' . $object['type']; // cache this data $this->data = $object; unset($this->data['_formatobj']); } /** * */ public function is_valid() { return $this->data || (is_object($this->obj) && $this->obj->isValid()); } /** * Convert the Configuration object into a hash array data structure * * @return array Config object data as hash array */ public function to_array() { // return cached result if (!empty($this->data)) return $this->data; $this->init(); $type_map = array_flip($this->type_map); // read object properties $object = array( 'uid' => $this->obj->uid(), 'created' => self::php_datetime($this->obj->created()), 'changed' => self::php_datetime($this->obj->lastModified()), 'type' => $type_map[$this->obj->type()], ); // read type-specific properties switch ($object['type']) { case 'dictionary': $dict = $this->obj->dictionary(); $object['language'] = $dict->language(); $object['e'] = self::vector2array($dict->entries()); break; case 'category': // TODO: implement this break; } // adjust content-type string if ($object['type']) - $this->CTYPE = 'application/x-vnd.kolab.configuration.' . $object['type']; + $this->CTYPE = $this->CTYPEv2 = 'application/x-vnd.kolab.configuration.' . $object['type']; $this->data = $object; return $this->data; } /** * Load data from old Kolab2 format */ public function fromkolab2($record) { $object = array( 'uid' => $record['uid'], 'changed' => $record['last-modification-date'], ); $this->data = $object + $record; } /** * 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(); if ($this->data['type'] == 'dictionary') $tags = array($this->data['language']); return $tags; } } diff --git a/plugins/libkolab/lib/kolab_format_contact.php b/plugins/libkolab/lib/kolab_format_contact.php index ffef059d..b147c384 100644 --- a/plugins/libkolab/lib/kolab_format_contact.php +++ b/plugins/libkolab/lib/kolab_format_contact.php @@ -1,509 +1,512 @@ * * 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 $CTYPEv2 = 'application/x-vnd.kolab.contact'; - protected $read_func = 'kolabformat::readContact'; - protected $write_func = 'kolabformat::writeContact'; + protected $objclass = 'Contact'; + protected $read_func = 'readContact'; + protected $write_func = 'writeContact'; 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) + function __construct($xmldata = null, $version = 3.0) { - $this->obj = new Contact; - $this->xmldata = $xmldata; + parent::__construct($xmldata, $version); // complete phone types $this->phonetypes['homefax'] |= Telephone::Home; $this->phonetypes['workfax'] |= Telephone::Work; } /** * 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']); $object['changed'] = new DateTime('now', self::$timezone); $this->obj->setLastModified(self::get_datetime($object['changed'], new DateTimeZone('UTC'))); // 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::NoType; $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'], false, true)); if (isset($object['anniversary'])) $this->obj->setAnniversary(self::get_datetime($object['anniversary'], false, true)); if (!empty($object['photo'])) { if ($type = rcube_mime::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(), 'name' => $this->obj->name(), 'changed' => self::php_datetime($this->obj->lastModified()), ); $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(); + else if ($this->xmlobject && ($photo_name = $this->xmlobject->pictureAttachmentName())) + $object['photo'] = $photo_name; // 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 index fcb94c14..ba54742b 100644 --- a/plugins/libkolab/lib/kolab_format_distributionlist.php +++ b/plugins/libkolab/lib/kolab_format_distributionlist.php @@ -1,147 +1,143 @@ * * 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'; + public $CTYPEv2 = 'application/x-vnd.kolab.distribution-list'; - protected $read_func = 'kolabformat::readDistlist'; - protected $write_func = 'kolabformat::writeDistlist'; + protected $objclass = 'DistList'; + protected $read_func = 'readDistlist'; + protected $write_func = 'writeDistlist'; - function __construct($xmldata = null) - { - $this->obj = new DistList; - $this->xmldata = $xmldata; - } - /** * Set properties to the kolabformat object * * @param array Object data as hash array */ public function set(&$object) { $this->init(); // set some automatic values if missing if (!empty($object['uid'])) $this->obj->setUid($object['uid']); $object['changed'] = new DateTime('now', self::$timezone); $this->obj->setLastModified(self::get_datetime($object['changed'], new DateTimeZone('UTC'))); $this->obj->setName($object['name']); $seen = array(); $members = new vectorcontactref; foreach ((array)$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); // set type property for proper caching $object['_type'] = 'distribution-list'; // 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 ((array)$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' => self::php_datetime($this->obj->lastModified()), 'name' => $this->obj->name(), 'member' => array(), '_type' => 'distribution-list', ); $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 index 90bfea3d..d1e6b2e3 100644 --- a/plugins/libkolab/lib/kolab_format_event.php +++ b/plugins/libkolab/lib/kolab_format_event.php @@ -1,303 +1,307 @@ * * 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_xcal { - protected $read_func = 'kolabformat::readEvent'; - protected $write_func = 'kolabformat::writeEvent'; + public $CTYPEv2 = 'application/x-vnd.kolab.event'; + + protected $objclass = 'Event'; + protected $read_func = 'readEvent'; + protected $write_func = 'writeEvent'; 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 + * Clones into an instance of libcalendaring's extended EventCal class + * + * @return mixed EventCal object or false on failure */ - function __construct($xmldata = null) + public function to_libcal() { - $this->obj = new Event; - $this->xmldata = $xmldata; + return class_exists('kolabcalendaring') ? new EventCal($this->obj) : false; } /** * Set event properties to the kolabformat object * * @param array Event data as hash array */ public function set(&$object) { $this->init(); // set common xcal properties parent::set($object); // 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->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); // save attachments $vattach = new vectorattachment; foreach ((array)$object['_attachments'] as $cid => $attr) { if (empty($attr)) continue; $attach = new Attachment; $attach->setLabel((string)$attr['name']); $attach->setUri('cid:' . $cid, $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 Event object into a hash array data structure * * @return array Event data as hash array */ public function to_array() { // return cached result if (!empty($this->data)) return $this->data; $this->init(); // read common xcal props $object = parent::to_array(); // read object properties $object += array( 'end' => self::php_datetime($this->obj->end()), 'allday' => $this->obj->start()->isDateOnly(), 'free_busy' => $this->obj->transparency() ? 'free' : 'busy', // TODO: transparency is only boolean 'attendees' => array(), ); // organizer is part of the attendees list in Roundcube if ($object['organizer']) { $object['organizer']['role'] = 'ORGANIZER'; array_unshift($object['attendees'], $object['organizer']); } // 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; // 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:') { + if (substr($attach->uri(), 0, 4) != 'cid:' && $attach->label()) { $name = $attach->label(); $data = $attach->data(); $object['_attachments'][$name] = array( 'name' => $name, '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; } /** * 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'] = date_create('@'.$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'][] = date_create($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' => new DateTime('@'.$rec['start-date']), 'end' => new DateTime('@'.$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'], ); // assign current timezone to event start/end $this->data['start']->setTimezone(self::$timezone); $this->data['end']->setTimezone(self::$timezone); } } diff --git a/plugins/libkolab/lib/kolab_format_journal.php b/plugins/libkolab/lib/kolab_format_journal.php index 5869af09..9144ea2d 100644 --- a/plugins/libkolab/lib/kolab_format_journal.php +++ b/plugins/libkolab/lib/kolab_format_journal.php @@ -1,112 +1,108 @@ * * 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_journal extends kolab_format { public $CTYPE = 'application/calendar+xml'; + public $CTYPEv2 = 'application/x-vnd.kolab.journal'; - protected $read_func = 'kolabformat::readJournal'; - protected $write_func = 'kolabformat::writeJournal'; + protected $objclass = 'Journal'; + protected $read_func = 'readJournal'; + protected $write_func = 'writeJournal'; - function __construct($xmldata = null) - { - $this->obj = new Journal; - $this->xmldata = $xmldata; - } - /** * Set properties to the kolabformat object * * @param array Object data as hash array */ public function set(&$object) { $this->init(); // set some automatic values if missing if (!empty($object['uid'])) $this->obj->setUid($object['uid']); $object['changed'] = new DateTime('now', self::$timezone); $this->obj->setLastModified(self::get_datetime($object['changed'], new DateTimeZone('UTC'))); // TODO: set object propeties // 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'], ); // TODO: implement this $this->data = $object; } /** * Convert the Configuration object into a hash array data structure * * @return array Config object 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(), 'created' => self::php_datetime($this->obj->created()), 'changed' => self::php_datetime($this->obj->lastModified()), ); // TODO: read object properties $this->data = $object; return $this->data; } } diff --git a/plugins/libkolab/lib/kolab_format_note.php b/plugins/libkolab/lib/kolab_format_note.php index 1c88a8bb..48e963e1 100644 --- a/plugins/libkolab/lib/kolab_format_note.php +++ b/plugins/libkolab/lib/kolab_format_note.php @@ -1,111 +1,107 @@ * * 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_note extends kolab_format { public $CTYPE = 'application/x-vnd.kolab.note'; + public $CTYPEv2 = 'application/x-vnd.kolab.note'; - protected $read_func = 'kolabformat::readNote'; - protected $write_func = 'kolabformat::writeNote'; + protected $objclass = 'Note'; + protected $read_func = 'readNote'; + protected $write_func = 'writeNote'; - function __construct($xmldata = null) - { - $this->obj = new Note; - $this->xmldata = $xmldata; - } - /** * Set properties to the kolabformat object * * @param array Object data as hash array */ public function set(&$object) { $this->init(); // set some automatic values if missing if (!empty($object['uid'])) $this->obj->setUid($object['uid']); $object['changed'] = new DateTime('now', self::$timezone); $this->obj->setLastModified(self::get_datetime($object['changed'], new DateTimeZone('UTC'))); // TODO: set object propeties // 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'], ); $this->data = $object; } /** * Convert the Configuration object into a hash array data structure * * @return array Config object 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(), 'created' => self::php_datetime($this->obj->created()), 'changed' => self::php_datetime($this->obj->lastModified()), ); // TODO: read object properties $this->data = $object; return $this->data; } } diff --git a/plugins/libkolab/lib/kolab_format_task.php b/plugins/libkolab/lib/kolab_format_task.php index 2a7a629f..0bfac3dd 100644 --- a/plugins/libkolab/lib/kolab_format_task.php +++ b/plugins/libkolab/lib/kolab_format_task.php @@ -1,145 +1,142 @@ * * 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_task extends kolab_format_xcal { - protected $read_func = 'kolabformat::readTodo'; - protected $write_func = 'kolabformat::writeTodo'; + public $CTYPEv2 = 'application/x-vnd.kolab.task'; + protected $objclass = 'Todo'; + protected $read_func = 'readTodo'; + protected $write_func = 'writeTodo'; - function __construct($xmldata = null) - { - $this->obj = new Todo; - $this->xmldata = $xmldata; - } /** * Set properties to the kolabformat object * * @param array Object data as hash array */ public function set(&$object) { $this->init(); // set common xcal properties parent::set($object); $this->obj->setPercentComplete(intval($object['complete'])); if (isset($object['start'])) $this->obj->setStart(self::get_datetime($object['start'], null, $object['start']->_dateonly)); $this->obj->setDue(self::get_datetime($object['due'], null, $object['due']->_dateonly)); $related = new vectors; if (!empty($object['parent_id'])) $related->push($object['parent_id']); $this->obj->setRelatedTo($related); // cache this data $this->data = $object; unset($this->data['_formatobj']); } /** * */ public function is_valid() { return $this->data || (is_object($this->obj) && $this->obj->isValid()); } /** * Convert the Configuration object into a hash array data structure * * @return array Config object data as hash array */ public function to_array() { // return cached result if (!empty($this->data)) return $this->data; $this->init(); // read common xcal props $object = parent::to_array(); $object['complete'] = intval($this->obj->percentComplete()); // if due date is set if ($due = $this->obj->due()) $object['due'] = self::php_datetime($due); // related-to points to parent task; we only support one relation $related = self::vector2array($this->obj->relatedTo()); if (count($related)) $object['parent_id'] = $related[0]; // TODO: map more properties $this->data = $object; return $this->data; } /** * Load data from old Kolab2 format */ public function fromkolab2($record) { $object = array( 'uid' => $record['uid'], 'changed' => $record['last-modification-date'], ); // TODO: implement this $this->data = $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() { $tags = array(); if ($this->data['status'] == 'COMPLETED' || $this->data['complete'] == 100) $tags[] = 'x-complete'; if ($this->data['priority'] == 1) $tags[] = 'x-flagged'; if (!empty($this->data['alarms'])) $tags[] = 'x-has-alarms'; if ($this->data['parent_id']) $tags[] = 'x-parent:' . $this->data['parent_id']; return $tags; } } diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php index 12419340..ac7de34b 100644 --- a/plugins/libkolab/lib/kolab_storage.php +++ b/plugins/libkolab/lib/kolab_storage.php @@ -1,654 +1,656 @@ * * 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 CTYPE_KEY_PRIVATE = '/private/vendor/kolab/folder-type'; const COLOR_KEY_SHARED = '/shared/vendor/kolab/color'; const COLOR_KEY_PRIVATE = '/private/vendor/kolab/color'; const SERVERSIDE_SUBSCRIPTION = 0; const CLIENTSIDE_SUBSCRIPTION = 1; + public static $version = 3.0; 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::$version = $rcmail->config->get('kolab_format_version', self::$version); 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) { // clear cached entries first if ($folder = self::get_folder($name)) $folder->cache->purge(); $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::set_folder_type($name, $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; } /** * Rename or Create a new IMAP folder. * * Does additional checks for permissions and folder name restrictions * * @param array Hash array with folder properties and metadata * - name: Folder name * - oldname: Old folder name when changed * - parent: Parent folder to create the new one in * - type: Folder type to create * @return mixed New folder name or False on failure */ public static function folder_update(&$prop) { self::setup(); $folder = rcube_charset::convert($prop['name'], RCMAIL_CHARSET, 'UTF7-IMAP'); $oldfolder = $prop['oldname']; // UTF7 $parent = $prop['parent']; // UTF7 $delimiter = self::$imap->get_hierarchy_delimiter(); if (strlen($oldfolder)) { $options = self::$imap->folder_info($oldfolder); } if (!empty($options) && ($options['norename'] || $options['protected'])) { } // sanity checks (from steps/settings/save_folder.inc) else if (!strlen($folder)) { self::$last_error = 'cannotbeempty'; return false; } else if (strlen($folder) > 128) { self::$last_error = 'nametoolong'; return false; } else { // these characters are problematic e.g. when used in LIST/LSUB foreach (array($delimiter, '%', '*') as $char) { if (strpos($folder, $delimiter) !== false) { self::$last_error = 'forbiddencharacter'; return false; } } } if (!empty($options) && ($options['protected'] || $options['norename'])) { $folder = $oldfolder; } else if (strlen($parent)) { $folder = $parent . $delimiter . $folder; } else { // add namespace prefix (when needed) $folder = self::$imap->mod_folder($folder, 'in'); } // Check access rights to the parent folder if (strlen($parent) && (!strlen($oldfolder) || $oldfolder != $folder)) { $parent_opts = self::$imap->folder_info($parent); if ($parent_opts['namespace'] != 'personal' && (empty($parent_opts['rights']) || !preg_match('/[ck]/', implode($parent_opts['rights']))) ) { self::$last_error = 'No permission to create folder'; return false; } } // update the folder name if (strlen($oldfolder)) { if ($oldfolder != $folder) { $result = self::folder_rename($oldfolder, $folder); } else $result = true; } // create new folder else { $result = self::folder_create($folder, $prop['type'], $prop['subscribed'] === self::SERVERSIDE_SUBSCRIPTION); } // save color in METADATA // TODO: also save 'showalarams' and other properties here if ($result && $prop['color']) { $meta_saved = false; $ns = self::$imap->folder_namespace($folder); if ($ns == 'personal') // save in shared namespace for personal folders $meta_saved = self::$imap->set_metadata($folder, array(self::COLOR_KEY_SHARED => $prop['color'])); if (!$meta_saved) // try in private namespace $meta_saved = self::$imap->set_metadata($folder, array(self::COLOR_KEY_PRIVATE => $prop['color'])); if ($meta_saved) unset($prop['color']); // unsetting will prevent fallback to local user prefs } return $result ? $folder : false; } /** * 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 = html::quote($folder); $folder = str_replace(html::quote($delim), ' » ', $folder); if ($prefix) $folder = html::quote($prefix) . ' ' . $folder; if (!$folder_ns) $folder_ns = 'personal'; return $folder; } /** * Helper method to generate a truncated folder name to display */ public static function folder_displayname($origname, &$names) { $name = $origname; // 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; return $name; } /** * 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 $attrs['is_escaped'] = true; $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, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE)); if (!is_array($folderdata)) { return array(); } $folderdata = array_map(array('kolab_storage', 'folder_select_metadata'), $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($root, $mbox); } else { $folders = self::$imap->list_folders($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; } /** * Callback for array_map to select the correct annotation value */ static function folder_select_metadata($types) { if (!empty($types[self::CTYPE_KEY_PRIVATE])) { return $types[self::CTYPE_KEY_PRIVATE]; } else if (!empty($types[self::CTYPE_KEY])) { list($ctype, $suffix) = explode('.', $types[self::CTYPE_KEY]); return $ctype; } return null; } /** * Returns type of IMAP folder * * @param string $folder Folder name (UTF7-IMAP) * * @return string Folder type */ static function folder_type($folder) { self::setup(); $metadata = self::$imap->get_metadata($folder, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE)); if (!is_array($metadata)) { return null; } if (!empty($metadata[$folder])) { return self::folder_select_metadata($metadata[$folder]); } return 'mail'; } /** * Sets folder content-type. * * @param string $folder Folder name * @param string $type Content type * * @return boolean True on success */ static function set_folder_type($folder, $type='mail') { self::setup(); list($ctype, $subtype) = explode('.', $type); $success = self::$imap->set_metadata($folder, array(self::CTYPE_KEY => $ctype, self::CTYPE_KEY_PRIVATE => $subtype ? $type : null)); if (!$success) // fallback: only set private annotation $success |= self::$imap->set_metadata($folder, array(self::CTYPE_KEY_PRIVATE => $type)); return $success; } } diff --git a/plugins/libkolab/lib/kolab_storage_cache.php b/plugins/libkolab/lib/kolab_storage_cache.php index c3e88da8..9eac164e 100644 --- a/plugins/libkolab/lib/kolab_storage_cache.php +++ b/plugins/libkolab/lib/kolab_storage_cache.php @@ -1,728 +1,729 @@ * * 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 $synclock = false; private $ready = false; private $max_sql_packet = 1046576; // 1 MB - 2000 bytes 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 ($this->enabled) { // remove sync-lock on script termination $rcmail->add_shutdown_function(array($this, '_sync_unlock')); // read max_allowed_packet from mysql config $this->max_sql_packet = min($this->db->get_variable('max_allowed_packet', 1048500), 4*1024*1024) - 2000; // mysql limit or max 4 MB } 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; // increase time limit @set_time_limit(500); // 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 foreach (array_diff($this->index, $old_index) as $msguid) { if ($object = $this->folder->read_object($msguid, '*')) { $this->_extended_insert($msguid, $object); } } $this->_extended_insert(0, null); // 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 from IMAP directly * * @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 type=? AND msguid=?", $this->resource_uri, $type ?: $this->folder->type, $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) { if (!$msguid) { return; } // delegate to another cache instance if ($foldername && $foldername != $this->folder->name) { kolab_storage::get_folder($foldername)->cache->set($msguid, $object); return; } // remove old entry if ($this->ready) { $this->db->query("DELETE FROM kolab_cache WHERE resource=? AND msguid=? AND type<>?", $this->resource_uri, $msguid, 'lock'); } if ($object) { // insert new object data... $this->insert($msguid, $object); } else { // ...or set in-memory cache to false $this->objects[$msguid] = $object; } } /** * Insert 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 */ public function insert($msguid, $object) { // write to cache if ($this->ready) { $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, created, changed, data, xml, dtstart, dtend, tags, words)". " VALUES (?, ?, ?, ?, " . $this->db->now() . ", ?, ?, ?, ?, ?, ?, ?)", $this->resource_uri, $objtype, $msguid, $object['uid'], $sql_data['changed'], $sql_data['data'], $sql_data['xml'], $sql_data['dtstart'], $sql_data['dtend'], $sql_data['tags'], $sql_data['words'] ); if (!$this->db->affected_rows($result)) { rcube::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; $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=? AND type<>?", $target->get_resource_uri(), $new_msguid, $this->resource_uri, $msguid, 'lock' ); } 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('', '', '') * @param boolean Set true to only return UIDs instead of complete objects * @return array List of Kolab data objects (each represented as hash array) or UIDs */ public function select($query = array(), $uids = false) { $result = array(); // read from local cache DB (assume it to be synchronized) if ($this->ready) { $sql_result = $this->db->query( "SELECT " . ($uids ? 'msguid, uid' : '*') . " FROM kolab_cache ". "WHERE resource=? " . $this->_sql_where($query), $this->resource_uri ); while ($sql_arr = $this->db->fetch_assoc($sql_result)) { if ($uids) { $this->uid2msg[$sql_arr['uid']] = $sql_arr['msguid']; $result[] = $sql_arr['uid']; } else 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_format::KTYPE_PREFIX . $filter['type']; $index = $this->imap->search_once($this->folder->name, $search)->get(); } // fetch all messages in $index from IMAP $result = $uids ? $this->_fetch_uids($index, $filter['type']) : $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_format::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 if ($param[1] == '~' || $param[1] == 'LIKE' || $param[1] == '!~' || $param[1] == '!LIKE') { $not = ($param[1] == '!~' || $param[1] == '!LIKE') ? 'NOT ' : ''; $param[1] = $not . 'LIKE'; $qvalue = $this->db->quote('%'.preg_replace('/(^\^|\$$)/', ' ', $param[2]).'%'); } else if ($param[0] == 'tags') { $param[1] = 'LIKE'; $qvalue = $this->db->quote('% '.$param[2].' %'); } 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 * @param string Requested object type or * for all * @param string IMAP folder to read from * @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->set($msguid, $object); } } return $results; } /** * Fetch object UIDs (aka message subjects) from IMAP * * @param array List of message UIDs to fetch * @param string Requested object type or * for all * @param string IMAP folder to read from * @return array List of parsed Kolab objects */ private function _fetch_uids($index, $type = null) { if (!$type) $type = $this->folder->type; $results = array(); foreach ((array)$this->imap->fetch_headers($this->folder->name, $index, false) as $msguid => $headers) { $object_type = kolab_format::mime2object_type($headers->others['x-kolab-type']); // check object type header and abort on mismatch if ($type != '*' && $object_type != $type) return false; $uid = $headers->subject; $this->uid2msg[$uid] = $msguid; $results[] = $uid; } 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('changed' => null, 'dtstart' => null, 'dtend' => null, 'xml' => '', 'tags' => '', 'words' => ''); $objtype = $object['_type'] ? $object['_type'] : $this->folder->type; // set type specific values if ($objtype == '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']) { - $recurrence = new kolab_date_recurrence($object); + if ($object['recurrence'] && $object['_formatobj']) { + $recurrence = new kolab_date_recurrence($object['_formatobj']); $sql_data['dtend'] = date('Y-m-d 23:59:59', $recurrence->end() ?: strtotime('now +1 year')); } } else if ($objtype == 'task') { if ($object['start']) $sql_data['dtstart'] = date('Y-m-d H:i:s', is_object($object['start']) ? $object['start']->format('U') : $object['start']); if ($object['due']) $sql_data['dtend'] = date('Y-m-d H:i:s', is_object($object['due']) ? $object['due']->format('U') : $object['due']); } if ($object['changed']) { $sql_data['changed'] = date('Y-m-d H:i:s', is_object($object['changed']) ? $object['changed']->format('U') : $object['changed']); } if ($object['_formatobj']) { - $sql_data['xml'] = preg_replace('!()[\n\r\t\s]+!ms', '$1', (string)$object['_formatobj']->write()); + $sql_data['xml'] = preg_replace('!()[\n\r\t\s]+!ms', '$1', (string)$object['_formatobj']->write(3.0)); $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']); + $object['_formatobj'] = kolab_format::factory($sql_arr['type'], 3.0, $sql_arr['xml']); return $object; } /** * Write records into cache using extended inserts to reduce the number of queries to be executed * * @param int Message UID. Set 0 to commit buffered inserts * @param array Kolab object to cache */ private function _extended_insert($msguid, $object) { static $buffer = ''; $line = ''; if ($object) { $sql_data = $this->_serialize($object); $objtype = $object['_type'] ? $object['_type'] : $this->folder->type; $values = array( $this->db->quote($this->resource_uri), $this->db->quote($objtype), $this->db->quote($msguid), $this->db->quote($object['uid']), $this->db->now(), $this->db->quote($sql_data['changed']), $this->db->quote($sql_data['data']), $this->db->quote($sql_data['xml']), $this->db->quote($sql_data['dtstart']), $this->db->quote($sql_data['dtend']), $this->db->quote($sql_data['tags']), $this->db->quote($sql_data['words']), ); $line = '(' . join(',', $values) . ')'; } if ($buffer && (!$msguid || (strlen($buffer) + strlen($line) > $this->max_sql_packet))) { $result = $this->db->query( "INSERT INTO kolab_cache ". " (resource, type, msguid, uid, created, changed, data, xml, dtstart, dtend, tags, words)". " VALUES $buffer" ); if (!$this->db->affected_rows($result)) { rcube::raise_error(array( 'code' => 900, 'type' => 'php', 'message' => "Failed to write to kolab cache" ), true); } $buffer = ''; } $buffer .= ($buffer ? ',' : '') . $line; } /** * 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' )); // abort if database is not set-up if ($this->db->is_error()) { $this->ready = false; return; } $this->synclock = true; // 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 */ public function _sync_unlock() { if (!$this->ready || !$this->synclock) return; $this->db->query( "UPDATE kolab_cache SET msguid=0 ". "WHERE resource=? AND type=?", $this->resource_uri, 'lock' ); $this->synclock = false; } /** * 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); + $index = $this->imap->search_once($this->folder->name, ($deleted ? '' : 'UNDELETED ') . + 'HEADER SUBJECT ' . rcube_imap_generic::escape($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 index 7c28eff5..302efd63 100644 --- a/plugins/libkolab/lib/kolab_storage_folder.php +++ b/plugins/libkolab/lib/kolab_storage_folder.php @@ -1,861 +1,860 @@ * * 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 { /** * The folder name. * @var string */ public $name; /** * The type of this folder. * @var string */ public $type; /** * Is this folder set to be the default for its type * @var boolean */ public $default = false; /** * Is this folder set to be default * @var boolean */ 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, $ftype = null) { $this->type_annotation = $ftype ? $ftype : kolab_storage::folder_type($name); list($this->type, $suffix) = explode('.', $this->type_annotation); $this->default = $suffix == 'default'; $this->name = $name; $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->get_user_name(); 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->get_user_name(), '@'); 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 mixed Pseudo-SQL query as list of filter parameter triplets * or string with object type (e.g. contact, event, todo, journal, note, configuration) * @return integer The number of objects of the given type * @see self::select() */ public function count($type_or_query = null) { if (!$type_or_query) $query = array(array('type','=',$this->type)); else if (is_string($type_or_query)) $query = array(array('type','=',$type_or_query)); else $query = $this->_prepare_query((array)$type_or_query); // synchronize cache first $this->cache->synchronize(); return $this->cache->count($query); } /** * 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(); // synchronize caches $this->cache->synchronize(); // fetch objects from cache return $this->cache->select($this->_prepare_query($query)); } /** * Getter for object UIDs only * * @param array Pseudo-SQL query as list of filter parameter triplets * @return array List of Kolab object UIDs */ public function get_uids($query = array()) { // synchronize caches $this->cache->synchronize(); // fetch UIDs from cache return $this->cache->select($this->_prepare_query($query), true); } /** * Helper method to sanitize query arguments */ private function _prepare_query($query) { $type = null; foreach ($query as $i => $param) { if ($param[0] == 'type') { $type = $param[2]; } else if (($param[0] == 'dtstart' || $param[0] == 'dtend' || $param[0] == 'changed')) { if (is_object($param[2]) && is_a($param[2], 'DateTime')) $param[2] = $param[2]->format('U'); if (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); return $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); // Message doesn't exist? if (empty($headers)) { return false; } $object_type = kolab_format::mime2object_type($headers->others['x-kolab-type']); $content_type = kolab_format::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; $size = null; // Use Content-Disposition 'size' as for the Kolab Format spec. if (isset($part->d_parameters['size'])) { $size = $part->d_parameters['size']; } // we can trust part size only if it's not encoded else if ($part->encoding == 'binary' || $part->encoding == '7bit' || $part->encoding == '8bit') { $size = $part->size; } $attachments[$key] = array( 'id' => $part->mime_id, 'name' => $part->filename, 'mimetype' => $part->mimetype, 'size' => $size, ); } } if (!$xml) { rcube::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 - $mime_version = $headers->others['x-kolab-mime-version']; - if (empty($mime_version)) { + $format_version = $headers->others['x-kolab-mime-version']; + if (empty($format_version)) { list($xmltype, $subtype) = explode('.', $object_type); $xmlhead = substr($xml, 0, 512); // detect old Kolab 2.0 format if (strpos($xmlhead, '<' . $xmltype) !== false && strpos($xmlhead, 'xmlns=') === false) - $mime_version = 2.0; + $format_version = 2.0; else - $mime_version = 3.0; // assume 3.0 + $format_version = 3.0; // assume 3.0 } - if ($mime_version <= 2.0) { - // read Kolab 2.0 format - $handler = class_exists('Horde_Kolab_Format') ? Horde_Kolab_Format::factory('XML', $xmltype, array('subtype' => $subtype)) : null; - if (!is_object($handler) || is_a($handler, 'PEAR_Error')) { - return false; - } + // get Kolab format handler for the given type + $format = kolab_format::factory($object_type, $format_version); - // XML-to-array - $object = $handler->load($xml); - $format->fromkolab2($object); - } - else { - // load Kolab 3 format using libkolabxml - $format->load($xml); - } + if (is_a($format, 'PEAR_Error')) + return false; + + // load Kolab object from XML part + $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])); rcube::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 $key => $att) { if (!isset($object['_attachments'][$key])) { $object['_attachments'][$key] = $old['_attachments'][$key]; } // unset deleted attachment entries if ($object['_attachments'][$key] == false) { unset($object['_attachments'][$key]); } // load photo.attachment from old Kolab2 format to be directly embedded in xcard block - else if ($key == 'photo.attachment' && !isset($object['photo']) && !$object['_attachments'][$key]['content'] && $att['id']) { - $object['photo'] = $this->get_attachment($object['_msguid'], $att['id'], $object['_mailbox']); + else if ($type == 'contact' && ($key == 'photo.attachment' || $key == 'kolab-picture.png') && $att['id']) { + if (!isset($object['photo'])) + $object['photo'] = $this->get_attachment($object['_msguid'], $att['id'], $object['_mailbox']); unset($object['_attachments'][$key]); } } } - // Parse attachments + // save contact photo to attachment for Kolab2 format + if (kolab_storage::$version == 2.0 && $object['photo'] && !$existing_photo) { + $attkey = 'kolab-picture.png'; // this file name is hard-coded in libkolab/kolabformatV2/contact.cpp + $object['_attachments'][$attkey] = array( + 'mimetype'=> rc_image_content_type($object['photo']), + 'content' => preg_match('![^a-z0-9/=+-]!i', $object['photo']) ? $object['photo'] : base64_decode($object['photo']), + ); + } + + // process attachments if (is_array($object['_attachments'])) { $numatt = count($object['_attachments']); foreach ($object['_attachments'] as $key => $attachment) { // make sure size is set, so object saved in cache contains this info if (!isset($attachment['size'])) { if (!empty($attachment['content'])) { $attachment['size'] = strlen($attachment['content']); } else if (!empty($attachment['path'])) { $attachment['size'] = filesize($attachment['path']); } $object['_attachments'][$key] = $attachment; } // generate unique keys (used as content-id) for attachments if (is_numeric($key) && $key < $numatt) { // derrive content-id from attachment file name $ext = preg_match('/(\.[a-z0-9]{1,6})$/i', $attachment['name'], $m) ? $m[1] : null; $basename = preg_replace('/[^a-z0-9_.-]/i', '', basename($attachment['name'], $ext)); // to 7bit ascii if (!$basename) $basename = 'noname'; $cid = $basename . '.' . microtime(true) . $ext; $object['_attachments'][$cid] = $attachment; unset($object['_attachments'][$key]); } } } 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->insert($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? * * @return boolean True if successful, false on error */ public function delete($object, $expunge = 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($msguid, 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 { rcube::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 = kolab_format::factory($type, kolab_storage::$version); if (PEAR::isError($format)) return false; $format->set($object); - $xml = $format->write(); + $xml = $format->write(kolab_storage::$version); $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'] = kolab_format::KTYPE_PREFIX . $type; - $headers['X-Kolab-Mime-Version'] = kolab_format::VERSION; + $headers['X-Kolab-Mime-Version'] = kolab_storage::$version; $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"); + $ctype = kolab_storage::$version == 2.0 ? $format->CTYPEv2 : $format->CTYPE; $mime->addAttachment($xml, // file - $format->CTYPE, // content-type + $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 $key => $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', '<' . $key . '>', RCMAIL_CHARSET, 'quoted-printable')); $name = !empty($att['name']) ? $att['name'] : $key; if (!empty($att['content'])) { $mime->addAttachment($att['content'], $att['mimetype'], $name, false, 'base64', 'attachment', '', '', '', null, null, '', RCMAIL_CHARSET, $headers); $part_id++; } else if (!empty($att['path'])) { $mime->addAttachment($att['path'], $att['mimetype'], $name, true, 'base64', 'attachment', '', '', '', null, null, '', RCMAIL_CHARSET, $headers); $part_id++; } $object['_attachments'][$key]['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; } } diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php index 0f9d35d1..f6ebe14d 100644 --- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php +++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php @@ -1,848 +1,849 @@ * * 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 tasklist_kolab_driver extends tasklist_driver { // features supported by the backend public $alarms = false; public $attachments = true; public $undelete = false; // task undelete action public $alarm_types = array('DISPLAY'); private $rc; private $plugin; private $lists; private $folders = array(); private $tasks = array(); /** * Default constructor */ public function __construct($plugin) { $this->rc = $plugin->rc; $this->plugin = $plugin; $this->_read_lists(); } /** * Read available calendars for the current user and store them internally */ private function _read_lists() { // already read sources if (isset($this->lists)) return $this->lists; // get all folders that have type "task" $this->folders = kolab_storage::get_folders('task'); $this->lists = array(); // convert to UTF8 and sort $names = array(); $default_folder = null; foreach ($this->folders as $i => $folder) { $names[$folder->name] = rcube_charset::convert($folder->name, 'UTF7-IMAP'); $this->folders[$folder->name] = $folder; if ($folder->default) $default_folder = $folder->name; } asort($names, SORT_LOCALE_STRING); // put default folder (aka INBOX) on top of the list if ($default_folder) { $default_name = $names[$default_folder]; unset($names[$default_folder]); $names = array_merge(array($default_folder => $default_name), $names); } $delim = $this->rc->get_storage()->get_hierarchy_delimiter(); $listnames = array(); $prefs = $this->rc->config->get('kolab_tasklists', array()); foreach ($names as $utf7name => $name) { $folder = $this->folders[$utf7name]; $path_imap = explode($delim, $name); $editname = array_pop($path_imap); // pop off raw name part $path_imap = join($delim, $path_imap); $name = kolab_storage::folder_displayname(kolab_storage::object_name($utf7name), $listnames); if ($folder->get_namespace() == 'personal') { $readonly = false; $alarms = true; } else { $alarms = false; $readonly = true; if (($rights = $folder->get_myrights()) && !PEAR::isError($rights)) { if (strpos($rights, 'i') !== false) $readonly = false; } } $list_id = kolab_storage::folder_id($utf7name); $tasklist = array( 'id' => $list_id, 'name' => $name, 'editname' => $editname, 'color' => 'CC0000', 'showalarms' => isset($prefs[$list_id]['showalarms']) ? $prefs[$list_id]['showalarms'] : $alarms, 'editable' => !$readonly, 'active' => $folder->is_subscribed(kolab_storage::SERVERSIDE_SUBSCRIPTION), 'parentfolder' => $path_imap, 'default' => $folder->default, 'class_name' => trim($folder->get_namespace() . ($folder->default ? ' default' : '')), ); $this->lists[$tasklist['id']] = $tasklist; $this->folders[$tasklist['id']] = $folder; } } /** * Get a list of available task lists from this source */ public function get_lists() { // attempt to create a default list for this user if (empty($this->lists)) { if ($this->create_list(array('name' => 'Default', 'color' => '000000'))) $this->_read_lists(); } return $this->lists; } /** * Create a new list assigned to the current user * * @param array Hash array with list properties * name: List name * color: The color of the list * showalarms: True if alarms are enabled * @return mixed ID of the new list on success, False on error */ public function create_list($prop) { $prop['type'] = 'task'; $prop['subscribed'] = kolab_storage::SERVERSIDE_SUBSCRIPTION; // subscribe to folder by default $folder = kolab_storage::folder_update($prop); if ($folder === false) { $this->last_error = kolab_storage::$last_error; return false; } // create ID $id = kolab_storage::folder_id($folder); $prefs['kolab_tasklists'] = $this->rc->config->get('kolab_tasklists', array()); if (isset($prop['showalarms'])) $prefs['kolab_tasklists'][$id]['showalarms'] = $prop['showalarms'] ? true : false; if ($prefs['kolab_tasklists'][$id]) $this->rc->user->save_prefs($prefs); return $id; } /** * Update properties of an existing tasklist * * @param array Hash array with list properties * id: List Identifier * name: List name * color: The color of the list * showalarms: True if alarms are enabled (if supported) * @return boolean True on success, Fales on failure */ public function edit_list($prop) { if ($prop['id'] && ($folder = $this->folders[$prop['id']])) { $prop['oldname'] = $folder->name; $prop['type'] = 'task'; $newfolder = kolab_storage::folder_update($prop); if ($newfolder === false) { $this->last_error = kolab_storage::$last_error; return false; } // create ID $id = kolab_storage::folder_id($newfolder); // fallback to local prefs $prefs['kolab_tasklists'] = $this->rc->config->get('kolab_tasklists', array()); unset($prefs['kolab_tasklists'][$prop['id']]); if (isset($prop['showalarms'])) $prefs['kolab_tasklists'][$id]['showalarms'] = $prop['showalarms'] ? true : false; if ($prefs['kolab_tasklists'][$id]) $this->rc->user->save_prefs($prefs); return $id; } return false; } /** * Set active/subscribed state of a list * * @param array Hash array with list properties * id: List Identifier * active: True if list is active, false if not * @return boolean True on success, Fales on failure */ public function subscribe_list($prop) { if ($prop['id'] && ($folder = $this->folders[$prop['id']])) { return $folder->subscribe($prop['active'], kolab_storage::SERVERSIDE_SUBSCRIPTION); } return false; } /** * Delete the given list with all its contents * * @param array Hash array with list properties * id: list Identifier * @return boolean True on success, Fales on failure */ public function remove_list($prop) { if ($prop['id'] && ($folder = $this->folders[$prop['id']])) { if (kolab_storage::folder_delete($folder->name)) return true; else $this->last_error = kolab_storage::$last_error; } return false; } /** * Get number of tasks matching the given filter * * @param array List of lists to count tasks of * @return array Hash array with counts grouped by status (all|flagged|completed|today|tomorrow|nodate) */ public function count_tasks($lists = null) { if (empty($lists)) $lists = array_keys($this->lists); else if (is_string($lists)) $lists = explode(',', $lists); $today_date = new DateTime('now', $this->plugin->timezone); $today = $today_date->format('Y-m-d'); $tomorrow_date = new DateTime('now + 1 day', $this->plugin->timezone); $tomorrow = $tomorrow_date->format('Y-m-d'); $counts = array('all' => 0, 'flagged' => 0, 'today' => 0, 'tomorrow' => 0, 'overdue' => 0, 'nodate' => 0); foreach ($lists as $list_id) { $folder = $this->folders[$list_id]; foreach ((array)$folder->select(array(array('tags','!~','x-complete'))) as $record) { $rec = $this->_to_rcube_task($record); if ($rec['complete'] >= 1.0) // don't count complete tasks continue; $counts['all']++; if ($rec['flagged']) $counts['flagged']++; if (empty($rec['date'])) $counts['nodate']++; else if ($rec['date'] == $today) $counts['today']++; else if ($rec['date'] == $tomorrow) $counts['tomorrow']++; else if ($rec['date'] < $today) $counts['overdue']++; } } return $counts; } /** * Get all taks records matching the given filter * * @param array Hash array with filter criterias: * - mask: Bitmask representing the filter selection (check against tasklist::FILTER_MASK_* constants) * - from: Date range start as string (Y-m-d) * - to: Date range end as string (Y-m-d) * - search: Search query string * @param array List of lists to get tasks from * @return array List of tasks records matchin the criteria */ public function list_tasks($filter, $lists = null) { if (empty($lists)) $lists = array_keys($this->lists); else if (is_string($lists)) $lists = explode(',', $lists); $results = array(); // query Kolab storage $query = array(); if ($filter['mask'] & tasklist::FILTER_MASK_COMPLETE) $query[] = array('tags','~','x-complete'); else $query[] = array('tags','!~','x-complete'); // full text search (only works with cache enabled) if ($filter['search']) { $search = mb_strtolower($filter['search']); foreach (rcube_utils::normalize_string($search, true) as $word) { $query[] = array('words', '~', $word); } } foreach ($lists as $list_id) { $folder = $this->folders[$list_id]; foreach ((array)$folder->select($query) as $record) { $task = $this->_to_rcube_task($record); $task['list'] = $list_id; // TODO: post-filter tasks returned from storage $results[] = $task; } } return $results; } /** * Return data of a specific task * * @param mixed Hash array with task properties or task UID * @return array Hash array with task properties or false if not found */ public function get_task($prop) { $id = is_array($prop) ? ($prop['uid'] ?: $prop['id']) : $prop; $list_id = is_array($prop) ? $prop['list'] : null; $folders = $list_id ? array($list_id => $this->folders[$list_id]) : $this->folders; // find task in the available folders foreach ($folders as $list_id => $folder) { if (is_numeric($list_id)) continue; if (!$this->tasks[$id] && ($object = $folder->get_object($id))) { $this->tasks[$id] = $this->_to_rcube_task($object); $this->tasks[$id]['list'] = $list_id; break; } } return $this->tasks[$id]; } /** * Get all decendents of the given task record * * @param mixed Hash array with task properties or task UID * @param boolean True if all childrens children should be fetched * @return array List of all child task IDs */ public function get_childs($prop, $recursive = false) { if (is_string($prop)) { $task = $this->get_task($prop); $prop = array('id' => $task['id'], 'list' => $task['list']); } $childs = array(); $list_id = $prop['list']; $task_ids = array($prop['id']); $folder = $this->folders[$list_id]; // query for childs (recursively) while ($folder && !empty($task_ids)) { $query_ids = array(); foreach ($task_ids as $task_id) { $query = array(array('tags','=','x-parent:' . $task_id)); foreach ((array)$folder->select($query) as $record) { // don't rely on kolab_storage_folder filtering if ($record['parent_id'] == $task_id) { $childs[] = $record['uid']; $query_ids[] = $record['uid']; } } } if (!$recursive) break; $task_ids = $query_ids; } return $childs; } /** * Get a list of pending alarms to be displayed to the user * * @param integer Current time (unix timestamp) * @param mixed List of list IDs to show alarms for (either as array or comma-separated string) * @return array A list of alarms, each encoded as hash array with task properties * @see tasklist_driver::pending_alarms() */ public function pending_alarms($time, $lists = null) { $interval = 300; $time -= $time % 60; $slot = $time; $slot -= $slot % $interval; $last = $time - max(60, $this->rc->session->get_keep_alive()); $last -= $last % $interval; // only check for alerts once in 5 minutes if ($last == $slot) return array(); if ($lists && is_string($lists)) $lists = explode(',', $lists); $time = $slot + $interval; $tasks = array(); $query = array(array('tags', '=', 'x-has-alarms'), array('tags', '!=', 'x-complete')); foreach ($this->lists as $lid => $list) { // skip lists with alarms disabled if (!$list['showalarms'] || ($lists && !in_array($lid, $lists))) continue; $folder = $this->folders[$lid]; foreach ((array)$folder->select($query) as $record) { if (!$record['alarms']) // don't trust query :-) continue; $task = $this->_to_rcube_task($record); // add to list if alarm is set $alarm = libcalendaring::get_next_alarm($task, 'task'); if ($alarm && $alarm['time'] && $alarm['time'] <= $time && $alarm['action'] == 'DISPLAY') { $id = $task['id']; $tasks[$id] = $task; $tasks[$id]['notifyat'] = $alarm['time']; } } } // get alarm information stored in local database if (!empty($tasks)) { $task_ids = array_map(array($this->rc->db, 'quote'), array_keys($tasks)); $result = $this->rc->db->query(sprintf( "SELECT * FROM kolab_alarms WHERE event_id IN (%s) AND user_id=?", join(',', $task_ids), $this->rc->db->now() ), $this->rc->user->ID ); while ($result && ($rec = $this->rc->db->fetch_assoc($result))) { $dbdata[$rec['event_id']] = $rec; } } $alarms = array(); foreach ($tasks as $id => $task) { // skip dismissed if ($dbdata[$id]['dismissed']) continue; // snooze function may have shifted alarm time $notifyat = $dbdata[$id]['notifyat'] ? strtotime($dbdata[$id]['notifyat']) : $task['notifyat']; if ($notifyat <= $time) $alarms[] = $task; } return $alarms; } /** * (User) feedback after showing an alarm notification * This should mark the alarm as 'shown' or snooze it for the given amount of time * * @param string Task identifier * @param integer Suspend the alarm for this number of seconds */ public function dismiss_alarm($id, $snooze = 0) { // delete old alarm entry $this->rc->db->query( "DELETE FROM kolab_alarms WHERE event_id=? AND user_id=?", $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( "INSERT INTO kolab_alarms (event_id, user_id, dismissed, notifyat) VALUES(?, ?, ?, ?)", $id, $this->rc->user->ID, $snooze > 0 ? 0 : 1, $notifyat ); return $this->rc->db->affected_rows($query); } /** * Convert from Kolab_Format to internal representation */ private function _to_rcube_task($record) { $task = array( 'id' => $record['uid'], 'uid' => $record['uid'], 'title' => $record['title'], # 'location' => $record['location'], 'description' => $record['description'], 'tags' => (array)$record['categories'], 'flagged' => $record['priority'] == 1, 'complete' => $record['status'] == 'COMPLETED' ? 1 : floatval($record['complete'] / 100), 'parent_id' => $record['parent_id'], ); // convert from DateTime to internal date format if (is_a($record['due'], 'DateTime')) { $task['date'] = $record['due']->format('Y-m-d'); - $task['time'] = $record['due']->format('h:i'); + if (!$record['due']->_dateonly) + $task['time'] = $record['due']->format('h:i'); } // convert from DateTime to internal date format if (is_a($record['start'], 'DateTime')) { $task['startdate'] = $record['start']->format('Y-m-d'); if (!$record['start']->_dateonly) $task['starttime'] = $record['start']->format('h:i'); } if (is_a($record['dtstamp'], 'DateTime')) { $task['changed'] = $record['dtstamp']; } if ($record['alarms']) { $task['alarms'] = $record['alarms']; } if (!empty($record['_attachments'])) { foreach ($record['_attachments'] as $key => $attachment) { if ($attachment !== false) { if (!$attachment['name']) $attachment['name'] = $key; $attachments[] = $attachment; } } $task['attachments'] = $attachments; } return $task; } /** * Convert the given task 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_task($task, $old = array()) { $object = $task; $object['categories'] = (array)$task['tags']; if (!empty($task['date'])) { $object['due'] = new DateTime($task['date'].' '.$task['time'], $this->plugin->timezone); if (empty($task['time'])) $object['due']->_dateonly = true; unset($object['date']); } if (!empty($task['startdate'])) { $object['start'] = new DateTime($task['startdate'].' '.$task['starttime'], $this->plugin->timezone); if (empty($task['starttime'])) $object['start']->_dateonly = true; unset($object['startdate']); } $object['complete'] = $task['complete'] * 100; if ($task['complete'] == 1.0) $object['status'] = 'COMPLETED'; if ($task['flagged']) $object['priority'] = 1; else $object['priority'] = $old['priority'] > 1 ? $old['priority'] : 0; // copy meta data (starting with _) from old object foreach ((array)$old as $key => $val) { if (!isset($object[$key]) && $key[0] == '_') $object[$key] = $val; } // delete existing attachment(s) if (!empty($task['deleted_attachments'])) { foreach ($task['deleted_attachments'] as $attachment) { if (is_array($object['_attachments'])) { foreach ($object['_attachments'] as $idx => $att) { if ($att['id'] == $attachment) $object['_attachments'][$idx] = false; } } } unset($task['deleted_attachments']); } // in kolab_storage attachments are indexed by content-id if (is_array($task['attachments'])) { foreach ($task['attachments'] as $idx => $attachment) { $key = null; // Roundcube ID has nothing to do with the storage ID, remove it if ($attachment['content']) { unset($attachment['id']); } else { foreach ((array)$old['_attachments'] as $cid => $oldatt) { if ($oldatt && $attachment['id'] == $oldatt['id']) $key = $cid; } } // replace existing entry if ($key) { $object['_attachments'][$key] = $attachment; } // append as new attachment else { $object['_attachments'][] = $attachment; } } unset($object['attachments']); } unset($object['tempid'], $object['raw'], $object['list'], $object['flagged'], $object['tags']); return $object; } /** * Add a single task to the database * * @param array Hash array with task properties (see header of tasklist_driver.php) * @return mixed New task ID on success, False on error */ public function create_task($task) { return $this->edit_task($task); } /** * Update an task entry with the given data * * @param array Hash array with task properties (see header of tasklist_driver.php) * @return boolean True on success, False on error */ public function edit_task($task) { $list_id = $task['list']; if (!$list_id || !($folder = $this->folders[$list_id])) return false; // moved from another folder if ($task['_fromlist'] && ($fromfolder = $this->folders[$task['_fromlist']])) { if (!$fromfolder->move($task['id'], $folder->name)) return false; unset($task['_fromlist']); } // load previous version of this task to merge if ($task['id']) { $old = $folder->get_object($task['id']); if (!$old || PEAR::isError($old)) return false; // merge existing properties if the update isn't complete if (!isset($task['title']) || !isset($task['complete'])) $task += $this->_to_rcube_task($old); } // generate new task object from RC input $object = $this->_from_rcube_task($task, $old); $saved = $folder->save($object, 'task', $task['id']); if (!$saved) { raise_error(array( 'code' => 600, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Error saving task object to Kolab server"), true, false); $saved = false; } else { $task = $this->_to_rcube_task($object); $task['list'] = $list_id; $this->tasks[$task['id']] = $task; } return $saved; } /** * Move a single task to another list * * @param array Hash array with task properties: * @return boolean True on success, False on error * @see tasklist_driver::move_task() */ public function move_task($task) { $list_id = $task['list']; if (!$list_id || !($folder = $this->folders[$list_id])) return false; // execute move command if ($task['_fromlist'] && ($fromfolder = $this->folders[$task['_fromlist']])) { return $fromfolder->move($task['id'], $folder->name); } return false; } /** * Remove a single task from the database * * @param array Hash array with task properties: * id: Task identifier * @param boolean Remove record irreversible (mark as deleted otherwise, if supported by the backend) * @return boolean True on success, False on error */ public function delete_task($task, $force = true) { $list_id = $task['list']; if (!$list_id || !($folder = $this->folders[$list_id])) return false; return $folder->delete($task['id']); } /** * Restores a single deleted task (if supported) * * @param array Hash array with task properties: * id: Task identifier * @return boolean True on success, False on error */ public function undelete_task($prop) { // TODO: implement this return false; } /** * Get attachment properties * * @param string $id Attachment identifier * @param array $task Hash array with event properties: * id: Task identifier * list: List identifier * * @return array Hash array with attachment properties: * id: Attachment identifier * name: Attachment name * mimetype: MIME content type of the attachment * size: Attachment size */ public function get_attachment($id, $task) { $task['uid'] = $task['id']; $task = $this->get_task($task); if ($task && !empty($task['attachments'])) { foreach ($task['attachments'] as $att) { if ($att['id'] == $id) return $att; } } return null; } /** * Get attachment body * * @param string $id Attachment identifier * @param array $task Hash array with event properties: * id: Task identifier * list: List identifier * * @return string Attachment body */ public function get_attachment_body($id, $task) { if ($storage = $this->folders[$task['list']]) { return $storage->get_attachment($task['id'], $id); } return false; } /** * */ public function tasklist_edit_form($fieldprop) { $select = kolab_storage::folder_selector('task', array('name' => 'parent', 'id' => 'taskedit-parentfolder'), null); $fieldprop['parent'] = array( 'id' => 'taskedit-parentfolder', 'label' => $this->plugin->gettext('parentfolder'), 'value' => $select->show(''), ); $formfields = array(); foreach (array('name','parent','showalarms') as $f) { $formfields[$f] = $fieldprop[$f]; } return parent::tasklist_edit_form($formfields); } }