Page MenuHomePhorge

No OneTemporary

Size
69 KB
Referenced Files
None
Subscribers
None
diff --git a/plugins/libkolab/lib/kolab_format.php b/plugins/libkolab/lib/kolab_format.php
index d00d5ee4..814800d2 100644
--- a/plugins/libkolab/lib/kolab_format.php
+++ b/plugins/libkolab/lib/kolab_format.php
@@ -1,774 +1,775 @@
<?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 $formaterror;
protected $loaded = false;
protected $version = '3.0';
const KTYPE_PREFIX = 'application/x-vnd.kolab.';
const PRODUCT_ID = 'Roundcube-libkolab-1.1';
// mapping table for valid PHP timezones not supported by libkolabxml
// basically the entire list of ftp://ftp.iana.org/tz/data/backward
protected static $timezone_map = array(
'Africa/Asmera' => 'Africa/Asmara',
'Africa/Timbuktu' => 'Africa/Abidjan',
'America/Argentina/ComodRivadavia' => 'America/Argentina/Catamarca',
'America/Atka' => 'America/Adak',
'America/Buenos_Aires' => 'America/Argentina/Buenos_Aires',
'America/Catamarca' => 'America/Argentina/Catamarca',
'America/Coral_Harbour' => 'America/Atikokan',
'America/Cordoba' => 'America/Argentina/Cordoba',
'America/Ensenada' => 'America/Tijuana',
'America/Fort_Wayne' => 'America/Indiana/Indianapolis',
'America/Indianapolis' => 'America/Indiana/Indianapolis',
'America/Jujuy' => 'America/Argentina/Jujuy',
'America/Knox_IN' => 'America/Indiana/Knox',
'America/Louisville' => 'America/Kentucky/Louisville',
'America/Mendoza' => 'America/Argentina/Mendoza',
'America/Porto_Acre' => 'America/Rio_Branco',
'America/Rosario' => 'America/Argentina/Cordoba',
'America/Virgin' => 'America/Port_of_Spain',
'Asia/Ashkhabad' => 'Asia/Ashgabat',
'Asia/Calcutta' => 'Asia/Kolkata',
'Asia/Chungking' => 'Asia/Shanghai',
'Asia/Dacca' => 'Asia/Dhaka',
'Asia/Katmandu' => 'Asia/Kathmandu',
'Asia/Macao' => 'Asia/Macau',
'Asia/Saigon' => 'Asia/Ho_Chi_Minh',
'Asia/Tel_Aviv' => 'Asia/Jerusalem',
'Asia/Thimbu' => 'Asia/Thimphu',
'Asia/Ujung_Pandang' => 'Asia/Makassar',
'Asia/Ulan_Bator' => 'Asia/Ulaanbaatar',
'Atlantic/Faeroe' => 'Atlantic/Faroe',
'Atlantic/Jan_Mayen' => 'Europe/Oslo',
'Australia/ACT' => 'Australia/Sydney',
'Australia/Canberra' => 'Australia/Sydney',
'Australia/LHI' => 'Australia/Lord_Howe',
'Australia/NSW' => 'Australia/Sydney',
'Australia/North' => 'Australia/Darwin',
'Australia/Queensland' => 'Australia/Brisbane',
'Australia/South' => 'Australia/Adelaide',
'Australia/Tasmania' => 'Australia/Hobart',
'Australia/Victoria' => 'Australia/Melbourne',
'Australia/West' => 'Australia/Perth',
'Australia/Yancowinna' => 'Australia/Broken_Hill',
'Brazil/Acre' => 'America/Rio_Branco',
'Brazil/DeNoronha' => 'America/Noronha',
'Brazil/East' => 'America/Sao_Paulo',
'Brazil/West' => 'America/Manaus',
'Canada/Atlantic' => 'America/Halifax',
'Canada/Central' => 'America/Winnipeg',
'Canada/East-Saskatchewan' => 'America/Regina',
'Canada/Eastern' => 'America/Toronto',
'Canada/Mountain' => 'America/Edmonton',
'Canada/Newfoundland' => 'America/St_Johns',
'Canada/Pacific' => 'America/Vancouver',
'Canada/Saskatchewan' => 'America/Regina',
'Canada/Yukon' => 'America/Whitehorse',
'Chile/Continental' => 'America/Santiago',
'Chile/EasterIsland' => 'Pacific/Easter',
'Cuba' => 'America/Havana',
'Egypt' => 'Africa/Cairo',
'Eire' => 'Europe/Dublin',
'Europe/Belfast' => 'Europe/London',
'Europe/Tiraspol' => 'Europe/Chisinau',
'GB' => 'Europe/London',
'GB-Eire' => 'Europe/London',
'Greenwich' => 'Etc/GMT',
'Hongkong' => 'Asia/Hong_Kong',
'Iceland' => 'Atlantic/Reykjavik',
'Iran' => 'Asia/Tehran',
'Israel' => 'Asia/Jerusalem',
'Jamaica' => 'America/Jamaica',
'Japan' => 'Asia/Tokyo',
'Kwajalein' => 'Pacific/Kwajalein',
'Libya' => 'Africa/Tripoli',
'Mexico/BajaNorte' => 'America/Tijuana',
'Mexico/BajaSur' => 'America/Mazatlan',
'Mexico/General' => 'America/Mexico_City',
'NZ' => 'Pacific/Auckland',
'NZ-CHAT' => 'Pacific/Chatham',
'Navajo' => 'America/Denver',
'PRC' => 'Asia/Shanghai',
'Pacific/Ponape' => 'Pacific/Pohnpei',
'Pacific/Samoa' => 'Pacific/Pago_Pago',
'Pacific/Truk' => 'Pacific/Chuuk',
'Pacific/Yap' => 'Pacific/Chuuk',
'Poland' => 'Europe/Warsaw',
'Portugal' => 'Europe/Lisbon',
'ROC' => 'Asia/Taipei',
'ROK' => 'Asia/Seoul',
'Singapore' => 'Asia/Singapore',
'Turkey' => 'Europe/Istanbul',
'UCT' => 'Etc/UCT',
'US/Alaska' => 'America/Anchorage',
'US/Aleutian' => 'America/Adak',
'US/Arizona' => 'America/Phoenix',
'US/Central' => 'America/Chicago',
'US/East-Indiana' => 'America/Indiana/Indianapolis',
'US/Eastern' => 'America/New_York',
'US/Hawaii' => 'Pacific/Honolulu',
'US/Indiana-Starke' => 'America/Indiana/Knox',
'US/Michigan' => 'America/Detroit',
'US/Mountain' => 'America/Denver',
'US/Pacific' => 'America/Los_Angeles',
'US/Samoa' => 'Pacific/Pago_Pago',
'Universal' => 'Etc/UTC',
'W-SU' => 'Europe/Moscow',
'Zulu' => 'Etc/UTC',
);
/**
* 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)
{
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')
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();
try {
// 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 = $tz ? new DateTime($datetime, $tz) : new DateTime($datetime);
}
}
catch (Exception $e) {}
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'));
// libkolabxml throws errors on some deprecated timezone names
$utc_aliases = array('UTC', 'GMT', '+00:00', 'Z', 'Etc/GMT');
if ($tz && in_array($tz->getName(), $utc_aliases)) {
$result->setUTC(true);
}
else if ($tz !== false) {
$tzid = $tz->getName();
if (array_key_exists($tzid, self::$timezone_map))
$tzid = self::$timezone_map[$tzid];
$result->setTimezone($tzid);
}
}
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 containing 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(
array('/dictionary.[a-z.]+$/', '/contact.distlist$/'),
array( 'dictionary', 'distribution-list'),
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;
$uid = is_object($this->obj) ? $this->obj->uid() : $this->data['uid'];
$log = "Warning @ $uid";
break;
default:
$ret = true;
$log = "Error";
}
if ($log && !isset($this->formaterror)) {
rcube::raise_error(array(
'code' => 660,
'type' => 'php',
'file' => __FILE__,
'line' => __LINE__,
'message' => "kolabformat $log: " . kolabformat::errorMessage(),
), true);
$this->formaterror = $ret;
}
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')
return kolabobject::KolabV2;
else
return kolabobject::KolabV3;
}
return false;
}
/**
* Determine the correct libkolab(xml) wrapper function for the given call
* depending on the available PHP modules
*/
protected function libfunc($func)
{
if (is_array($func) || strpos($func, '::'))
return $func;
else if (class_exists('kolabobject'))
return array($this->xmlobject, $func);
else
return 'kolabformat::' . $func;
}
/**
* Direct getter for object properties
*/
public function __get($var)
{
return $this->data[$var];
}
/**
* Load Kolab object data from the given XML block
*
* @param string XML data
* @return boolean True on success, False on failure
*/
public function load($xml)
{
$this->formaterror = null;
$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->formaterror = 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
*/
public function set(&$object)
{
$this->init();
if (!empty($object['uid']))
$this->obj->setUid($object['uid']);
// set some automatic values if missing
if (method_exists($this->obj, 'setCreated')) {
// Always set created date to workaround libkolabxml (>1.1.4) bug
$created = $object['created'] ?: new DateTime('now');
$created->setTimezone(new DateTimeZone('UTC')); // must be UTC
$this->obj->setCreated(self::get_datetime($created));
+ $object['created'] = $created;
}
$object['changed'] = new DateTime('now', new DateTimeZone('UTC'));
$this->obj->setLastModified(self::get_datetime($object['changed']));
// Save custom properties of the given object
if (isset($object['x-custom']) && method_exists($this->obj, 'setCustomProperties')) {
$vcustom = new vectorcs;
foreach ((array)$object['x-custom'] as $cp) {
if (is_array($cp))
$vcustom->push(new CustomProperty($cp[0], $cp[1]));
}
$this->obj->setCustomProperties($vcustom);
}
// load custom properties from XML for caching (#2238) if method exists (#3125)
else if (method_exists($this->obj, 'customProperties')) {
$object['x-custom'] = array();
$vcustom = $this->obj->customProperties();
for ($i=0; $i < $vcustom->size(); $i++) {
$cp = $vcustom->get($i);
$object['x-custom'][] = array($cp->identifier, $cp->value);
}
}
}
/**
* Convert the Kolab object into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Kolab object data as hash 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
if (method_exists($this->obj, 'customProperties')) {
$vcustom = $this->obj->customProperties();
for ($i=0; $i < $vcustom->size(); $i++) {
$cp = $vcustom->get($i);
$object['x-custom'][] = array($cp->identifier, $cp->value);
}
}
// 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 $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();
}
/**
* Utility function to extract object attachment data
*
* @param array Hash array reference to append attachment data into
*/
public function get_attachments(&$object, $all = false)
{
$this->init();
// 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();
$key = $name . (isset($object['_attachments'][$name]) ? '.'.$i : '');
$content = $attach->data();
$object['_attachments'][$key] = array(
'id' => 'i:'.$i,
'name' => $name,
'mimetype' => $attach->mimetype(),
'size' => strlen($content),
'content' => $content,
);
}
else if ($all && substr($attach->uri(), 0, 4) == 'cid:') {
$key = $attach->uri();
$object['_attachments'][$key] = array(
'id' => $key,
'name' => $attach->label(),
'mimetype' => $attach->mimetype(),
);
}
else if (in_array(substr($attach->uri(), 0, 4), array('http','imap'))) {
$object['links'][] = $attach->uri();
}
}
}
/**
* Utility function to set attachment properties to the kolabformat object
*
* @param array Object data as hash array
* @param boolean True to always overwrite attachment information
*/
protected function set_attachments($object, $write = true)
{
// 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'] ?: 'application/octet-stream');
if ($attach->isValid()) {
$vattach->push($attach);
$write = true;
}
else {
rcube::raise_error(array(
'code' => 660,
'type' => 'php',
'file' => __FILE__,
'line' => __LINE__,
'message' => "Invalid attributes for attachment $cid: " . var_export($attr, true),
), true);
}
}
foreach ((array) $object['links'] as $link) {
$attach = new Attachment;
$attach->setUri($link, 'unknown');
$vattach->push($attach);
$write = true;
}
if ($write) {
$this->obj->setAttachments($vattach);
}
}
/**
* Unified way of updating/deleting attachments of edited object
*
* @param array $object Kolab object data
* @param array $old Old version of Kolab object
*/
public static function merge_attachments(&$object, $old)
{
$object['_attachments'] = (array) $old['_attachments'];
// delete existing attachment(s)
if (!empty($object['deleted_attachments'])) {
foreach ($object['_attachments'] as $idx => $att) {
if ($object['deleted_attachments'] === true || in_array($att['id'], $object['deleted_attachments'])) {
$object['_attachments'][$idx] = false;
}
}
}
// in kolab_storage attachments are indexed by content-id
foreach ((array) $object['attachments'] as $attachment) {
$key = null;
// Roundcube ID has nothing to do with the storage ID, remove it
// for uploaded/new attachments
// FIXME: Roundcube uses 'data', kolab_format uses 'content'
if ($attachment['content'] || $attachment['path'] || $attachment['data']) {
unset($attachment['id']);
}
if ($attachment['id']) {
foreach ((array) $object['_attachments'] as $cid => $att) {
if ($att && $attachment['id'] == $att['id']) {
$key = $cid;
}
}
}
else {
// find attachment by name, so we can update it if exists
// and make sure there are no duplicates
foreach ((array) $object['_attachments'] as $cid => $att) {
if ($att && $attachment['name'] == $att['name']) {
$key = $cid;
}
}
}
if ($key && $attachment['_deleted']) {
$object['_attachments'][$key] = false;
}
// replace existing entry
else if ($key) {
$object['_attachments'][$key] = $attachment;
}
// append as new attachment
else {
$object['_attachments'][] = $attachment;
}
}
unset($object['attachments']);
unset($object['deleted_attachments']);
}
}
diff --git a/plugins/libkolab/lib/kolab_storage_cache.php b/plugins/libkolab/lib/kolab_storage_cache.php
index 3a73d982..87c0649b 100644
--- a/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/plugins/libkolab/lib/kolab_storage_cache.php
@@ -1,1237 +1,1243 @@
<?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-2013, 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
{
const DB_DATE_FORMAT = 'Y-m-d H:i:s';
const MAX_RECORDS = 500;
public $sync_complete = false;
protected $db;
protected $imap;
protected $folder;
protected $uid2msg;
protected $objects;
protected $metadata = array();
protected $folder_id;
protected $resource_uri;
protected $enabled = true;
protected $synched = false;
protected $synclock = false;
protected $ready = false;
protected $cache_table;
protected $cache_refresh = 3600;
protected $folders_table;
protected $max_sql_packet;
protected $max_sync_lock_time = 600;
protected $binary_items = array();
protected $extra_cols = array();
protected $order_by = null;
protected $limit = null;
protected $error = 0;
protected $server_timezone;
/**
* Factory constructor
*/
public static function factory(kolab_storage_folder $storage_folder)
{
$subclass = 'kolab_storage_cache_' . $storage_folder->type;
if (class_exists($subclass)) {
return new $subclass($storage_folder);
}
else {
rcube::raise_error(array(
'code' => 900,
'type' => 'php',
'message' => "No kolab_storage_cache class found for folder '$storage_folder->name' of type '$storage_folder->type'"
), true);
return new kolab_storage_cache($storage_folder);
}
}
/**
* 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);
$this->folders_table = $this->db->table_name('kolab_folders');
$this->cache_refresh = get_offset_sec($rcmail->config->get('kolab_cache_refresh', '12h'));
$this->server_timezone = new DateTimeZone(date_default_timezone_get());
if ($this->enabled) {
// always read folder cache and lock state from DB master
$this->db->set_table_dsn('kolab_folders', 'w');
// remove sync-lock on script termination
$rcmail->add_shutdown_function(array($this, '_sync_unlock'));
}
if ($storage_folder)
$this->set_folder($storage_folder);
}
/**
* Direct access to cache by folder_id
* (only for internal use)
*/
public function select_by_id($folder_id)
{
$sql_arr = $this->db->fetch_assoc($this->db->query("SELECT * FROM `{$this->folders_table}` WHERE `folder_id` = ?", $folder_id));
if ($sql_arr) {
$this->metadata = $sql_arr;
$this->folder_id = $sql_arr['folder_id'];
$this->folder = new StdClass;
$this->folder->type = $sql_arr['type'];
$this->resource_uri = $sql_arr['resource'];
$this->cache_table = $this->db->table_name('kolab_cache_' . $sql_arr['type']);
$this->ready = true;
}
}
/**
* 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->folder->valid) {
$this->ready = false;
return;
}
// compose fully qualified ressource uri for this instance
$this->resource_uri = $this->folder->get_resource_uri();
$this->cache_table = $this->db->table_name('kolab_cache_' . $this->folder->type);
$this->ready = $this->enabled && !empty($this->folder->type);
$this->folder_id = null;
}
/**
* Returns true if this cache supports query by type
*/
public function has_type_col()
{
return in_array('type', $this->extra_cols);
}
/**
* Getter for the numeric ID used in cache tables
*/
public function get_folder_id()
{
$this->_read_folder_data();
return $this->folder_id;
}
/**
* Returns code of last error
*
* @return int Error code
*/
public function get_error()
{
return $this->error;
}
/**
* 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($this->max_sync_lock_time - 60);
// get effective time limit we have for synchronization (~70% of the execution time)
$time_limit = ini_get('max_execution_time') * 0.7;
$sync_start = time();
// assume sync will be completed
$this->sync_complete = true;
if (!$this->ready) {
// kolab cache is disabled, synchronize IMAP mailbox cache only
$this->imap_mode(true);
$this->imap->folder_sync($this->folder->name);
$this->imap_mode(false);
}
else {
// read cached folder metadata
$this->_read_folder_data();
// check cache status ($this->metadata is set in _read_folder_data())
if ( empty($this->metadata['ctag']) ||
empty($this->metadata['changed']) ||
$this->metadata['objectcount'] === null ||
$this->metadata['changed'] < date(self::DB_DATE_FORMAT, time() - $this->cache_refresh) ||
$this->metadata['ctag'] != $this->folder->get_ctag() ||
intval($this->metadata['objectcount']) !== $this->count()
) {
// lock synchronization for this folder or wait if locked
$this->_sync_lock();
// disable messages cache if configured to do so
$this->imap_mode(true);
// 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, null, null, true, true);
$this->imap_mode(false);
// determine objects to fetch or to invalidate
if (!$imap_index->is_error()) {
$imap_index = $imap_index->get();
// read cache index
$sql_result = $this->db->query(
"SELECT `msguid`, `uid` FROM `{$this->cache_table}` WHERE `folder_id` = ?",
$this->folder_id
);
$old_index = array();
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$old_index[] = $sql_arr['msguid'];
}
// fetch new objects from imap
$i = 0;
foreach (array_diff($imap_index, $old_index) as $msguid) {
if ($object = $this->folder->read_object($msguid, '*')) {
$this->_extended_insert($msguid, $object);
// check time limit and abort sync if running too long
if (++$i % 50 == 0 && time() - $sync_start > $time_limit) {
$this->sync_complete = false;
break;
}
}
}
$this->_extended_insert(0, null);
// delete invalid entries from local DB
$del_index = array_diff($old_index, $imap_index);
if (!empty($del_index)) {
$quoted_ids = join(',', array_map(array($this->db, 'quote'), $del_index));
$this->db->query(
"DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `msguid` IN ($quoted_ids)",
$this->folder_id
);
}
// update ctag value (will be written to database in _sync_unlock())
if ($this->sync_complete) {
$this->metadata['ctag'] = $this->folder->get_ctag();
$this->metadata['changed'] = date(self::DB_DATE_FORMAT, time());
// remember the number of cache entries linked to this folder
$this->metadata['objectcount'] = $this->count();
}
}
// remove lock
$this->_sync_unlock();
}
}
$this->check_error();
$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) {
$success = false;
if ($targetfolder = kolab_storage::get_folder($foldername)) {
$success = $targetfolder->cache->get($msguid, $type);
$this->error = $targetfolder->cache->get_error();
}
return $success;
}
// load object if not in memory
if (!isset($this->objects[$msguid])) {
if ($this->ready) {
$this->_read_folder_data();
$sql_result = $this->db->query(
"SELECT * FROM `{$this->cache_table}` ".
"WHERE `folder_id` = ? AND `msguid` = ?",
$this->folder_id,
$msguid
);
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$this->objects = array($msguid => $this->_unserialize($sql_arr)); // store only this object in memory (#2827)
}
}
// fetch from IMAP if not present in cache
if (empty($this->objects[$msguid])) {
if ($object = $this->folder->read_object($msguid, $type ?: '*', $foldername)) {
$this->objects = array($msguid => $object);
$this->set($msguid, $object);
}
}
}
$this->check_error();
return $this->objects[$msguid];
}
/**
* Getter for a single Kolab object identified by its UID
*
* @param string $uid Object UID
*
* @return array The Kolab object represented as hash array
*/
public function get_by_uid($uid)
{
$old_order_by = $this->order_by;
$old_limit = $this->limit;
// set order to make sure we get most recent object version
// set limit to skip count query
$this->order_by = '`msguid` DESC';
$this->limit = array(1, 0);
$list = $this->select(array(array('uid', '=', $uid)));
// set the order/limit back to defined value
$this->order_by = $old_order_by;
$this->limit = $old_limit;
if (!empty($list) && !empty($list[0])) {
return $list[0];
}
}
/**
* 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) {
if ($targetfolder = kolab_storage::get_folder($foldername)) {
$targetfolder->cache->set($msguid, $object);
$this->error = $targetfolder->cache->get_error();
}
return;
}
// remove old entry
if ($this->ready) {
$this->_read_folder_data();
$this->db->query("DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `msguid` = ?",
$this->folder_id, $msguid);
}
if ($object) {
// insert new object data...
$this->save($msguid, $object);
}
else {
// ...or set in-memory cache to false
$this->objects[$msguid] = $object;
}
$this->check_error();
}
/**
* Insert (or update) a cache entry
*
* @param int Related IMAP message UID
* @param mixed Hash array with object properties to save or false to delete the cache entry
* @param int Optional old message UID (for update)
*/
public function save($msguid, $object, $olduid = null)
{
// write to cache
if ($this->ready) {
$this->_read_folder_data();
$sql_data = $this->_serialize($object);
$sql_data['folder_id'] = $this->folder_id;
$sql_data['msguid'] = $msguid;
$sql_data['uid'] = $object['uid'];
$args = array();
$cols = array('folder_id', 'msguid', 'uid', 'changed', 'data', 'xml', 'tags', 'words');
$cols = array_merge($cols, $this->extra_cols);
foreach ($cols as $idx => $col) {
$cols[$idx] = $this->db->quote_identifier($col);
$args[] = $sql_data[$col];
}
if ($olduid) {
foreach ($cols as $idx => $col) {
$cols[$idx] = "$col = ?";
}
$query = "UPDATE `{$this->cache_table}` SET " . implode(', ', $cols)
. " WHERE `folder_id` = ? AND `msguid` = ?";
$args[] = $this->folder_id;
$args[] = $olduid;
}
else {
$query = "INSERT INTO `{$this->cache_table}` (`created`, " . implode(', ', $cols)
. ") VALUES (" . $this->db->now() . str_repeat(', ?', count($cols)) . ")";
}
$result = $this->db->query($query, $args);
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 = array($msguid => $object);
$this->uid2msg = array($object['uid'] => $msguid);
$this->check_error();
}
/**
* Move an existing cache entry to a new resource
*
* @param string Entry's IMAP message UID
* @param string Entry's Object UID
* @param kolab_storage_folder Target storage folder instance
* @param string Target entry's IMAP message UID
*/
public function move($msguid, $uid, $target, $new_msguid = null)
{
if ($this->ready && $target) {
// clear cached uid mapping and force new lookup
unset($target->cache->uid2msg[$uid]);
// resolve new message UID in target folder
if (!$new_msguid) {
$new_msguid = $target->cache->uid2msguid($uid);
}
if ($new_msguid) {
$this->_read_folder_data();
$this->db->query(
"UPDATE `{$this->cache_table}` SET `folder_id` = ?, `msguid` = ? ".
"WHERE `folder_id` = ? AND `msguid` = ?",
$target->cache->get_folder_id(),
$new_msguid,
$this->folder_id,
$msguid
);
$result = $this->db->affected_rows();
}
}
if (empty($result)) {
// just clear cache entry
$this->set($msguid, false);
}
unset($this->uid2msg[$uid]);
$this->check_error();
}
/**
* Remove all objects from local cache
*/
public function purge()
{
if (!$this->ready) {
return true;
}
$this->_read_folder_data();
$result = $this->db->query(
"DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ?",
$this->folder_id
);
return $this->db->affected_rows($result);
}
/**
* Update resource URI for existing cache entries
*
* @param string Target IMAP folder to move it to
*/
public function rename($new_folder)
{
if (!$this->ready) {
return;
}
if ($target = kolab_storage::get_folder($new_folder)) {
// resolve new message UID in target folder
$this->db->query(
"UPDATE `{$this->folders_table}` SET `resource` = ? ".
"WHERE `resource` = ?",
$target->get_resource_uri(),
$this->resource_uri
);
$this->check_error();
}
else {
$this->error = kolab_storage::ERROR_IMAP_CONN;
}
}
/**
* 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 = $uids ? array() : new kolab_storage_dataset($this);
// read from local cache DB (assume it to be synchronized)
if ($this->ready) {
$this->_read_folder_data();
// fetch full object data on one query if a small result set is expected
$fetchall = !$uids && ($this->limit ? $this->limit[0] : ($count = $this->count($query))) < self::MAX_RECORDS;
// skip SELECT if we know it will return nothing
if ($count === 0) {
return $result;
}
$sql_query = "SELECT " . ($fetchall ? '*' : "`msguid` AS `_msguid`, `uid`")
. " FROM `{$this->cache_table}` WHERE `folder_id` = ?"
. $this->_sql_where($query)
. (!empty($this->order_by) ? " ORDER BY " . $this->order_by : '');
$sql_result = $this->limit ?
$this->db->limitquery($sql_query, $this->limit[1], $this->limit[0], $this->folder_id) :
$this->db->query($sql_query, $this->folder_id);
if ($this->db->is_error($sql_result)) {
if ($uids) {
return null;
}
$result->set_error(true);
return $result;
}
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 ($fetchall && ($object = $this->_unserialize($sql_arr))) {
$result[] = $object;
}
else if (!$fetchall) {
// only add msguid to dataset index
$result[] = $sql_arr;
}
}
}
// use IMAP
else {
$filter = $this->_query2assoc($query);
$this->imap_mode(true);
if ($filter['type']) {
$search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_format::KTYPE_PREFIX . $filter['type'];
$index = $this->imap->search_once($this->folder->name, $search);
}
else {
$index = $this->imap->index($this->folder->name, null, null, true, true);
}
$this->imap_mode(false);
if ($index->is_error()) {
$this->check_error();
if ($uids) {
return null;
}
$result->set_error(true);
return $result;
}
$index = $index->get();
$result = $uids ? $index : $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 = array($msguid => $result[0]);
}
}
$this->check_error();
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())
{
// read from local cache DB (assume it to be synchronized)
if ($this->ready) {
$this->_read_folder_data();
$sql_result = $this->db->query(
"SELECT COUNT(*) AS `numrows` FROM `{$this->cache_table}` ".
"WHERE `folder_id` = ?" . $this->_sql_where($query),
$this->folder_id
);
if ($this->db->is_error($sql_result)) {
return null;
}
$sql_arr = $this->db->fetch_assoc($sql_result);
$count = intval($sql_arr['numrows']);
}
// use IMAP
else {
$filter = $this->_query2assoc($query);
$this->imap_mode(true);
if ($filter['type']) {
$search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_format::KTYPE_PREFIX . $filter['type'];
$index = $this->imap->search_once($this->folder->name, $search);
}
else {
$index = $this->imap->index($this->folder->name, null, null, true, true);
}
$this->imap_mode(false);
if ($index->is_error()) {
$this->check_error();
return null;
}
// TODO: post-filter result according to query
$count = $index->count();
}
$this->check_error();
return $count;
}
/**
* Define ORDER BY clause for cache queries
*/
public function set_order_by($sortcols)
{
if (!empty($sortcols)) {
$sortcols = array_map(function($v) {
list($column, $order) = explode(' ', $v, 2);
return "`$column`" . ($order ? " $order" : '');
}, (array) $sortcols);
$this->order_by = join(', ', $sortcols);
}
else {
$this->order_by = null;
}
}
/**
* Define LIMIT clause for cache queries
*/
public function set_limit($length, $offset = 0)
{
$this->limit = array($length, $offset);
}
/**
* Helper method to compose a valid SQL query from pseudo filter triplets
*/
protected function _sql_where($query)
{
$sql_where = '';
foreach ((array) $query as $param) {
if (is_array($param[0])) {
$subq = array();
foreach ($param[0] as $q) {
$subq[] = preg_replace('/^\s*AND\s+/i', '', $this->_sql_where(array($q)));
}
if (!empty($subq)) {
$sql_where .= ' AND (' . implode($param[1] == 'OR' ? ' OR ' : ' AND ', $subq) . ')';
}
continue;
}
else 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] = ($param[1] == '!=' ? 'NOT ' : '' ) . '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
*/
protected 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
*/
protected function _fetch($index, $type = null, $folder = null)
{
$results = new kolab_storage_dataset($this);
foreach ((array)$index as $msguid) {
if ($object = $this->folder->read_object($msguid, $type, $folder)) {
$results[] = $object;
$this->set($msguid, $object);
}
}
return $results;
}
/**
* Helper method to convert the given Kolab object into a dataset to be written to cache
*/
protected function _serialize($object)
{
$sql_data = array('changed' => null, 'xml' => '', 'tags' => '', 'words' => '');
if ($object['changed']) {
$sql_data['changed'] = date(self::DB_DATE_FORMAT, 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) {
// skip empty properties
if ($val === "" || $val === null) {
continue;
}
// mark binary data to be extracted from xml on unserialize()
if (isset($this->binary_items[$key])) {
$data[$key] = true;
}
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;
}
}
}
// use base64 encoding (Bug #1912, #2662)
$sql_data['data'] = base64_encode(serialize($data));
return $sql_data;
}
/**
* Helper method to turn stored cache data into a valid storage object
*/
protected function _unserialize($sql_arr)
{
// check if data is a base64-encoded string, for backward compat.
if (strpos(substr($sql_arr['data'], 0, 64), ':') === false) {
$sql_arr['data'] = base64_decode($sql_arr['data']);
}
$object = unserialize($sql_arr['data']);
// de-serialization failed
if ($object === false) {
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'message' => "Malformed data for {$this->resource_uri}/{$sql_arr['msguid']} object."
), true);
return null;
}
// decode binary properties
foreach ($this->binary_items as $key => $regexp) {
if (!empty($object[$key]) && preg_match($regexp, $sql_arr['xml'], $m)) {
$object[$key] = base64_decode($m[1]);
}
}
$object_type = $sql_arr['type'] ?: $this->folder->type;
$format_type = $this->folder->type == 'configuration' ? 'configuration' : $object_type;
// add meta data
$object['_type'] = $object_type;
$object['_msguid'] = $sql_arr['msguid'];
$object['_mailbox'] = $this->folder->name;
$object['_size'] = strlen($sql_arr['xml']);
$object['_formatobj'] = kolab_format::factory($format_type, 3.0, $sql_arr['xml']);
+ // Fix old broken objects with missing creation date
+ if (empty($object['created']) && method_exists($object['_formatobj'], 'to_array')) {
+ $new_object = $object['_formatobj']->to_array();
+ $object['created'] = $new_object['created'];
+ }
+
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
*/
protected function _extended_insert($msguid, $object)
{
static $buffer = '';
$line = '';
if ($object) {
$sql_data = $this->_serialize($object);
// Skip multifolder insert for Oracle, we can't put long data inline
if ($this->db->db_provider == 'oracle') {
$extra_cols = '';
if ($this->extra_cols) {
$extra_cols = array_map(function($n) { return "`{$n}`"; }, $this->extra_cols);
$extra_cols = ', ' . join(', ', $extra_cols);
$extra_args = str_repeat(', ?', count($this->extra_cols));
}
$params = array($this->folder_id, $msguid, $object['uid'], $sql_data['changed'],
$sql_data['data'], $sql_data['xml'], $sql_data['tags'], $sql_data['words']);
foreach ($this->extra_cols as $col) {
$params[] = $sql_data[$col];
}
$result = $this->db->query(
"INSERT INTO `{$this->cache_table}` "
. " (`folder_id`, `msguid`, `uid`, `created`, `changed`, `data`, `xml`, `tags`, `words` $extra_cols)"
. " VALUES (?, ?, ?, " . $this->db->now() . ", ?, ?, ?, ?, ? $extra_args)",
$params
);
if (!$this->db->affected_rows($result)) {
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'message' => "Failed to write to kolab cache"
), true);
}
return;
}
$values = array(
$this->db->quote($this->folder_id),
$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['tags']),
$this->db->quote($sql_data['words']),
);
foreach ($this->extra_cols as $col) {
$values[] = $this->db->quote($sql_data[$col]);
}
$line = '(' . join(',', $values) . ')';
}
if ($buffer && (!$msguid || (strlen($buffer) + strlen($line) > $this->max_sql_packet()))) {
$extra_cols = '';
if ($this->extra_cols) {
$extra_cols = array_map(function($n) { return "`{$n}`"; }, $this->extra_cols);
$extra_cols = ', ' . join(', ', $extra_cols);
}
$result = $this->db->query(
"INSERT INTO `{$this->cache_table}` ".
" (`folder_id`, `msguid`, `uid`, `created`, `changed`, `data`, `xml`, `tags`, `words` $extra_cols)".
" 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;
}
/**
* Returns max_allowed_packet from mysql config
*/
protected function max_sql_packet()
{
if (!$this->max_sql_packet) {
// mysql limit or max 4 MB
$value = $this->db->get_variable('max_allowed_packet', 1048500);
$this->max_sql_packet = min($value, 4*1024*1024) - 2000;
}
return $this->max_sql_packet;
}
/**
* Read this folder's ID and cache metadata
*/
protected function _read_folder_data()
{
// already done
if (!empty($this->folder_id) || !$this->ready)
return;
$sql_arr = $this->db->fetch_assoc($this->db->query(
"SELECT `folder_id`, `synclock`, `ctag`, `changed`, `objectcount`"
. " FROM `{$this->folders_table}` WHERE `resource` = ?",
$this->resource_uri
));
if ($sql_arr) {
$this->metadata = $sql_arr;
$this->folder_id = $sql_arr['folder_id'];
}
else {
$this->db->query("INSERT INTO `{$this->folders_table}` (`resource`, `type`)"
. " VALUES (?, ?)", $this->resource_uri, $this->folder->type);
$this->folder_id = $this->db->insert_id('kolab_folders');
$this->metadata = array();
}
}
/**
* Check lock record for this folder and wait if locked or set lock
*/
protected function _sync_lock()
{
if (!$this->ready)
return;
$this->_read_folder_data();
// abort if database is not set-up
if ($this->db->is_error()) {
$this->check_error();
$this->ready = false;
return;
}
$read_query = "SELECT `synclock`, `ctag` FROM `{$this->folders_table}` WHERE `folder_id` = ?";
$write_query = "UPDATE `{$this->folders_table}` SET `synclock` = ? WHERE `folder_id` = ? AND `synclock` = ?";
// wait if locked (expire locks after 10 minutes) ...
// ... or if setting lock fails (another process meanwhile set it)
while (
(intval($this->metadata['synclock']) + $this->max_sync_lock_time > time()) ||
(($res = $this->db->query($write_query, time(), $this->folder_id, intval($this->metadata['synclock']))) &&
!($affected = $this->db->affected_rows($res)))
) {
usleep(500000);
$this->metadata = $this->db->fetch_assoc($this->db->query($read_query, $this->folder_id));
}
$this->synclock = $affected > 0;
}
/**
* Remove lock for this folder
*/
public function _sync_unlock()
{
if (!$this->ready || !$this->synclock)
return;
$this->db->query(
"UPDATE `{$this->folders_table}` SET `synclock` = 0, `ctag` = ?, `changed` = ?, `objectcount` = ? WHERE `folder_id` = ?",
$this->metadata['ctag'],
$this->metadata['changed'],
$this->metadata['objectcount'],
$this->folder_id
);
$this->synclock = false;
}
/**
* Check IMAP connection error state
*/
protected function check_error()
{
if (($err_code = $this->imap->get_error_code()) < 0) {
$this->error = kolab_storage::ERROR_IMAP_CONN;
if (($res_code = $this->imap->get_response_code()) !== 0 && in_array($res_code, array(rcube_storage::NOPERM, rcube_storage::READONLY))) {
$this->error = kolab_storage::ERROR_NO_PERMISSION;
}
}
else if ($this->db->is_error()) {
$this->error = kolab_storage::ERROR_CACHE_DB;
}
}
/**
* 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)
{
// query local database if available
if (!isset($this->uid2msg[$uid]) && $this->ready) {
$this->_read_folder_data();
$sql_result = $this->db->query(
"SELECT `msguid` FROM `{$this->cache_table}` ".
"WHERE `folder_id` = ? AND `uid` = ? ORDER BY `msguid` DESC",
$this->folder_id,
$uid
);
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$this->uid2msg[$uid] = $sql_arr['msguid'];
}
}
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] = end($results);
}
return $this->uid2msg[$uid];
}
/**
* Getter for protected member variables
*/
public function __get($name)
{
if ($name == 'folder_id') {
$this->_read_folder_data();
}
return $this->$name;
}
/**
* Set Roundcube storage options and bypass messages cache.
*
* We use skip_deleted and threading settings specific to Kolab,
* we have to change these global settings only temporarily.
* Roundcube cache duplicates information already stored in kolab_cache,
* that's why we can disable it for better performance.
*
* @param bool $force True to start Kolab mode, False to stop it.
*/
public function imap_mode($force = false)
{
// remember current IMAP settings
if ($force) {
$this->imap_options = array(
'skip_deleted' => $this->imap->get_option('skip_deleted'),
'threading' => $this->imap->get_threading(),
);
}
// re-set IMAP settings
$this->imap->set_threading($force ? false : $this->imap_options['threading']);
$this->imap->set_options(array(
'skip_deleted' => $force ? true : $this->imap_options['skip_deleted'],
));
// if kolab cache is disabled do nothing
if (!$this->enabled) {
return;
}
static $messages_cache, $cache_bypass;
if ($messages_cache === null) {
$rcmail = rcube::get_instance();
$messages_cache = (bool) $rcmail->config->get('messages_cache');
$cache_bypass = (int) $rcmail->config->get('kolab_messages_cache_bypass');
}
if ($messages_cache) {
// handle recurrent (multilevel) bypass() calls
if ($force) {
$this->cache_bypassed += 1;
if ($this->cache_bypassed > 1) {
return;
}
}
else {
$this->cache_bypassed -= 1;
if ($this->cache_bypassed > 0) {
return;
}
}
switch ($cache_bypass) {
case 2:
// Disable messages cache completely
$this->imap->set_messages_caching(!$force);
break;
case 1:
// We'll disable messages cache, but keep index cache.
// Default mode is both (MODE_INDEX | MODE_MESSAGE)
$mode = rcube_imap_cache::MODE_INDEX;
if (!$force) {
$mode |= rcube_imap_cache::MODE_MESSAGE;
}
$this->imap->set_messages_caching(true, $mode);
}
}
}
/**
* Converts DateTime or unix timestamp into sql date format
* using server timezone.
*/
protected function _convert_datetime($datetime)
{
if (is_object($datetime)) {
$dt = clone $datetime;
$dt->setTimeZone($this->server_timezone);
return $dt->format(self::DB_DATE_FORMAT);
}
else if ($datetime) {
return date(self::DB_DATE_FORMAT, $datetime);
}
}
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Aug 18, 1:06 AM (1 d, 22 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
1258638
Default Alt Text
(69 KB)

Event Timeline