Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F256930
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
174 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/lib/kolab/plugins/libkolab/README b/lib/kolab/plugins/libkolab/README
index 7631220..2f94839 100644
--- a/lib/kolab/plugins/libkolab/README
+++ b/lib/kolab/plugins/libkolab/README
@@ -1,32 +1,28 @@
libkolab plugin to access to Kolab groupware data
=================================================
The contained library classes establish a connection to the Kolab server
and manage the access to the Kolab groupware objects stored in various
IMAP folders. For reading and writing these objects, the PHP bindings of
the libkolabxml library are used.
REQUIREMENTS
------------
* libkolabxml PHP bindings
- kolabformat.so loaded into PHP
- kolabformat.php placed somewhere in the include_path
* PEAR: HTTP/Request2
* PEAR: Net/URL2
-* Optional for old format support:
- Horde Kolab_Format package and all of its dependencies
- which are at least Horde_(Browser,DOM,NLS,String,Utils)
-
INSTALLATION
------------
To use local cache you need to create a dedicated table in Roundcube's database.
-To do so, execute the SQL commands in SQL/<yourdatabase>.sql
+To do so, execute the SQL commands in SQL/<yourdatabase>.initial.sql
CONFIGURATION
-------------
Rename config.inc.php.dist to config.inc.php in the plugin folder.
For available configuration options see config.inc.php.dist file.
diff --git a/lib/kolab/plugins/libkolab/SQL/mysql.sql b/lib/kolab/plugins/libkolab/SQL/mysql.initial.sql
similarity index 89%
rename from lib/kolab/plugins/libkolab/SQL/mysql.sql
rename to lib/kolab/plugins/libkolab/SQL/mysql.initial.sql
index 244ab3d..8603bc8 100644
--- a/lib/kolab/plugins/libkolab/SQL/mysql.sql
+++ b/lib/kolab/plugins/libkolab/SQL/mysql.initial.sql
@@ -1,25 +1,27 @@
/**
* libkolab database schema
*
* @version @package_version@
* @author Thomas Bruederli
* @licence GNU AGPL
**/
DROP TABLE IF EXISTS `kolab_cache`;
CREATE TABLE `kolab_cache` (
`resource` VARCHAR(255) CHARACTER SET ascii NOT NULL,
`type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(128) CHARACTER SET ascii NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` TEXT NOT NULL,
`xml` TEXT NOT NULL,
`dtstart` DATETIME,
`dtend` DATETIME,
`tags` VARCHAR(255) NOT NULL,
`words` TEXT NOT NULL,
PRIMARY KEY(`resource`,`type`,`msguid`)
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
+
+INSERT INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2013011000');
diff --git a/lib/kolab/plugins/libkolab/SQL/mysql/2013011000.sql b/lib/kolab/plugins/libkolab/SQL/mysql/2013011000.sql
new file mode 100644
index 0000000..fe6741a
--- /dev/null
+++ b/lib/kolab/plugins/libkolab/SQL/mysql/2013011000.sql
@@ -0,0 +1 @@
+-- empty
\ No newline at end of file
diff --git a/lib/kolab/plugins/libkolab/UPGRADING b/lib/kolab/plugins/libkolab/UPGRADING
new file mode 100644
index 0000000..e7f04d8
--- /dev/null
+++ b/lib/kolab/plugins/libkolab/UPGRADING
@@ -0,0 +1,9 @@
+UPGRADING instructions
+======================
+
+To update database schema please run in Roundcube bin/ directory:
+
+updatedb.sh --package=libkolab --version=<version> --dir=../plugins/libkolab/SQL
+
+[*] Replace <version> with Roundcube version e.g. 0.7.3
+[*] Roundcube should be upgraded before plugin upgrades
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_format.php b/lib/kolab/plugins/libkolab/lib/kolab_format.php
index b3bb4c4..809fb29 100644
--- a/lib/kolab/plugins/libkolab/lib/kolab_format.php
+++ b/lib/kolab/plugins/libkolab/lib/kolab_format.php
@@ -1,419 +1,468 @@
<?php
/**
* Kolab format model class wrapping libkolabxml bindings
*
* Abstract base class for different Kolab groupware objects read from/written
* to the new Kolab 3 format using the PHP bindings of libkolabxml.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
abstract class kolab_format
{
public static $timezone;
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;
+ protected $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 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 = 3.0, $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, $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)
+ 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 $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']) {
if ($this->xmlobject) {
$this->data['uid'] = $this->xmlobject->getSerializedUID();
}
if (empty($this->data['uid'])) {
$this->data['uid'] = kolabformat::getSerializedUID();
}
$this->obj->setUid($this->data['uid']);
}
}
/**
* Initialize libkolabxml object with cached xml data
*/
protected function init()
{
if (!$this->loaded) {
if ($this->xmldata) {
$this->load($this->xmldata);
$this->xmldata = null;
}
$this->loaded = true;
}
}
/**
* 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)
+ 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)
{
$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($version = null)
{
$this->init();
$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);
+ public function set(&$object)
+ {
+ $this->init();
- /**
- *
- */
- abstract public function is_valid();
+ if (!empty($object['uid']))
+ $this->obj->setUid($object['uid']);
+
+ // set some automatic values if missing
+ if (method_exists($this->obj, 'setCreated') && !$this->obj->created()) {
+ if (empty($object['created']))
+ $object['created'] = new DateTime('now', self::$timezone);
+ $this->obj->setCreated(self::get_datetime($object['created']));
+ }
+
+ $object['changed'] = new DateTime('now', self::$timezone);
+ $this->obj->setLastModified(self::get_datetime($object['changed'], new DateTimeZone('UTC')));
+
+ // Save custom properties of the given object
+ if (!empty($object['x-custom'])) {
+ $vcustom = new vectorcs;
+ foreach ($object['x-custom'] as $cp) {
+ if (is_array($cp))
+ $vcustom->push(new CustomProperty($cp[0], $cp[1]));
+ }
+ $this->obj->setCustomProperties($vcustom);
+ }
+ }
/**
* Convert the Kolab object into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Kolab object data as hash array
*/
- abstract public function to_array($data = array());
+ public function to_array($data = array())
+ {
+ $this->init();
+
+ // read object properties into local data object
+ $object = array(
+ 'uid' => $this->obj->uid(),
+ 'changed' => self::php_datetime($this->obj->lastModified()),
+ );
+
+ // not all container support the created property
+ if (method_exists($this->obj, 'created')) {
+ $object['created'] = self::php_datetime($this->obj->created());
+ }
+
+ // read custom properties
+ $vcustom = $this->obj->customProperties();
+ for ($i=0; $i < $vcustom->size(); $i++) {
+ $cp = $vcustom->get($i);
+ $object['x-custom'][] = array($cp->identifier, $cp->value);
+ }
+
+ return $object;
+ }
+
+ /**
+ * Object validation method to be implemented by derived classes
+ */
+ abstract public function is_valid();
/**
* 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/lib/kolab/plugins/libkolab/lib/kolab_format_contact.php b/lib/kolab/plugins/libkolab/lib/kolab_format_contact.php
index 7994b97..cde0288 100644
--- a/lib/kolab/plugins/libkolab/lib/kolab_format_contact.php
+++ b/lib/kolab/plugins/libkolab/lib/kolab_format_contact.php
@@ -1,423 +1,409 @@
<?php
/**
* Kolab Contact model class
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_format_contact extends kolab_format
{
public $CTYPE = 'application/vcard+xml';
public $CTYPEv2 = 'application/x-vnd.kolab.contact';
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,
);
/**
* Default constructor
*/
function __construct($xmldata = null, $version = 3.0)
{
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')));
+ // set common object properties
+ parent::set($object);
// 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']);
+ $this->obj->setCategories(self::array2vector($object['categories']));
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
*
* @param array Additional data for merge
*
* @return array Contact data as hash array
*/
public function to_array($data = array())
{
// return cached result
if (!empty($this->data))
return $this->data;
- $this->init();
+ // read common object props into local data object
+ $object = parent::to_array();
- // read object properties into local data object
- $object = array(
- 'uid' => $this->obj->uid(),
- 'name' => $this->obj->name(),
- 'changed' => self::php_datetime($this->obj->lastModified()),
- );
+ $object['name'] = $this->obj->name();
$nc = $this->obj->nameComponents();
$object['surname'] = join(' ', self::vector2array($nc->surnames()));
$object['firstname'] = join(' ', self::vector2array($nc->given()));
$object['middlename'] = join(' ', self::vector2array($nc->additional()));
$object['prefix'] = join(' ', self::vector2array($nc->prefixes()));
$object['suffix'] = join(' ', self::vector2array($nc->suffixes()));
$object['nickname'] = join(' ', self::vector2array($this->obj->nickNames()));
$object['profession'] = join(' ', self::vector2array($this->obj->titles()));
+ $object['categories'] = self::vector2array($this->obj->categories());
// 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));
}
/**
* 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/lib/kolab/plugins/libkolab/lib/kolab_format_distributionlist.php b/lib/kolab/plugins/libkolab/lib/kolab_format_distributionlist.php
index f91f697..d25bd47 100644
--- a/lib/kolab/plugins/libkolab/lib/kolab_format_distributionlist.php
+++ b/lib/kolab/plugins/libkolab/lib/kolab_format_distributionlist.php
@@ -1,122 +1,115 @@
<?php
/**
* Kolab Distribution List model class
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_format_distributionlist extends kolab_format
{
public $CTYPE = 'application/vcard+xml';
public $CTYPEv2 = 'application/x-vnd.kolab.distribution-list';
protected $objclass = 'DistList';
protected $read_func = 'readDistlist';
protected $write_func = 'writeDistlist';
/**
* 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')));
+ // set common object properties
+ parent::set($object);
$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());
}
/**
* Convert the Distlist object into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Distribution list data as hash array
*/
public function to_array($data = array())
{
// return cached result
if (!empty($this->data))
return $this->data;
- $this->init();
+ // read common object props into local data object
+ $object = parent::to_array();
- // read object properties
- $object = array(
- 'uid' => $this->obj->uid(),
- 'changed' => self::php_datetime($this->obj->lastModified()),
+ // add object properties
+ $object += array(
'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/lib/kolab/plugins/libkolab/lib/kolab_format_event.php b/lib/kolab/plugins/libkolab/lib/kolab_format_event.php
index d750f8e..ec97767 100644
--- a/lib/kolab/plugins/libkolab/lib/kolab_format_event.php
+++ b/lib/kolab/plugins/libkolab/lib/kolab_format_event.php
@@ -1,198 +1,275 @@
<?php
/**
* Kolab Event model class
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_format_event extends kolab_format_xcal
{
public $CTYPEv2 = 'application/x-vnd.kolab.event';
protected $objclass = 'Event';
protected $read_func = 'readEvent';
protected $write_func = 'writeEvent';
+ /**
+ * Default constructor
+ */
+ function __construct($data = null, $version = 3.0)
+ {
+ parent::__construct(is_string($data) ? $data : null, $version);
+
+ // got an Event object as argument
+ if (is_object($data) && is_a($data, $this->objclass)) {
+ $this->obj = $data;
+ $this->loaded = true;
+ }
+ }
/**
* Clones into an instance of libcalendaring's extended EventCal class
*
* @return mixed EventCal object or false on failure
*/
public function to_libcal()
{
static $error_logged = false;
if (class_exists('kolabcalendaring')) {
return new EventCal($this->obj);
}
else if (!$error_logged) {
$error_logged = true;
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'message' => "required kolabcalendaring module not found"
), true);
}
return 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);
}
+
+ foreach ((array)$object['links'] as $link) {
+ $attach = new Attachment;
+ $attach->setUri($link, null);
+ $vattach->push($attach);
+ }
+
$this->obj->setAttachments($vattach);
+ // save recurrence exceptions
+ if ($object['recurrence']['EXCEPTIONS']) {
+ $vexceptions = new vectorevent;
+ foreach((array)$object['recurrence']['EXCEPTIONS'] as $exception) {
+ $exevent = new kolab_format_event;
+ $exevent->set($this->compact_exception($exception, $object)); // only save differing values
+ $exevent->obj->setRecurrenceID(self::get_datetime($exception['start'], null, true), (bool)$exception['thisandfuture']);
+ $vexceptions->push($exevent->obj);
+ }
+ $this->obj->setExceptions($vexceptions);
+ }
+
// 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
*
* @param array Additional data for merge
*
* @return array Event data as hash array
*/
public function to_array($data = 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:' && $attach->label()) {
$name = $attach->label();
$data = $attach->data();
$object['_attachments'][$name] = array(
'name' => $name,
'mimetype' => $attach->mimetype(),
'size' => strlen($data),
'content' => $data,
);
}
+ else if (substr($attach->uri(), 0, 4) == 'http') {
+ $object['links'][] = $attach->uri();
+ }
+ }
+
+ // read exception event objects
+ if (($exceptions = $this->obj->exceptions()) && is_object($exceptions) && $exceptions->size()) {
+ for ($i=0; $i < $exceptions->size(); $i++) {
+ if (($exobj = $exceptions->get($i))) {
+ $exception = new kolab_format_event($exobj);
+ if ($exception->is_valid()) {
+ $object['recurrence']['EXCEPTIONS'][] = $this->expand_exception($exception->to_array(), $object);
+ }
+ }
+ }
+ }
+ // this is an exception object
+ else if ($this->obj->recurrenceID()->isValid()) {
+ $object['thisandfuture'] = $this->obj->thisAndFuture();
}
// merge with additional data, e.g. attachments from the message
if ($data) {
foreach ($data as $idx => $value) {
if (is_array($value)) {
$object[$idx] = array_merge((array)$object[$idx], $value);
}
else {
$object[$idx] = $value;
}
}
}
return $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();
foreach ((array)$this->data['categories'] as $cat) {
$tags[] = rcube_utils::normalize_string($cat);
}
if (!empty($this->data['alarms'])) {
$tags[] = 'x-has-alarms';
}
return $tags;
}
+ /**
+ * Remove some attributes from the exception container
+ */
+ private function compact_exception($exception, $master)
+ {
+ static $forbidden = array('recurrence','organizer','attendees','sequence');
+
+ $out = $exception;
+ foreach ($exception as $prop => $val) {
+ if (in_array($prop, $forbidden)) {
+ unset($out[$prop]);
+ }
+ }
+
+ return $out;
+ }
+
+ /**
+ * Copy attributes not specified by the exception from the master event
+ */
+ private function expand_exception($exception, $master)
+ {
+ foreach ($master as $prop => $value) {
+ if (empty($exception[$prop]) && !empty($value))
+ $exception[$prop] = $value;
+ }
+
+ return $exception;
+ }
+
}
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_format_file.php b/lib/kolab/plugins/libkolab/lib/kolab_format_file.php
index 22d1e7c..191c7fe 100644
--- a/lib/kolab/plugins/libkolab/lib/kolab_format_file.php
+++ b/lib/kolab/plugins/libkolab/lib/kolab_format_file.php
@@ -1,169 +1,162 @@
<?php
/**
* Kolab File model class
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_format_file extends kolab_format
{
public $CTYPE = 'application/x-vnd.kolab.file';
protected $objclass = 'File';
protected $read_func = 'kolabformat::readKolabFile';
protected $write_func = 'kolabformat::writeKolabFile';
protected $sensitivity_map = array(
'public' => kolabformat::ClassPublic,
'private' => kolabformat::ClassPrivate,
'confidential' => kolabformat::ClassConfidential,
);
/**
* Set properties to the kolabformat object
*
* @param array Object data as hash array
*/
public function set(&$object)
{
- $this->init();
+ // set common object properties
+ parent::set($object);
- // 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->setClassification($this->sensitivity_map[$object['sensitivity']]);
$this->obj->setCategories(self::array2vector($object['categories']));
if (isset($object['notes'])) {
$this->obj->setNote($object['notes']);
}
// Add file attachment
if (!empty($object['_attachments'])) {
$cid = key($object['_attachments']);
$attach_attr = $object['_attachments'][$cid];
$attach = new Attachment;
$attach->setLabel((string)$attach_attr['name']);
$attach->setUri('cid:' . $cid, $attach_attr['mimetype']);
$this->obj->setFile($attach);
// make sure size is set, so object saved in cache contains this info
if (!isset($attach_attr['size'])) {
if (isset($attach_attr['content'])) {
$object['_attachments'][$cid]['size'] = strlen($attach_attr['content']);
}
else if (isset($attach_attr['path'])) {
$object['_attachments'][$cid]['size'] = @filesize($attach_attr['path']);
}
}
}
// 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
*
* @param array Additional data for merge
*
* @return array Config object data as hash array
*/
public function to_array($data = array())
{
// return cached result
if (!empty($this->data))
return $this->data;
- $this->init();
+ // read common object props into local data object
+ $object = parent::to_array();
$sensitivity_map = array_flip($this->sensitivity_map);
// read object properties
- $object = array(
- 'uid' => $this->obj->uid(),
- 'created' => self::php_datetime($this->obj->created()),
- 'changed' => self::php_datetime($this->obj->lastModified()),
+ $object += array(
'sensitivity' => $sensitivity_map[$this->obj->classification()],
'categories' => self::vector2array($this->obj->categories()),
'notes' => $this->obj->note(),
);
// merge with additional data, e.g. attachments from the message
if ($data) {
foreach ($data as $idx => $value) {
if (is_array($value)) {
$object[$idx] = array_merge((array)$object[$idx], $value);
}
else {
$object[$idx] = $value;
}
}
}
return $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();
foreach ((array)$this->data['categories'] as $cat) {
$tags[] = rcube_utils::normalize_string($cat);
}
return $tags;
}
/**
* Callback for kolab_storage_cache to get words to index for fulltext search
*
* @return array List of words to save in cache
*/
public function get_words()
{
// Store filename in 'words' for fast access to file by name
if (empty($this->data['_attachments'])) {
return array();
}
$attachment = array_shift($this->data['_attachments']);
return array($attachment['name']);
}
}
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_format_journal.php b/lib/kolab/plugins/libkolab/lib/kolab_format_journal.php
index 01cdc3d..3528d16 100644
--- a/lib/kolab/plugins/libkolab/lib/kolab_format_journal.php
+++ b/lib/kolab/plugins/libkolab/lib/kolab_format_journal.php
@@ -1,94 +1,82 @@
<?php
/**
* Kolab Journal model class
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_format_journal extends kolab_format
{
public $CTYPE = 'application/calendar+xml';
public $CTYPEv2 = 'application/x-vnd.kolab.journal';
protected $objclass = 'Journal';
protected $read_func = 'readJournal';
protected $write_func = 'writeJournal';
/**
* 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')));
+ // set common object properties
+ parent::set($object);
// 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());
}
/**
* Convert the Configuration object into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Config object data as hash array
*/
public function to_array($data = 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()),
- );
+ // read common object props into local data object
+ $object = parent::to_array();
// TODO: read object properties
$this->data = $object;
return $this->data;
}
}
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_format_note.php b/lib/kolab/plugins/libkolab/lib/kolab_format_note.php
index 5a9c0bc..cee6345 100644
--- a/lib/kolab/plugins/libkolab/lib/kolab_format_note.php
+++ b/lib/kolab/plugins/libkolab/lib/kolab_format_note.php
@@ -1,94 +1,82 @@
<?php
/**
* Kolab Note model class
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_format_note extends kolab_format
{
public $CTYPE = 'application/x-vnd.kolab.note';
public $CTYPEv2 = 'application/x-vnd.kolab.note';
protected $objclass = 'Note';
protected $read_func = 'readNote';
protected $write_func = 'writeNote';
/**
* 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')));
+ // set common object properties
+ parent::set($object);
// 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());
}
/**
* Convert the Configuration object into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Config object data as hash array
*/
public function to_array($data = 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()),
- );
+ // read common object props into local data object
+ $object = parent::to_array();
// TODO: read object properties
$this->data = $object;
return $this->data;
}
}
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_format_task.php b/lib/kolab/plugins/libkolab/lib/kolab_format_task.php
index d66cafd..0fa2806 100644
--- a/lib/kolab/plugins/libkolab/lib/kolab_format_task.php
+++ b/lib/kolab/plugins/libkolab/lib/kolab_format_task.php
@@ -1,129 +1,125 @@
<?php
/**
* Kolab Task (ToDo) model class
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_format_task extends kolab_format_xcal
{
public $CTYPEv2 = 'application/x-vnd.kolab.task';
protected $objclass = 'Todo';
protected $read_func = 'readTodo';
protected $write_func = 'writeTodo';
/**
* 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
*
* @param array Additional data for merge
*
* @return array Config object data as hash array
*/
public function to_array($data = 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;
}
/**
* 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/lib/kolab/plugins/libkolab/lib/kolab_format_xcal.php b/lib/kolab/plugins/libkolab/lib/kolab_format_xcal.php
index a6450af..95f0718 100644
--- a/lib/kolab/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/lib/kolab/plugins/libkolab/lib/kolab_format_xcal.php
@@ -1,402 +1,396 @@
<?php
/**
* Xcal based Kolab format class wrapping libkolabxml bindings
*
* Base class for xcal-based Kolab groupware objects such as event, todo, journal
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
abstract class kolab_format_xcal extends kolab_format
{
public $CTYPE = 'application/calendar+xml';
public static $fulltext_cols = array('title', 'description', 'location', 'attendees:name', 'attendees:email', 'categories');
protected $sensitivity_map = array(
'public' => kolabformat::ClassPublic,
'private' => kolabformat::ClassPrivate,
'confidential' => kolabformat::ClassConfidential,
);
protected $role_map = array(
'REQ-PARTICIPANT' => kolabformat::Required,
'OPT-PARTICIPANT' => kolabformat::Optional,
'NON-PARTICIPANT' => kolabformat::NonParticipant,
'CHAIR' => kolabformat::Chair,
);
protected $rrule_type_map = array(
'MINUTELY' => RecurrenceRule::Minutely,
'HOURLY' => RecurrenceRule::Hourly,
'DAILY' => RecurrenceRule::Daily,
'WEEKLY' => RecurrenceRule::Weekly,
'MONTHLY' => RecurrenceRule::Monthly,
'YEARLY' => RecurrenceRule::Yearly,
);
protected $weekday_map = array(
'MO' => kolabformat::Monday,
'TU' => kolabformat::Tuesday,
'WE' => kolabformat::Wednesday,
'TH' => kolabformat::Thursday,
'FR' => kolabformat::Friday,
'SA' => kolabformat::Saturday,
'SU' => kolabformat::Sunday,
);
protected $alarm_type_map = array(
'DISPLAY' => Alarm::DisplayAlarm,
'EMAIL' => Alarm::EMailAlarm,
'AUDIO' => Alarm::AudioAlarm,
);
private $status_map = array(
'NEEDS-ACTION' => kolabformat::StatusNeedsAction,
'IN-PROCESS' => kolabformat::StatusInProcess,
'COMPLETED' => kolabformat::StatusCompleted,
'CANCELLED' => kolabformat::StatusCancelled,
);
protected $part_status_map = array(
'UNKNOWN' => kolabformat::PartNeedsAction,
'NEEDS-ACTION' => kolabformat::PartNeedsAction,
'TENTATIVE' => kolabformat::PartTentative,
'ACCEPTED' => kolabformat::PartAccepted,
'DECLINED' => kolabformat::PartDeclined,
'DELEGATED' => kolabformat::PartDelegated,
);
/**
* Convert common xcard properties into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Object data as hash array
*/
public function to_array($data = array())
{
+ // read common object props
+ $object = parent::to_array();
+
$status_map = array_flip($this->status_map);
$sensitivity_map = array_flip($this->sensitivity_map);
- $object = array(
- 'uid' => $this->obj->uid(),
- 'created' => self::php_datetime($this->obj->created()),
- 'changed' => self::php_datetime($this->obj->lastModified()),
+ $object += array(
'sequence' => intval($this->obj->sequence()),
'title' => $this->obj->summary(),
'location' => $this->obj->location(),
'description' => $this->obj->description(),
'status' => $this->status_map[$this->obj->status()],
'sensitivity' => $sensitivity_map[$this->obj->classification()],
'priority' => $this->obj->priority(),
'categories' => self::vector2array($this->obj->categories()),
'start' => self::php_datetime($this->obj->start()),
);
// read organizer and attendees
- if ($organizer = $this->obj->organizer()) {
+ if (($organizer = $this->obj->organizer()) && ($organizer->email() || $organizer->name())) {
$object['organizer'] = array(
'email' => $organizer->email(),
'name' => $organizer->name(),
);
}
$role_map = array_flip($this->role_map);
$part_status_map = array_flip($this->part_status_map);
$attvec = $this->obj->attendees();
for ($i=0; $i < $attvec->size(); $i++) {
$attendee = $attvec->get($i);
$cr = $attendee->contact();
- $object['attendees'][] = array(
- 'role' => $role_map[$attendee->role()],
- 'status' => $part_status_map[$attendee->partStat()],
- 'rsvp' => $attendee->rsvp(),
- 'email' => $cr->email(),
- 'name' => $cr->name(),
- );
+ if ($cr->email() != $object['organizer']['email']) {
+ $object['attendees'][] = array(
+ 'role' => $role_map[$attendee->role()],
+ 'status' => $part_status_map[$attendee->partStat()],
+ 'rsvp' => $attendee->rsvp(),
+ 'email' => $cr->email(),
+ 'name' => $cr->name(),
+ );
+ }
}
// read recurrence rule
if (($rr = $this->obj->recurrenceRule()) && $rr->isValid()) {
$rrule_type_map = array_flip($this->rrule_type_map);
$object['recurrence'] = array('FREQ' => $rrule_type_map[$rr->frequency()]);
if ($intvl = $rr->interval())
$object['recurrence']['INTERVAL'] = $intvl;
if (($count = $rr->count()) && $count > 0) {
$object['recurrence']['COUNT'] = $count;
}
else if ($until = self::php_datetime($rr->end())) {
$until->setTime($object['start']->format('G'), $object['start']->format('i'), 0);
$object['recurrence']['UNTIL'] = $until;
}
if (($byday = $rr->byday()) && $byday->size()) {
$weekday_map = array_flip($this->weekday_map);
$weekdays = array();
for ($i=0; $i < $byday->size(); $i++) {
$daypos = $byday->get($i);
$prefix = $daypos->occurence();
$weekdays[] = ($prefix ? $prefix : '') . $weekday_map[$daypos->weekday()];
}
$object['recurrence']['BYDAY'] = join(',', $weekdays);
}
if (($bymday = $rr->bymonthday()) && $bymday->size()) {
$object['recurrence']['BYMONTHDAY'] = join(',', self::vector2array($bymday));
}
if (($bymonth = $rr->bymonth()) && $bymonth->size()) {
$object['recurrence']['BYMONTH'] = join(',', self::vector2array($bymonth));
}
- if ($exceptions = $this->obj->exceptionDates()) {
- for ($i=0; $i < $exceptions->size(); $i++) {
- if ($exdate = self::php_datetime($exceptions->get($i)))
+ if ($exdates = $this->obj->exceptionDates()) {
+ for ($i=0; $i < $exdates->size(); $i++) {
+ if ($exdate = self::php_datetime($exdates->get($i)))
$object['recurrence']['EXDATE'][] = $exdate;
}
}
}
// read alarm
$valarms = $this->obj->alarms();
$alarm_types = array_flip($this->alarm_type_map);
for ($i=0; $i < $valarms->size(); $i++) {
$alarm = $valarms->get($i);
$type = $alarm_types[$alarm->type()];
if ($type == 'DISPLAY' || $type == 'EMAIL') { // only DISPLAY and EMAIL alarms are supported
if ($start = self::php_datetime($alarm->start())) {
$object['alarms'] = '@' . $start->format('U');
}
else if ($offset = $alarm->relativeStart()) {
$value = $alarm->relativeTo() == kolabformat::End ? '+' : '-';
if ($w = $offset->weeks()) $value .= $w . 'W';
else if ($d = $offset->days()) $value .= $d . 'D';
else if ($h = $offset->hours()) $value .= $h . 'H';
else if ($m = $offset->minutes()) $value .= $m . 'M';
else if ($s = $offset->seconds()) $value .= $s . 'S';
else continue;
$object['alarms'] = $value;
}
$object['alarms'] .= ':' . $type;
break;
}
}
return $object;
}
/**
* Set common xcal properties to the kolabformat object
*
* @param array Event data as hash array
*/
public function set(&$object)
{
- $is_new = !$this->obj->uid();
+ $this->init();
- // set some automatic values if missing
- if (!$this->obj->created()) {
- if (!empty($object['created']))
- $object['created'] = new DateTime('now', self::$timezone);
- $this->obj->setCreated(self::get_datetime($object['created']));
- }
-
- if (!empty($object['uid']))
- $this->obj->setUid($object['uid']);
+ $is_new = !$this->obj->uid();
- $object['changed'] = new DateTime('now', self::$timezone);
- $this->obj->setLastModified(self::get_datetime($object['changed'], new DateTimeZone('UTC')));
+ // set common object properties
+ parent::set($object);
// increment sequence on updates
$object['sequence'] = !$is_new ? $this->obj->sequence()+1 : 0;
$this->obj->setSequence($object['sequence']);
$this->obj->setSummary($object['title']);
$this->obj->setLocation($object['location']);
$this->obj->setDescription($object['description']);
$this->obj->setPriority($object['priority']);
$this->obj->setClassification($this->sensitivity_map[$object['sensitivity']]);
$this->obj->setCategories(self::array2vector($object['categories']));
// process event attendees
$attendees = new vectorattendee;
foreach ((array)$object['attendees'] as $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
$object['organizer'] = $attendee;
}
- else {
+ else if ($attendee['email'] != $object['organizer']['email']) {
$cr = new ContactReference(ContactReference::EmailReference, $attendee['email']);
$cr->setName($attendee['name']);
$att = new Attendee;
$att->setContact($cr);
- $att->setPartStat($this->status_map[$attendee['status']]);
+ $att->setPartStat($this->part_status_map[$attendee['status']]);
$att->setRole($this->role_map[$attendee['role']] ? $this->role_map[$attendee['role']] : kolabformat::Required);
$att->setRSVP((bool)$attendee['rsvp']);
if ($att->isValid()) {
$attendees->push($att);
}
else {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Invalid event attendee: " . json_encode($attendee),
), true);
}
}
}
$this->obj->setAttendees($attendees);
if ($object['organizer']) {
$organizer = new ContactReference(ContactReference::EmailReference, $object['organizer']['email']);
$organizer->setName($object['organizer']['name']);
$this->obj->setOrganizer($organizer);
}
// save recurrence rule
if ($object['recurrence']) {
$rr = new RecurrenceRule;
$rr->setFrequency($this->rrule_type_map[$object['recurrence']['FREQ']]);
if ($object['recurrence']['INTERVAL'])
$rr->setInterval(intval($object['recurrence']['INTERVAL']));
if ($object['recurrence']['BYDAY']) {
$byday = new vectordaypos;
foreach (explode(',', $object['recurrence']['BYDAY']) as $day) {
$occurrence = 0;
if (preg_match('/^([\d-]+)([A-Z]+)$/', $day, $m)) {
$occurrence = intval($m[1]);
$day = $m[2];
}
if (isset($this->weekday_map[$day]))
$byday->push(new DayPos($occurrence, $this->weekday_map[$day]));
}
$rr->setByday($byday);
}
if ($object['recurrence']['BYMONTHDAY']) {
$bymday = new vectori;
foreach (explode(',', $object['recurrence']['BYMONTHDAY']) as $day)
$bymday->push(intval($day));
$rr->setBymonthday($bymday);
}
if ($object['recurrence']['BYMONTH']) {
$bymonth = new vectori;
foreach (explode(',', $object['recurrence']['BYMONTH']) as $month)
$bymonth->push(intval($month));
$rr->setBymonth($bymonth);
}
if ($object['recurrence']['COUNT'])
$rr->setCount(intval($object['recurrence']['COUNT']));
else if ($object['recurrence']['UNTIL'])
$rr->setEnd(self::get_datetime($object['recurrence']['UNTIL'], null, true));
if ($rr->isValid()) {
$this->obj->setRecurrenceRule($rr);
// add exception dates (only if recurrence rule is valid)
$exdates = new vectordatetime;
foreach ((array)$object['recurrence']['EXDATE'] as $exdate)
$exdates->push(self::get_datetime($exdate, null, true));
$this->obj->setExceptionDates($exdates);
}
else {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Invalid event recurrence rule: " . json_encode($object['recurrence']),
), true);
}
}
// save alarm
$valarms = new vectoralarm;
if ($object['alarms']) {
list($offset, $type) = explode(":", $object['alarms']);
if ($type == 'EMAIL') { // email alarms implicitly go to event owner
$recipients = new vectorcontactref;
$recipients->push(new ContactReference(ContactReference::EmailReference, $object['_owner']));
$alarm = new Alarm($object['title'], strval($object['description']), $recipients);
}
else { // default: display alarm
$alarm = new Alarm($object['title']);
}
if (preg_match('/^@(\d+)/', $offset, $d)) {
$alarm->setStart(self::get_datetime($d[1], new DateTimeZone('UTC')));
}
else if (preg_match('/^([-+]?)(\d+)([SMHDW])/', $offset, $d)) {
$days = $hours = $minutes = $seconds = 0;
switch ($d[3]) {
case 'W': $days = 7*intval($d[2]); break;
case 'D': $days = intval($d[2]); break;
case 'H': $hours = intval($d[2]); break;
case 'M': $minutes = intval($d[2]); break;
case 'S': $seconds = intval($d[2]); break;
}
$alarm->setRelativeStart(new Duration($days, $hours, $minutes, $seconds, $d[1] == '-'), $d[1] == '-' ? kolabformat::Start : kolabformat::End);
}
$valarms->push($alarm);
}
$this->obj->setAlarms($valarms);
}
/**
* Callback for kolab_storage_cache to get words to index for fulltext search
*
* @return array List of words to save in cache
*/
public function get_words()
{
$data = '';
foreach (self::$fulltext_cols as $colname) {
list($col, $field) = explode(':', $colname);
if ($field) {
$a = array();
foreach ((array)$this->data[$col] as $attr)
$a[] = $attr[$field];
$val = join(' ', $a);
}
else {
$val = is_array($this->data[$col]) ? join(' ', $this->data[$col]) : $this->data[$col];
}
if (strlen($val))
$data .= $val . ' ';
}
return array_unique(rcube_utils::normalize_string($data, true));
}
}
\ No newline at end of file
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_storage.php b/lib/kolab/plugins/libkolab/lib/kolab_storage.php
index ebbadf2..7d5e1c6 100644
--- a/lib/kolab/plugins/libkolab/lib/kolab_storage.php
+++ b/lib/kolab/plugins/libkolab/lib/kolab_storage.php
@@ -1,835 +1,847 @@
<?php
/**
* Kolab storage class providing static methods to access groupware objects on a Kolab server.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_storage
{
const CTYPE_KEY = '/shared/vendor/kolab/folder-type';
const CTYPE_KEY_PRIVATE = '/private/vendor/kolab/folder-type';
- const COLOR_KEY_SHARED = '/shared/vendor/kolab/color';
+ const COLOR_KEY_SHARED = '/shared/vendor/kolab/color';
const COLOR_KEY_PRIVATE = '/private/vendor/kolab/color';
+ const NAME_KEY_SHARED = '/shared/vendor/kolab/name';
+ const NAME_KEY_PRIVATE = '/private/vendor/kolab/name';
public static $version = '3.0';
public static $last_error;
private static $ready = false;
private static $subscriptions;
private static $states;
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::$version = strval($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);
}
else if (!class_exists('kolabformat')) {
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'message' => "required kolabformat module not found"
), true);
}
else {
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'message' => "IMAP server doesn't support METADATA or ANNOTATEMORE"
), true);
}
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, null, $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
* @param bool $active Sets folder state (client-side subscription)
*
* @return bool True on success, false on failure
*/
public static function folder_create($name, $type = null, $subscribed = false, $active = 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);
}
// activate folder
else if ($active) {
self::set_state($name, true);
}
}
}
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
* - subscribed: Subscribed flag (IMAP subscription)
* - active: Activation flag (client-side subscription)
* @return mixed New folder name or False on failure
*/
public static function folder_update(&$prop)
{
self::setup();
$folder = rcube_charset::convert($prop['name'], RCUBE_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'], $prop['active']);
}
- // save color in METADATA
+ // save displayname and 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
+ if ($result) {
+ $ns = null;
+ foreach (array('color' => array(self::COLOR_KEY_SHARED,self::COLOR_KEY_PRIVATE),
+ 'displayname' => array(self::NAME_KEY_SHARED,self::NAME_KEY_PRIVATE)) as $key => $metakeys) {
+ if (!empty($prop[$key])) {
+ if (!isset($ns))
+ $ns = self::$imap->folder_namespace($folder);
+
+ $meta_saved = false;
+ if ($ns == 'personal') // save in shared namespace for personal folders
+ $meta_saved = self::$imap->set_metadata($folder, array($metakeys[0] => $prop[$key]));
+ if (!$meta_saved) // try in private namespace
+ $meta_saved = self::$imap->set_metadata($folder, array($metakeys[1] => $prop[$key]));
+ if ($meta_saved)
+ unset($prop[$key]); // 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();
+ // find custom display name in folder METADATA
+ $metadata = self::$imap->get_metadata($folder, array(self::NAME_KEY_PRIVATE, self::NAME_KEY_SHARED));
+ if (($name = $metadata[$folder][self::NAME_KEY_PRIVATE]) || ($name = $metadata[$folder][self::NAME_KEY_SHARED])) {
+ return $name;
+ }
+
$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');
+ $names[$name] = self::object_name($name);
}
// Make sure parent folder is listed (might be skipped e.g. if it's namespace root)
if ($p_len && !isset($names[$parent])) {
- $names[$parent] = rcube_charset::convert($parent, 'UTF7-IMAP');
+ $names[$parent] = self::object_name($parent);
}
// 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);
+ $listnames = array();
+ foreach (array_keys($names) as $imap_name) {
+ $name = $origname = $names[$imap_name];
// find folder prefix to truncate
- for ($i = count($names)-1; $i >= 0; $i--) {
- if (strpos($name, $names[$i].' » ') === 0) {
- $length = strlen($names[$i].' » ');
+ for ($i = count($listnames)-1; $i >= 0; $i--) {
+ if (strpos($name, $listnames[$i].' » ') === 0) {
+ $length = strlen($listnames[$i].' » ');
$prefix = substr($name, 0, $length);
$count = count(explode(' » ', $prefix));
$name = str_repeat(' ', $count-1) . '» ' . substr($name, $length);
break;
}
}
- $names[] = $origname;
+ $listnames[] = $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 boolean Enable to return subscribed folders only (null to use configured subscription mode)
* @param array Will be filled with folder-types data
*
* @return array List of folders
*/
public static function list_folders($root = '', $mbox = '*', $filter = null, $subscribed = null, &$folderdata = array())
{
if (!self::setup()) {
return null;
}
// use IMAP subscriptions
if ($subscribed === null && self::$config->get('kolab_use_subscriptions')) {
$subscribed = true;
}
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 && $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;
}
/**
* Check subscription status of this folder
*
* @param string $folder Folder name
*
* @return boolean True if subscribed, false if not
*/
public static function folder_is_subscribed($folder)
{
if (self::$subscriptions === null) {
self::setup();
self::$subscriptions = self::$imap->list_folders_subscribed();
}
return in_array($folder, self::$subscriptions);
}
/**
* Change subscription status of this folder
*
* @param string $folder Folder name
*
* @return True on success, false on error
*/
public static function folder_subscribe($folder)
{
self::setup();
if (self::$imap->subscribe($folder)) {
self::$subscriptions === null;
return true;
}
return false;
}
/**
* Change subscription status of this folder
*
* @param string $folder Folder name
*
* @return True on success, false on error
*/
public static function folder_unsubscribe($folder)
{
self::setup();
if (self::$imap->unsubscribe($folder)) {
self::$subscriptions === null;
return true;
}
return false;
}
/**
* Check activation status of this folder
*
* @param string $folder Folder name
*
* @return boolean True if active, false if not
*/
public static function folder_is_active($folder)
{
$active_folders = self::get_states();
return in_array($folder, $active_folders);
}
/**
* Change activation status of this folder
*
* @param string $folder Folder name
*
* @return True on success, false on error
*/
public static function folder_activate($folder)
{
return self::set_state($folder, true);
}
/**
* Change activation status of this folder
*
* @param string $folder Folder name
*
* @return True on success, false on error
*/
public static function folder_deactivate($folder)
{
return self::set_state($folder, false);
}
/**
* Return list of active folders
*/
private static function get_states()
{
if (self::$states !== null) {
return self::$states;
}
$rcube = rcube::get_instance();
$folders = $rcube->config->get('kolab_active_folders');
if ($folders !== null) {
self::$states = !empty($folders) ? explode('**', $folders) : array();
}
// for backward-compatibility copy server-side subscriptions to activation states
else {
self::setup();
if (self::$subscriptions === null) {
self::$subscriptions = self::$imap->list_folders_subscribed();
}
self::$states = self::$subscriptions;
$folders = implode(self::$states, '**');
$rcube->user->save_prefs(array('kolab_active_folders' => $folders));
}
return self::$states;
}
/**
* Update list of active folders
*/
private static function set_state($folder, $state)
{
self::get_states();
// update in-memory list
$idx = array_search($folder, self::$states);
if ($state && $idx === false) {
self::$states[] = $folder;
}
else if (!$state && $idx !== false) {
unset(self::$states[$idx]);
}
// update user preferences
$folders = implode(self::$states, '**');
$rcube = rcube::get_instance();
return $rcube->user->save_prefs(array('kolab_active_folders' => $folders));
}
}
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_storage_cache.php b/lib/kolab/plugins/libkolab/lib/kolab_storage_cache.php
index 6f6e16e..ef4dd22 100644
--- a/lib/kolab/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/lib/kolab/plugins/libkolab/lib/kolab_storage_cache.php
@@ -1,738 +1,739 @@
<?php
/**
* Kolab storage cache class providing a local caching layer for Kolab groupware objects.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_storage_cache
{
private $db;
private $imap;
private $folder;
private $uid2msg;
private $objects;
private $index = array();
private $resource_uri;
private $enabled = true;
private $synched = false;
private $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('<colname>', '<comparator>', '<value>')
* @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
}
// We don't want to cache big results in-memory, however
// if we select only one object here, there's a big chance we will need it later
if (!$uids && count($result) == 1) {
if ($msguid = $result[0]['_msguid']) {
$this->uid2msg[$result[0]['uid']] = $msguid;
$this->objects[$msguid] = $result[0];
}
}
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'] && $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('!(</?[a-z0-9:-]+>)[\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['_size'] = strlen($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 ' . rcube_imap_generic::escape($uid));
$results = $index->get();
$this->uid2msg[$uid] = $results[0];
}
return $this->uid2msg[$uid];
}
}
diff --git a/lib/kolab/plugins/libkolab/lib/kolab_storage_folder.php b/lib/kolab/plugins/libkolab/lib/kolab_storage_folder.php
index a671faf..fe17d63 100644
--- a/lib/kolab/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/lib/kolab/plugins/libkolab/lib/kolab_storage_folder.php
@@ -1,946 +1,1071 @@
<?php
/**
* The kolab_storage_folder class represents an IMAP folder on the Kolab server.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_storage_folder
{
/**
* 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 $namespace;
private $imap;
private $info;
+ private $idata;
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;
}
+ /**
+ * Make IMAP folder data available for this folder
+ */
+ public function get_imap_data()
+ {
+ if (!isset($this->idata))
+ $this->idata = $this->imap->folder_data($this->name);
+
+ return $this->idata;
+ }
/**
* 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);
+ if (!isset($this->namespace))
+ $this->namespace = $this->imap->folder_namespace($this->name);
+ return $this->namespace;
}
/**
* 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);
}
/**
- * Get the color value stores in metadata
+ * Get the display name value of this folder
+ *
+ * @return string Folder name
+ */
+ public function get_name()
+ {
+ return kolab_storage::object_name($this->name, $this->namespace);
+ }
+
+
+ /**
+ * Get the color value stored in metadata
*
* @param string Default color value to return if not set
* @return mixed Color value from IMAP metadata or $default is not set
*/
public function get_color($default = null)
{
// color is defined in folder METADATA
$metadata = $this->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;
}
return $default;
}
/**
* 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 activation status of this folder
*
* @return boolean True if enabled, false if not
*/
public function is_active()
{
return kolab_storage::folder_is_active($this->name);
}
/**
* Change activation status of this folder
*
* @param boolean The desired subscription status: true = active, false = not active
*
* @return True on success, false on error
*/
public function activate($active)
{
return $active ? kolab_storage::folder_activate($this->name) : kolab_storage::folder_deactivate($this->name);
}
/**
* Check subscription status of this folder
*
* @return boolean True if subscribed, false if not
*/
public function is_subscribed()
{
return kolab_storage::folder_is_subscribed($this->name);
}
/**
* Change subscription status of this folder
*
* @param boolean The desired subscription status: true = subscribed, false = not subscribed
*
* @return True on success, false on error
*/
public function subscribe($subscribed)
{
return $subscribed ? kolab_storage::folder_subscribe($this->name) : kolab_storage::folder_unsubscribe($this->name);
}
/**
* 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('<colname>', '<comparator>', '<value>')
* @return array List of Kolab data objects (each represented as hash array)
*/
public function select($query = array())
{
// check query argument
if (empty($query))
return $this->get_objects();
// 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
* @param bool True to print the part content
* @param resource File pointer to save the message part
* @param boolean Disables charset conversion
*
* @return mixed The attachment content as binary string
*/
public function get_attachment($uid, $part, $mailbox = null, $print = false, $fp = null, $skip_charset_conv = false)
{
if ($msguid = ($mailbox ? $uid : $this->cache->uid2msguid($uid))) {
$this->imap->set_folder($mailbox ? $mailbox : $this->name);
return $this->imap->get_message_part($msguid, $part, null, $print, $fp, $skip_charset_conv);
}
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 = null;
// Message doesn't exist?
if (empty($headers)) {
return false;
}
+ // extract the X-Kolab-Type header from the XML attachment part if missing
+ if (empty($headers->others['x-kolab-type'])) {
+ $message = new rcube_message($msguid);
+ foreach ((array)$message->attachments as $part) {
+ if (strpos($part->mimetype, kolab_format::KTYPE_PREFIX) === 0) {
+ $headers->others['x-kolab-type'] = $part->mimetype;
+ break;
+ }
+ }
+ }
+ // fix buggy messages stating the X-Kolab-Type header twice
+ else if (is_array($headers->others['x-kolab-type'])) {
+ $headers->others['x-kolab-type'] = reset($headers->others['x-kolab-type']);
+ }
+
+ // no object type header found: abort
+ if (empty($headers->others['x-kolab-type'])) {
+ rcube::raise_error(array(
+ 'code' => 600,
+ 'type' => 'php',
+ 'file' => __FILE__,
+ 'line' => __LINE__,
+ 'message' => "No X-Kolab-Type information found in message $msguid ($this->name).",
+ ), true);
+ 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);
+ if (!$message) $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;
}
// check kolab format 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)
- $format_version = 2.0;
+ $format_version = '2.0';
else
- $format_version = 3.0; // assume 3.0
+ $format_version = '3.0'; // assume 3.0
}
// get Kolab format handler for the given type
$format = kolab_format::factory($object_type, $format_version);
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(array('_attachments' => $attachments));
$object['_type'] = $object_type;
$object['_msguid'] = $msguid;
$object['_mailbox'] = $this->name;
$object['_formatobj'] = $format;
return $object;
}
else {
// try to extract object UID from XML block
if (preg_match('!<uid>(.+)</uid>!Uims', $xml, $m))
$msgadd = " UID = " . trim(strip_tags($m[1]));
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 ($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]);
}
}
}
// 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]);
}
}
}
+ // save recurrence exceptions as individual objects due to lack of support in Kolab v2 format
+ if (kolab_storage::$version == '2.0' && $object['recurrence']['EXCEPTIONS']) {
+ $this->save_recurrence_exceptions($object, $type);
+ }
+
// check IMAP BINARY extension support for 'file' objects
// allow configuration to workaround bug in Cyrus < 2.4.17
$rcmail = rcube::get_instance();
$binary = $type == 'file' && !$rcmail->config->get('kolab_binary_disable') && $this->imap->get_capability('BINARY');
// generate and save object message
if ($raw_msg = $this->build_message($object, $type, $binary)) {
+ // resolve old msguid before saving
+ if ($uid && empty($object['_msguid']) && ($msguid = $this->cache->uid2msguid($uid))) {
+ $object['_msguid'] = $msguid;
+ $object['_mailbox'] = $this->name;
+ }
+
if (is_array($raw_msg)) {
$result = $this->imap->save_message($this->name, $raw_msg[0], $raw_msg[1], true, null, null, $binary);
@unlink($raw_msg[0]);
}
else {
$result = $this->imap->save_message($this->name, $raw_msg, null, false, null, null, $binary);
}
// 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;
}
+ /**
+ * Save recurrence exceptions as individual objects.
+ * The Kolab v2 format doesn't allow us to save fully embedded exception objects.
+ *
+ * @param array Hash array with event properties
+ * @param string Object type
+ */
+ private function save_recurrence_exceptions(&$object, $type = null)
+ {
+ if ($object['recurrence']['EXCEPTIONS']) {
+ $exdates = array();
+ foreach ((array)$object['recurrence']['EXDATE'] as $exdate) {
+ $key = is_a($exdate, 'DateTime') ? $exdate->format('Y-m-d') : strval($exdate);
+ $exdates[$key] = 1;
+ }
+
+ // save every exception as individual object
+ foreach((array)$object['recurrence']['EXCEPTIONS'] as $exception) {
+ $exception['uid'] = self::recurrence_exception_uid($object['uid'], $exception['start']->format('Ymd'));
+ $exception['sequence'] = $object['sequence'] + 1;
+
+ if ($exception['thisandfuture']) {
+ $exception['recurrence'] = $object['recurrence'];
+
+ // adjust the recurrence duration of the exception
+ if ($object['recurrence']['COUNT']) {
+ $recurrence = new kolab_date_recurrence($object['_formatobj']);
+ if ($end = $recurrence->end()) {
+ unset($exception['recurrence']['COUNT']);
+ $exception['recurrence']['UNTIL'] = new DateTime('@'.$end);
+ }
+ }
+
+ // set UNTIL date if we have a thisandfuture exception
+ $untildate = clone $exception['start'];
+ $untildate->sub(new DateInterval('P1D'));
+ $object['recurrence']['UNTIL'] = $untildate;
+ unset($object['recurrence']['COUNT']);
+ }
+ else {
+ if (!$exdates[$exception['start']->format('Y-m-d')])
+ $object['recurrence']['EXDATE'][] = clone $exception['start'];
+ unset($exception['recurrence']);
+ }
+
+ unset($exception['recurrence']['EXCEPTIONS'], $exception['_formatobj'], $exception['_msguid']);
+ $this->save($exception, $type, $exception['uid']);
+ }
+
+ unset($object['recurrence']['EXCEPTIONS']);
+ }
+ }
+
+ /**
+ * Generate an object UID with the given recurrence-ID in a way that it is
+ * unique (the original UID is not a substring) but still recoverable.
+ */
+ private static function recurrence_exception_uid($uid, $recurrence_id)
+ {
+ $offset = -2;
+ return substr($uid, 0, $offset) . '-' . $recurrence_id . '-' . substr($uid, $offset);
+ }
/**
* 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
*
* @return mixed Message as string or array with two elements
* (one for message file path, second for message headers)
*/
private function build_message(&$object, $type, $binary)
{
// 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, kolab_storage::$version);
if (PEAR::isError($format))
return false;
$format->set($object);
$xml = $format->write(kolab_storage::$version);
$object['uid'] = $format->uid; // read UID from format
$object['_formatobj'] = $format;
if (empty($xml) || !$format->is_valid() || empty($object['uid'])) {
return false;
}
$mime = new Mail_mime("\r\n");
$rcmail = rcube::get_instance();
$headers = array();
$part_id = 1;
$encoding = $binary ? 'binary' : 'base64';
- if ($ident = $rcmail->user->get_identity()) {
- $headers['From'] = $ident['email'];
- $headers['To'] = $ident['email'];
+ if ($user_email = $rcmail->get_user_email()) {
+ $headers['From'] = $user_email;
+ $headers['To'] = $user_email;
}
$headers['Date'] = date('r');
$headers['X-Kolab-Type'] = kolab_format::KTYPE_PREFIX . $type;
$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');
// Check if we have enough memory to handle the message in it
// It's faster than using files, so we'll do this if we only can
if (!empty($object['_attachments']) && ($mem_limit = parse_bytes(ini_get('memory_limit'))) > 0) {
$memory = function_exists('memory_get_usage') ? memory_get_usage() : 16*1024*1024; // safe value: 16MB
foreach ($object['_attachments'] as $id => $attachment) {
$memory += $attachment['size'];
}
// 1.33 is for base64, we need at least 2x more memory than the message size
if ($memory * ($binary ? 1 : 1.33) * 2 > $mem_limit) {
$is_file = true;
$temp_dir = unslashify($rcmail->config->get('temp_dir'));
$mime->setParam('delay_file_io', true);
}
}
$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;
// Convert new lines to \r\n, to wrokaround "NO Message contains bare newlines"
// when APPENDing from temp file
$xml = preg_replace('/\r?\n/', "\r\n", $xml);
$mime->addAttachment($xml, // file
$ctype, // content-type
'kolab.xml', // filename
false, // is_file
'8bit', // encoding
'attachment', // disposition
- RCUBE_CHARSET // charset
+ RCUBE_CHARSET // charset
);
$part_id++;
// save object attachments as separate parts
foreach ((array)$object['_attachments'] as $key => $att) {
if (empty($att['content']) && !empty($att['id'])) {
$msguid = !empty($object['_msguid']) ? $object['_msguid'] : $object['uid'];
if ($is_file) {
$att['path'] = tempnam($temp_dir, 'rcmAttmnt');
if (($fp = fopen($att['path'], 'w')) && $this->get_attachment($msguid, $att['id'], $object['_mailbox'], false, $fp)) {
fclose($fp);
}
else {
return false;
}
}
else {
$att['content'] = $this->get_attachment($msguid, $att['id'], $object['_mailbox']);
}
}
$headers = array('Content-ID' => Mail_mimePart::encodeHeader('Content-ID', '<' . $key . '>', RCUBE_CHARSET, 'quoted-printable'));
$name = !empty($att['name']) ? $att['name'] : $key;
if (!empty($att['content'])) {
$mime->addAttachment($att['content'], $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
$part_id++;
}
else if (!empty($att['path'])) {
$mime->addAttachment($att['path'], $att['mimetype'], $name, true, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
$part_id++;
}
$object['_attachments'][$key]['id'] = $part_id;
}
if ($is_file) {
// use common temp dir
$body_file = tempnam($temp_dir, 'rcmMsg');
if (PEAR::isError($mime_result = $mime->saveMessageBody($body_file))) {
self::raise_error(array('code' => 650, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Could not create message: ".$mime_result->getMessage()),
true, false);
return false;
}
return array($body_file, $mime->txtHeaders());
}
else {
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)),
+ sprintf('%s/trigger/%s/%s.pfb',
+ kolab_storage::get_freebusy_server(),
+ urlencode($owner),
+ urlencode($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;
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Tue, Jun 10, 6:41 AM (1 d, 10 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
196983
Default Alt Text
(174 KB)
Attached To
Mode
R26 chwala
Attached
Detach File
Event Timeline
Log In to Comment