diff --git a/plugins/libkolab/lib/kolab_format.php b/plugins/libkolab/lib/kolab_format.php index 97ac252d..76db15e5 100644 --- a/plugins/libkolab/lib/kolab_format.php +++ b/plugins/libkolab/lib/kolab_format.php @@ -1,328 +1,338 @@ * * 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; protected /*abstract*/ $xmltype; protected /*abstract*/ $subtype; protected $handler; protected $data; protected $xmldata; protected $kolab_object; protected $loaded = false; protected $version = 2.0; const KTYPE_PREFIX = 'application/x-vnd.kolab.'; const PRODUCT_ID = 'Roundcube-libkolab-horde-0.9'; /** * 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, $version = 2.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); list($xmltype, $subtype) = explode('.', $type); $type = preg_replace('/configuration\.[a-z.]+$/', 'configuration', $type); $suffix = preg_replace('/[^a-z]+/', '', $xmltype); $classname = 'kolab_format_' . $suffix; if (class_exists($classname)) return new $classname($xmldata, $subtype); 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('Horde_Kolab_Format'); + return class_exists('Horde_Kolab_Format_Xml'); return false; } /** * 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))); } /** * Convert alarm time into internal ical-based format * * @param int Alarm value as saved in Kolab 2 format * @return string iCal-style alarm value for internal use */ public static function from_kolab2_alarm($alarm_value) { if (!$alarm_value) return null; $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; return $alarm_value . $alarm_unit; } /** * Utility function to convert from Roundcube's internal alarms format * to an alarm offset in minutes used by the Kolab 2 format. * * @param string iCal-style alarm string * @return int Alarm offset in minutes */ public static function to_kolab2_alarm($alarms) { $ret = null; if (!$alarms) return $ret; $alarmbase = explode(":", $alarms); $avalue = intval(preg_replace('/[^0-9]/', '', $alarmbase[0])); if (preg_match("/H/",$alarmbase[0])) { $ret = $avalue*60; } else if (preg_match("/D/",$alarmbase[0])) { $ret = $avalue*24*60; } else { $ret = $avalue; } return $ret; } /** * Default constructor of all kolab_format_* objects */ public function __construct($xmldata = null, $subtype = null) { $this->subtype = $subtype; - $handler = Horde_Kolab_Format::factory('XML', $this->xmltype, array('subtype' => $this->subtype)); - if (!is_object($handler) || is_a($handler, 'PEAR_Error')) { - return; + try { + $factory = new Horde_Kolab_Format_Factory(); + $handler = $factory->create('Xml', $this->xmltype, array('subtype' => $this->subtype)); + if (is_object($handler) && !is_a($handler, 'PEAR_Error')) { + $this->handler = $handler; + $this->xmldata = $xmldata; + } + } + catch (Exception $e) { + rcube::raise_error($e, true); } - - $this->handler = $handler; - $this->xmldata = $xmldata; } /** * Check for format errors after calling kolabformat::write*() * * @return boolean True if there were errors, False if OK */ protected function format_errors($p) { $ret = false; if (is_object($p) && is_a($p, 'PEAR_Error')) { rcube::raise_error(array( 'code' => 660, 'type' => 'php', 'file' => __FILE__, 'line' => __LINE__, 'message' => "Horde_Kolab_Format error: " . $p->getMessage(), ), true); $ret = true; } return $ret; } /** * Generate a unique identifier for a Kolab object */ protected function generate_uid() { $rc = rcube::get_instance(); return strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($rc->user ? $rc->user->get_username() : rand()), 0, 16)); } /** * Initialize libkolabxml object with cached xml data */ protected function init() { if (!$this->loaded) { if ($this->xmldata) { $this->load($this->xmldata); $this->xmldata = null; } $this->loaded = true; } } /** * Direct getter for object properties */ public function __get($var) { return $this->data[$var]; } /** * Load Kolab object data from the given XML block * * @param string XML data * @return boolean True on success, False on failure */ public function load($xml) { $this->loaded = false; // XML-to-array - $object = $this->handler->load($xml); - if (!$this->format_errors($object)) { + try { + $object = $this->handler->load($xml, array('relaxed' => true)); $this->kolab_object = $object; $this->fromkolab2($object); $this->loaded = true; } + catch (Exception $e) { + rcube::raise_error($e, true); + console($xml); + } } /** * Write object data to XML format * * @param float Format version to write * @return string XML data */ public function write($version = null) { $this->init(); if ($version && !self::supports($version)) return false; // generate UID if not set if (!$this->kolab_object['uid']) { $this->kolab_object['uid'] = $this->generate_uid(); } - $xml = $this->handler->save($this->kolab_object); - - if (!$this->format_errors($xml) && strlen($xml)) { - $this->xmldata = $xml; - $this->data['uid'] = $this->kolab_object['uid']; + try { + $xml = $this->handler->save($this->kolab_object); + if (strlen($xml)) { + $this->xmldata = $xml; + $this->data['uid'] = $this->kolab_object['uid']; + } } - else { + catch (Exception $e) { + rcube::raise_error($e, true); $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(); /** * Getter for the parsed object data * * @return array Kolab object data as hash array */ public function to_array() { // load from XML if not done yet if (!empty($this->data)) $this->init(); return $this->data; } /** * Load object data from Kolab2 format * * @param array Hash array with object properties (produced by Horde Kolab_Format classes) */ abstract public function fromkolab2($object); /** * Callback for kolab_storage_cache to get object specific tags to cache * * @return array List of tags to save in cache */ public function get_tags() { return array(); } /** * Callback for kolab_storage_cache to get words to index for fulltext search * * @return array List of words to save in cache */ public function get_words() { return array(); } } diff --git a/plugins/libkolab/lib/kolab_format_contact.php b/plugins/libkolab/lib/kolab_format_contact.php index a7dcd5ce..5e125b26 100644 --- a/plugins/libkolab/lib/kolab_format_contact.php +++ b/plugins/libkolab/lib/kolab_format_contact.php @@ -1,338 +1,324 @@ * * 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/x-vnd.kolab.contact'; protected $xmltype = 'contact'; public static $fulltext_cols = array('name', 'firstname', 'surname', 'middlename', 'email'); public $phonetypes = array( 'home' => 'home1', 'work' => 'business1', 'text' => 'text', 'main' => 'primary', 'homefax' => 'homefax', 'workfax' => 'businessfax', 'mobile' => 'mobile', 'isdn' => 'isdn', 'pager' => 'pager', 'car' => 'car', 'company' => 'company', 'radio' => 'radio', 'telex' => 'telex', 'ttytdd' => 'ttytdd', 'other' => 'other', 'assistant' => 'assistant', 'callback' => 'callback', ); public $addresstypes = array( 'home' => 'home', 'work' => 'business', 'other' => 'other', 'office' => 0, ); // 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_fieldmap_name = array( + 'full-name' => 'name', + 'given-name' => 'firstname', + 'middle-names' => 'middlename', + 'last-name' => 'surname', + 'prefix' => 'prefix', + 'suffix' => 'suffix', + ); private $kolab2_phonetypes = array( 'home1' => 'home', 'business1' => 'work', 'business2' => 'work', 'businessfax' => 'workfax', ); private $kolab2_addresstypes = array( 'business' => 'work' ); private $kolab2_arrays = array( 'web-page' => 'url', 'im-address' => true, 'manager-name' => true, 'assistant' => true, 'children' => true, ); private $kolab2_gender = array(0 => 'male', 1 => 'female'); /** * Default constructor */ function __construct($xmldata = null, $subtype = null) { parent::__construct($xmldata, $subtype); } /** * Set contact properties to the kolabformat object * * @param array Contact data as hash array */ public function set(&$object) { $this->init(); if ($object['uid']) $this->kolab_object['uid'] = $object['uid']; $this->kolab_object['last-modification-date'] = time(); + // map name fields rcube => $kolab + foreach ($this->kolab2_fieldmap_name as $kolab => $rcube) { + $this->kolab_object['name'][$kolab] = $object[$rcube]; + } + // map fields rcube => $kolab foreach ($this->kolab2_fieldmap as $kolab => $rcube) { $this->kolab_object[$kolab] = $object[$rcube]; } // map gener values if (isset($object['gender'])) { $gender_map = array_flip($this->kolab2_gender); $this->kolab_object['gender'] = $gender_map[$object['gender']]; } // format dates - if ($object['birthday'] && ($date = @strtotime($object['birthday']))) - $this->kolab_object['birthday'] = date('Y-m-d', $date); - if ($object['anniversary'] && ($date = @strtotime($object['anniversary']))) - $this->kolab_object['anniversary'] = date('Y-m-d', $date); + foreach (array('birthday','anniversary') as $col) { + if (!empty($object[$col])) { + try { + $tz = new DateTimeZone(rcube::get_instance()->config->get('timezone')); + $dt = new DateTime($object[$col], $tz); + $this->kolab_object[$col] = $dt; + } + catch (Exception $e) { + rcube::raise_error($e, true); + } + } + else + $this->kolab_object[$col] = null; + } // make sure these attributes are single string values foreach ($this->kolab2_arrays as $col => $field) { if (!is_array($this->kolab_object[$col])) continue; if ($field === true) { $values = $this->kolab_object[$col]; } else { $values = array(); foreach ($this->kolab_object[$col] as $v) $values[] = $v[$field]; } $this->kolab_object[$col] = join('; ', $values); } - // save email addresses to field 'emails' + // save email addresses as simple-person attributes $emails = array(); - foreach ((array)$object['email'] as $email) - $emails[] = $email; - $this->kolab_object['emails'] = join(', ', array_filter($emails)); - unset($this->kolab_object['email']); + foreach ((array)$object['email'] as $email) { + if (!empty($email)) + $emails[] = array('smtp-address' => $email); + } + $this->kolab_object['email'] = $emails; // map phone types foreach ((array)$this->kolab_object['phone'] as $i => $phone) { if ($type = $this->phonetypes[$phone['type']]) $this->kolab_object['phone'][$i]['type'] = $type; } // save addresses (how weird is that?!) $this->kolab_object['address'] = array(); - $seen_types = array(); foreach ((array)$object['address'] as $adr) { if ($type = $this->addresstypes[$adr['type']]) { - $updated = false; - $basekey = 'addr-' . $type . '-'; - - $this->kolab_object[$basekey . 'type'] = $type; - $this->kolab_object[$basekey . 'street'] = $adr['street']; - $this->kolab_object[$basekey . 'locality'] = $adr['locality']; - $this->kolab_object[$basekey . 'postal-code'] = $adr['zipcode']; - $this->kolab_object[$basekey . 'region'] = $adr['region']; - $this->kolab_object[$basekey . 'country'] = $adr['country']; - - // check if we updates an existing address entry of this type... - foreach($this->kolab_object['address'] as $index => $address) { - if ($this->kolab_object['type'] == $type) { - $this->kolab_object['address'][$index] = $new_address; - $updated = true; - } - } - - // ... add as new if not - if (!$updated) { - $this->kolab_object['address'][] = array( - 'type' => $type, - 'street' => $adr['street'], - 'locality' => $adr['locality'], - 'postal-code' => $adr['code'], - 'region' => $adr['region'], - 'country' => $adr['country'], - ); - } - - $seen_types[$type] = true; + $this->kolab_object['address'][] = array( + 'type' => $type, + 'street' => $adr['street'], + 'locality' => $adr['locality'], + 'postal-code' => $adr['code'], + 'region' => $adr['region'], + 'country' => $adr['country'], + ); } else if ($adr['type'] == 'office') { $this->kolab_object['office-location'] = $adr['locality']; } } - // unset removed address properties - foreach ($this->addresstypes as $type) { - if (!$seen_types[$type]) { - $basekey = 'addr-' . $type . '-'; - unset( - $this->kolab_object[$basekey . 'type'], - $this->kolab_object[$basekey . 'street'], - $this->kolab_object[$basekey . 'locality'], - $this->kolab_object[$basekey . 'postal-code'], - $this->kolab_object[$basekey . 'region'], - $this->kolab_object[$basekey . 'country'] - ); - } - } - // cache this data $this->data = $object; unset($this->data['_formatobj']); } /** * */ public function is_valid() { return !empty($this->data['uid']); } /** * 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'], 'changed' => $record['last-modification-date'], 'email' => array(), 'phone' => array(), ); + // map name fields rcube => $kolab + foreach ($this->kolab2_fieldmap_name as $kolab => $rcube) { + $object[$rcube] = $record['name'][$kolab]; + } + foreach ($this->kolab2_fieldmap as $kolab => $rcube) { - if (is_array($record[$kolab]) || strlen($record[$kolab])) { + if (is_object($record[$kolab]) && is_a($record[$kolab], 'DateTime')) { + $object[$rcube] = $record[$kolab]; + } + else if (is_array($record[$kolab]) || strlen($record[$kolab])) { $object[$rcube] = $record[$kolab]; // split pseudo-arry values if ($field = $this->kolab2_arrays[$kolab]) { if ($field === true) { $object[$rcube] = explode('; ', $record[$kolab]); } else { $values = array(); foreach (explode('; ', $record[$kolab]) as $v) $values[] = array($field => $v); $object[$rcube] = $values; } } } } 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'])) { $kolab2_addresstypes = array_flip($this->addresstypes); foreach ($record['address'] as $i => $adr) { $object['address'][] = array( 'type' => $kolab2_addresstypes[$adr['type']] ? $kolab2_addresstypes[$adr['type']] : $adr['type'], 'street' => $adr['street'], 'locality' => $adr['locality'], 'code' => $adr['postal-code'], 'region' => $adr['region'], 'country' => $adr['country'], ); } } // map Kolab format phone types to Roundcube types if (!empty($object['phone'])) { $kolab2_phonetypes = array_merge(array_flip($this->phonetypes), $this->kolab2_phonetypes); foreach ($object['phone'] as $i => $phone) { if ($type = $kolab2_phonetypes[$phone['type']]) $object['phone'][$i]['type'] = $type; } } // 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); } } diff --git a/plugins/libkolab/lib/kolab_format_distributionlist.php b/plugins/libkolab/lib/kolab_format_distributionlist.php index 67822dfd..e138d8bd 100644 --- a/plugins/libkolab/lib/kolab_format_distributionlist.php +++ b/plugins/libkolab/lib/kolab_format_distributionlist.php @@ -1,92 +1,92 @@ * * 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/x-vnd.kolab.distribution-list'; protected $xmltype = 'distributionlist'; /** * Set properties to the kolabformat object * * @param array Object data as hash array */ public function set(&$object) { $this->init(); if ($object['uid']) $this->kolab_object['uid'] = $object['uid']; - $this->kolab_object['last-modification-date'] = time(); - $this->kolab_object['last-name'] = $object['name']; + $this->kolab_object['changed'] = new DateTime(); + $this->kolab_object['display-name'] = $object['name']; $this->kolab_object['member'] = array(); foreach ($object['member'] as $member) { $this->kolab_object['member'][] = array( 'uid' => $member['uid'], 'smtp-address' => $member['email'], 'display-name' => $member['name'], ); } // 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 !empty($this->data['uid']) && !empty($this->data['name']); } /** * Load data from old Kolab2 format */ public function fromkolab2($record) { $object = array( 'uid' => $record['uid'], - 'changed' => $record['last-modification-date'], - 'name' => $record['last-name'], + 'changed' => $record['changed'], + 'name' => $record['display-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; } } diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php index 5b8eca7c..36f304b9 100644 --- a/plugins/libkolab/lib/kolab_storage.php +++ b/plugins/libkolab/lib/kolab_storage.php @@ -1,655 +1,655 @@ * * 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 = 2.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::$imap = $rcmail->get_storage(); - self::$ready = class_exists('Horde_Kolab_Format') && + self::$ready = class_exists('Horde_Kolab_Format_Factory') && (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/libkolab.php b/plugins/libkolab/libkolab.php index 5e55943e..5c35a198 100644 --- a/plugins/libkolab/libkolab.php +++ b/plugins/libkolab/libkolab.php @@ -1,78 +1,83 @@ * * Copyright (C) 2012, Kolab Systems AG * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ class libkolab extends rcube_plugin { /** * Required startup method of a Roundcube plugin */ public function init() { // load local config $this->load_config(); $this->add_hook('storage_init', array($this, 'storage_init')); // extend include path to load bundled lib classes $include_path = $this->home . '/lib' . PATH_SEPARATOR . ini_get('include_path'); set_include_path($include_path); $rcmail = rcube::get_instance(); try { kolab_format::$timezone = new DateTimeZone($rcmail->config->get('timezone', 'GMT')); } catch (Exception $e) { rcube::raise_error($e, true); kolab_format::$timezone = new DateTimeZone('GMT'); } - // load Horde Kolab_Format dependencies - if (include_once('Horde/Util.php')) { - include_once 'Horde/Kolab/Format.php'; - include_once 'Horde/Kolab/Format/XML.php'; - include_once 'Horde/Kolab/Format/XML/contact.php'; - include_once 'Horde/Kolab/Format/XML/distributionlist.php'; - include_once 'Horde/Kolab/Format/XML/event.php'; - include_once 'Horde/Kolab/Format/XML/note.php'; - include_once 'Horde/Kolab/Format/XML/task.php'; - include_once 'Horde_Kolab_Format_XML_configuration.php'; - - String::setDefaultCharset('UTF-8'); - } + // Register autoloader + spl_autoload_register('libkolab::autoload'); } /** * Hook into IMAP FETCH HEADER.FIELDS command and request Kolab-specific headers */ function storage_init($p) { $p['fetch_headers'] = trim($p['fetch_headers'] .' X-KOLAB-TYPE X-KOLAB-MIME-VERSION'); return $p; } + /** + * Autloader routine for Horde classes + */ + public static function autoload($classname) + { + if (strpos($classname, 'Horde') === 0) { + $filename = str_replace('_', '/', $classname); + if ($fp = @fopen("$filename.php", 'r', true)) { + fclose($fp); + include_once "$filename.php"; + return true; + } + } + + return false; + } }