summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAleksander Machniak <machniak@kolabsys.com>2014-03-15 14:32:52 (GMT)
committerAleksander Machniak <machniak@kolabsys.com>2014-03-15 14:32:52 (GMT)
commitf4d368d74b050b55e053ad04d9643b493456c40f (patch)
treec716ffafea041bf581511cd8488fc87b43f415f6
parentcb7725c6fc35ee5fcd48292db11584bb977a0c09 (diff)
downloadkolab-syncroton-f4d368d74b050b55e053ad04d9643b493456c40f.tar.gz
Support Notes synchronization, updated libkolab plugin
-rw-r--r--lib/ext/Syncroton/Command/FolderCreate.php4
-rw-r--r--lib/ext/Syncroton/Command/FolderSync.php3
-rw-r--r--lib/ext/Syncroton/Command/Sync.php9
-rw-r--r--lib/ext/Syncroton/Data/Factory.php5
-rw-r--r--lib/ext/Syncroton/Data/Notes.php24
-rw-r--r--lib/ext/Syncroton/Model/Note.php38
-rw-r--r--lib/ext/Syncroton/Registry.php10
-rw-r--r--lib/kolab_sync.php1
-rw-r--r--lib/kolab_sync_backend.php3
-rw-r--r--lib/kolab_sync_data_notes.php148
-rw-r--r--lib/plugins/libkolab/lib/kolab_date_recurrence.php12
-rw-r--r--lib/plugins/libkolab/lib/kolab_format.php51
-rw-r--r--lib/plugins/libkolab/lib/kolab_format_contact.php59
-rw-r--r--lib/plugins/libkolab/lib/kolab_format_distributionlist.php26
-rw-r--r--lib/plugins/libkolab/lib/kolab_format_note.php43
-rw-r--r--lib/plugins/libkolab/lib/kolab_format_xcal.php84
-rw-r--r--lib/plugins/libkolab/lib/kolab_storage_cache.php360
-rw-r--r--lib/plugins/libkolab/lib/kolab_storage_cache_contact.php16
-rw-r--r--lib/plugins/libkolab/lib/kolab_storage_cache_event.php8
-rw-r--r--lib/plugins/libkolab/lib/kolab_storage_cache_task.php4
-rw-r--r--lib/plugins/libkolab/lib/kolab_storage_dataset.php154
-rw-r--r--lib/plugins/libkolab/lib/kolab_storage_folder.php49
22 files changed, 852 insertions, 259 deletions
diff --git a/lib/ext/Syncroton/Command/FolderCreate.php b/lib/ext/Syncroton/Command/FolderCreate.php
index e7eda47..f3787f3 100644
--- a/lib/ext/Syncroton/Command/FolderCreate.php
+++ b/lib/ext/Syncroton/Command/FolderCreate.php
@@ -60,6 +60,10 @@ class Syncroton_Command_FolderCreate extends Syncroton_Command_Wbxml
case Syncroton_Command_FolderSync::FOLDERTYPE_MAIL_USER_CREATED:
$folder->class = Syncroton_Data_Factory::CLASS_EMAIL;
break;
+
+ case Syncroton_Command_FolderSync::FOLDERTYPE_NOTE_USER_CREATED:
+ $folder->class = Syncroton_Data_Factory::CLASS_NOTES;
+ break;
case Syncroton_Command_FolderSync::FOLDERTYPE_TASK_USER_CREATED:
$folder->class = Syncroton_Data_Factory::CLASS_TASKS;
diff --git a/lib/ext/Syncroton/Command/FolderSync.php b/lib/ext/Syncroton/Command/FolderSync.php
index 87d9d3f..ed3a350 100644
--- a/lib/ext/Syncroton/Command/FolderSync.php
+++ b/lib/ext/Syncroton/Command/FolderSync.php
@@ -48,7 +48,7 @@ class Syncroton_Command_FolderSync extends Syncroton_Command_Wbxml
const FOLDERTYPE_CONTACT_USER_CREATED = 14;
const FOLDERTYPE_TASK_USER_CREATED = 15;
const FOLDERTYPE_JOURNAL_USER_CREATED = 16;
- const FOLDERTYPE_NOTES_USER_CREATED = 17;
+ const FOLDERTYPE_NOTE_USER_CREATED = 17;
const FOLDERTYPE_UNKOWN = 18;
protected $_defaultNameSpace = 'uri:FolderHierarchy';
@@ -58,6 +58,7 @@ class Syncroton_Command_FolderSync extends Syncroton_Command_Wbxml
Syncroton_Data_Factory::CLASS_CALENDAR,
Syncroton_Data_Factory::CLASS_CONTACTS,
Syncroton_Data_Factory::CLASS_EMAIL,
+ Syncroton_Data_Factory::CLASS_NOTES,
Syncroton_Data_Factory::CLASS_TASKS
);
diff --git a/lib/ext/Syncroton/Command/Sync.php b/lib/ext/Syncroton/Command/Sync.php
index 8feb0ef..339808b 100644
--- a/lib/ext/Syncroton/Command/Sync.php
+++ b/lib/ext/Syncroton/Command/Sync.php
@@ -235,27 +235,26 @@ class Syncroton_Command_Sync extends Syncroton_Command_Wbxml
switch($collectionData->folder->class) {
case Syncroton_Data_Factory::CLASS_CALENDAR:
$dataClass = 'Syncroton_Model_Event';
-
break;
case Syncroton_Data_Factory::CLASS_CONTACTS:
$dataClass = 'Syncroton_Model_Contact';
-
break;
case Syncroton_Data_Factory::CLASS_EMAIL:
$dataClass = 'Syncroton_Model_Email';
-
break;
+ case Syncroton_Data_Factory::CLASS_NOTES:
+ $dataClass = 'Syncroton_Model_Note';
+ break;
+
case Syncroton_Data_Factory::CLASS_TASKS:
$dataClass = 'Syncroton_Model_Task';
-
break;
default:
throw new Syncroton_Exception_UnexpectedValue('invalid class provided');
-
break;
}
diff --git a/lib/ext/Syncroton/Data/Factory.php b/lib/ext/Syncroton/Data/Factory.php
index 2904b28..994cf46 100644
--- a/lib/ext/Syncroton/Data/Factory.php
+++ b/lib/ext/Syncroton/Data/Factory.php
@@ -21,6 +21,7 @@ class Syncroton_Data_Factory
const CLASS_CALENDAR = 'Calendar';
const CLASS_CONTACTS = 'Contacts';
const CLASS_EMAIL = 'Email';
+ const CLASS_NOTES = 'Notes';
const CLASS_TASKS = 'Tasks';
const STORE_EMAIL = 'Mailbox';
const STORE_GAL = 'GAL';
@@ -50,6 +51,10 @@ class Syncroton_Data_Factory
$className = Syncroton_Registry::get(Syncroton_Registry::EMAIL_DATA_CLASS);
break;
+ case self::CLASS_NOTES:
+ $className = Syncroton_Registry::get(Syncroton_Registry::NOTES_DATA_CLASS);
+ break;
+
case self::CLASS_TASKS:
$className = Syncroton_Registry::get(Syncroton_Registry::TASKS_DATA_CLASS);
break;
diff --git a/lib/ext/Syncroton/Data/Notes.php b/lib/ext/Syncroton/Data/Notes.php
new file mode 100644
index 0000000..0ca89ad
--- /dev/null
+++ b/lib/ext/Syncroton/Data/Notes.php
@@ -0,0 +1,24 @@
+<?php
+/**
+ * Syncroton
+ *
+ * @package Syncroton
+ * @subpackage Data
+ * @license http://www.tine20.org/licenses/lgpl.html LGPL Version 3
+ * @copyright Copyright (c) 2009-2012 Metaways Infosystems GmbH (http://www.metaways.de)
+ * @author Lars Kneschke <l.kneschke@metaways.de>
+ */
+
+/**
+ * class to handle ActiveSync Sync command
+ *
+ * @package Syncroton
+ * @subpackage Data
+ */
+class Syncroton_Data_Notes extends Syncroton_Data_AData
+{
+ protected $_supportedFolderTypes = array(
+ Syncroton_Command_FolderSync::FOLDERTYPE_NOTE,
+ Syncroton_Command_FolderSync::FOLDERTYPE_NOTE_USER_CREATED
+ );
+}
diff --git a/lib/ext/Syncroton/Model/Note.php b/lib/ext/Syncroton/Model/Note.php
new file mode 100644
index 0000000..5f4b542
--- /dev/null
+++ b/lib/ext/Syncroton/Model/Note.php
@@ -0,0 +1,38 @@
+<?php
+/**
+ * Syncroton
+ *
+ * @package Syncroton
+ * @subpackage Model
+ * @license http://www.tine20.org/licenses/lgpl.html LGPL Version 3
+ * @copyright Copyright (c) 2014 Kolab Systems AG (http://www.kolabsys.com)
+ * @author Aleksander Machniak <machniak@kolabsys.com>
+ */
+
+/**
+ * class to handle ActiveSync note
+ *
+ * @package Syncroton
+ * @subpackage Model
+ * @property Syncroton_Model_EmailBody body
+ * @property array categories
+ * @property DateTime lastModifiedDate
+ * @property string messageClass
+ * @property string subject
+ */
+class Syncroton_Model_Note extends Syncroton_Model_AXMLEntry
+{
+ protected $_xmlBaseElement = 'ApplicationData';
+
+ protected $_properties = array(
+ 'AirSyncBase' => array(
+ 'body' => array('type' => 'container', 'class' => 'Syncroton_Model_EmailBody')
+ ),
+ 'Notes' => array(
+ 'categories' => array('type' => 'container', 'childElement' => 'category'),
+ 'lastModifiedDate' => array('type' => 'datetime'),
+ 'messageClass' => array('type' => 'string'),
+ 'subject' => array('type' => 'string'),
+ )
+ );
+} \ No newline at end of file
diff --git a/lib/ext/Syncroton/Registry.php b/lib/ext/Syncroton/Registry.php
index 5f9ac46..5460f96 100644
--- a/lib/ext/Syncroton/Registry.php
+++ b/lib/ext/Syncroton/Registry.php
@@ -30,6 +30,7 @@ class Syncroton_Registry extends ArrayObject
const CALENDAR_DATA_CLASS = 'calendar_data_class';
const CONTACTS_DATA_CLASS = 'contacts_data_class';
const EMAIL_DATA_CLASS = 'email_data_class';
+ const NOTES_DATA_CLASS = 'notes_data_class';
const TASKS_DATA_CLASS = 'tasks_data_class';
const GAL_DATA_CLASS = 'gal_data_class';
@@ -356,6 +357,15 @@ class Syncroton_Registry extends ArrayObject
self::set(self::EMAIL_DATA_CLASS, $className);
}
+ public static function setNotesDataClass($className)
+ {
+ if (!class_exists($className)) {
+ throw new InvalidArgumentException('invalid $_className provided');
+ }
+
+ self::set(self::NOTES_DATA_CLASS, $className);
+ }
+
public static function setTasksDataClass($className)
{
if (!class_exists($className)) {
diff --git a/lib/kolab_sync.php b/lib/kolab_sync.php
index 1870996..3ae473c 100644
--- a/lib/kolab_sync.php
+++ b/lib/kolab_sync.php
@@ -149,6 +149,7 @@ class kolab_sync extends rcube
Syncroton_Registry::setContactsDataClass('kolab_sync_data_contacts');
Syncroton_Registry::setCalendarDataClass('kolab_sync_data_calendar');
Syncroton_Registry::setEmailDataClass('kolab_sync_data_email');
+ Syncroton_Registry::setNotesDataClass('kolab_sync_data_notes');
Syncroton_Registry::setTasksDataClass('kolab_sync_data_tasks');
Syncroton_Registry::setGALDataClass('kolab_sync_data_gal');
diff --git a/lib/kolab_sync_backend.php b/lib/kolab_sync_backend.php
index 7e7ea66..cd984bb 100644
--- a/lib/kolab_sync_backend.php
+++ b/lib/kolab_sync_backend.php
@@ -61,6 +61,7 @@ class kolab_sync_backend
Syncroton_Data_Factory::CLASS_CALENDAR => 'event',
Syncroton_Data_Factory::CLASS_CONTACTS => 'contact',
Syncroton_Data_Factory::CLASS_EMAIL => 'mail',
+ Syncroton_Data_Factory::CLASS_NOTES => 'note',
Syncroton_Data_Factory::CLASS_TASKS => 'task',
);
@@ -521,9 +522,11 @@ class kolab_sync_backend
'mail.outbox',
'event.default',
'contact.default',
+ 'note.default',
'task.default',
'event',
'contact',
+ 'note',
'task'
);
diff --git a/lib/kolab_sync_data_notes.php b/lib/kolab_sync_data_notes.php
new file mode 100644
index 0000000..1bc151f
--- /dev/null
+++ b/lib/kolab_sync_data_notes.php
@@ -0,0 +1,148 @@
+<?php
+
+/**
+ +--------------------------------------------------------------------------+
+ | Kolab Sync (ActiveSync for Kolab) |
+ | |
+ | Copyright (C) 2011-2014, 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/> |
+ +--------------------------------------------------------------------------+
+ | Author: Aleksander Machniak <machniak@kolabsys.com> |
+ +--------------------------------------------------------------------------+
+*/
+
+/**
+ * Notes data class for Syncroton
+ */
+class kolab_sync_data_notes extends kolab_sync_data
+{
+ /**
+ * Mapping from ActiveSync Calendar namespace fields
+ */
+ protected $mapping = array(
+ 'body' => 'description',
+ 'categories' => 'categories',
+ 'lastModifiedDate' => 'changed',
+ //'messageClass' => 'messageClass',
+ 'subject' => 'title',
+ );
+
+
+
+ /**
+ * Kolab object type
+ *
+ * @var string
+ */
+ protected $modelName = 'note';
+
+ /**
+ * Type of the default folder
+ *
+ * @var int
+ */
+ protected $defaultFolderType = Syncroton_Command_FolderSync::FOLDERTYPE_NOTE;
+
+ /**
+ * Default container for new entries
+ *
+ * @var string
+ */
+ protected $defaultFolder = 'Notes';
+
+ /**
+ * Type of user created folders
+ *
+ * @var int
+ */
+ protected $folderType = Syncroton_Command_FolderSync::FOLDERTYPE_NOTE_USER_CREATED;
+
+
+ /**
+ * Appends note data to xml element
+ *
+ * @param Syncroton_Model_SyncCollection $collection Collection data
+ * @param string $serverId Local entry identifier
+ * @param boolean $as_array Return entry as an array
+ *
+ * @return array|Syncroton_Model_Note|array Note object
+ */
+ public function getEntry(Syncroton_Model_SyncCollection $collection, $serverId, $as_array = false)
+ {
+ $note = is_array($serverId) ? $serverId : $this->getObject($collection->collectionId, $serverId);
+ $config = $this->getFolderConfig($note['_mailbox']);
+ $result = array();
+
+ // Calendar namespace fields
+ foreach ($this->mapping as $key => $name) {
+ $value = $this->getKolabDataItem($note, $name);
+
+ switch ($name) {
+ case 'changed':
+ $value = self::date_from_kolab($value);
+ break;
+
+ case 'description':
+ $value = $this->setBody($value);
+ break;
+ }
+
+ if (empty($value) || is_array($value)) {
+ continue;
+ }
+
+ $result[$key] = $value;
+ }
+
+ $result['messageClass'] = 'IPM.StickyNote';
+
+ return $as_array ? $result : new Syncroton_Model_Note($result);
+ }
+
+ /**
+ * convert note from xml to libkolab array
+ *
+ * @param Syncroton_Model_IEntry $data Note to convert
+ * @param string $folderid Folder identifier
+ * @param array $entry Existing entry
+ *
+ * @return array
+ */
+ public function toKolab(Syncroton_Model_IEntry $data, $folderid, $entry = null)
+ {
+ $note = !empty($entry) ? $entry : array();
+ $foldername = isset($note['_mailbox']) ? $note['_mailbox'] : $this->getFolderName($folderid);
+ $config = $this->getFolderConfig($foldername);
+
+ // Calendar namespace fields
+ foreach ($this->mapping as $key => $name) {
+ $value = $data->$key;
+
+ switch ($name) {
+ case 'description':
+ $value = $this->getBody($value, Syncroton_Model_EmailBody::TYPE_PLAINTEXT);
+ // If description isn't specified keep old description
+ if ($value === null) {
+ continue 2;
+ }
+ break;
+ }
+
+ $this->setKolabDataItem($note, $name, $value);
+ }
+
+ return $note;
+ }
+}
diff --git a/lib/plugins/libkolab/lib/kolab_date_recurrence.php b/lib/plugins/libkolab/lib/kolab_date_recurrence.php
index 85ffd91..06dd331 100644
--- a/lib/plugins/libkolab/lib/kolab_date_recurrence.php
+++ b/lib/plugins/libkolab/lib/kolab_date_recurrence.php
@@ -101,7 +101,7 @@ class kolab_date_recurrence
/**
* Get the end date of the occurence of this recurrence cycle
*
- * @return mixed Timestamp with end date of the last event or False if recurrence exceeds limit
+ * @return DateTime|bool End datetime of the last event or False if recurrence exceeds limit
*/
public function end()
{
@@ -109,25 +109,25 @@ class kolab_date_recurrence
// recurrence end date is given
if ($event['recurrence']['UNTIL'] instanceof DateTime) {
- return $event['recurrence']['UNTIL']->format('U');
+ return $event['recurrence']['UNTIL'];
}
// let libkolab do the work
if ($this->engine && ($cend = $this->engine->getLastOccurrence()) && ($end_dt = kolab_format::php_datetime(new cDateTime($cend)))) {
- return $end_dt->format('U');
+ return $end_dt;
}
// determine a reasonable end date if none given
- if (!$event['recurrence']['COUNT']) {
+ if (!$event['recurrence']['COUNT'] && $event['end'] instanceof DateTime) {
switch ($event['recurrence']['FREQ']) {
case 'YEARLY': $intvl = 'P100Y'; break;
case 'MONTHLY': $intvl = 'P20Y'; break;
default: $intvl = 'P10Y'; break;
}
- $end_dt = clone $event['start'];
+ $end_dt = clone $event['end'];
$end_dt->add(new DateInterval($intvl));
- return $end_dt->format('U');
+ return $end_dt;
}
return false;
diff --git a/lib/plugins/libkolab/lib/kolab_format.php b/lib/plugins/libkolab/lib/kolab_format.php
index 679ae10..11a5c4f 100644
--- a/lib/plugins/libkolab/lib/kolab_format.php
+++ b/lib/plugins/libkolab/lib/kolab_format.php
@@ -45,7 +45,7 @@ abstract class kolab_format
protected $version = '3.0';
const KTYPE_PREFIX = 'application/x-vnd.kolab.';
- const PRODUCT_ID = 'Roundcube-libkolab-0.9';
+ const PRODUCT_ID = 'Roundcube-libkolab-0.9';
/**
* Factory method to instantiate a kolab_format object of the given type and version
@@ -242,7 +242,8 @@ abstract class kolab_format
break;
case kolabformat::Warning:
$ret = false;
- $log = "Warning";
+ $uid = is_object($this->obj) ? $this->obj->uid() : $this->data['uid'];
+ $log = "Warning @ $uid";
break;
default:
$ret = true;
@@ -496,4 +497,50 @@ abstract class kolab_format
{
return array();
}
+
+ protected function get_attachments(&$object)
+ {
+ // 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();
+ $content = $attach->data();
+ $object['_attachments'][$name] = array(
+ 'name' => $name,
+ 'mimetype' => $attach->mimetype(),
+ 'size' => strlen($content),
+ 'content' => $content,
+ );
+ }
+ else if (substr($attach->uri(), 0, 4) == 'http') {
+ $object['links'][] = $attach->uri();
+ }
+ }
+ }
+
+ protected function set_attachments($object)
+ {
+ // 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, 'unknown');
+ $vattach->push($attach);
+ }
+
+ $this->obj->setAttachments($vattach);
+ }
}
diff --git a/lib/plugins/libkolab/lib/kolab_format_contact.php b/lib/plugins/libkolab/lib/kolab_format_contact.php
index 0d0bc75..63efe9a 100644
--- a/lib/plugins/libkolab/lib/kolab_format_contact.php
+++ b/lib/plugins/libkolab/lib/kolab_format_contact.php
@@ -107,8 +107,8 @@ class kolab_format_contact extends kolab_format
if (isset($object['nickname']))
$this->obj->setNickNames(self::array2vector($object['nickname']));
- if (isset($object['profession']))
- $this->obj->setTitles(self::array2vector($object['profession']));
+ if (isset($object['jobtitle']))
+ $this->obj->setTitles(self::array2vector($object['jobtitle']));
// organisation related properties (affiliation)
$org = new Affiliation;
@@ -117,17 +117,17 @@ class kolab_format_contact extends kolab_format
$org->setOrganisation($object['organization']);
if ($object['department'])
$org->setOrganisationalUnits(self::array2vector($object['department']));
- if ($object['jobtitle'])
- $org->setRoles(self::array2vector($object['jobtitle']));
+ if ($object['profession'])
+ $org->setRoles(self::array2vector($object['profession']));
$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));
+ foreach (array('manager','assistant') as $field) {
+ if (!empty($object[$field])) {
+ $reltype = $this->relatedmap[$field];
+ foreach ((array)$object[$field] as $value) {
+ $rels->push(new Related(Related::Text, $value, $reltype));
+ }
+ }
}
$org->setRelateds($rels);
@@ -219,12 +219,13 @@ class kolab_format_contact extends kolab_format
// 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));
+ foreach (array('spouse','children') as $field) {
+ if (!empty($object[$field])) {
+ $reltype = $this->relatedmap[$field];
+ foreach ((array)$object[$field] as $value) {
+ $rels->push(new Related(Related::Text, $value, $reltype));
+ }
+ }
}
$this->obj->setRelateds($rels);
@@ -296,7 +297,7 @@ class kolab_format_contact extends kolab_format
$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['jobtitle'] = join(' ', self::vector2array($this->obj->titles()));
$object['categories'] = self::vector2array($this->obj->categories());
// organisation related properties (affiliation)
@@ -304,7 +305,7 @@ class kolab_format_contact extends kolab_format
if ($orgs->size()) {
$org = $orgs->get(0);
$object['organization'] = $org->organisation();
- $object['jobtitle'] = join(' ', self::vector2array($org->roles()));
+ $object['profession'] = join(' ', self::vector2array($org->roles()));
$object['department'] = join(' ', self::vector2array($org->organisationalUnits()));
$this->read_relateds($org->relateds(), $object);
}
@@ -347,10 +348,10 @@ class kolab_format_contact extends kolab_format
$object['freebusyurl'] = $this->obj->freeBusyUrl();
if ($bday = self::php_datetime($this->obj->bDay()))
- $object['birthday'] = $bday->format('c');
+ $object['birthday'] = $bday;
if ($anniversary = self::php_datetime($this->obj->anniversary()))
- $object['anniversary'] = $anniversary->format('c');
+ $object['anniversary'] = $anniversary;
$gendermap = array_flip($this->gendermap);
if (($g = $this->obj->gender()) && $gendermap[$g])
@@ -407,6 +408,22 @@ class kolab_format_contact extends kolab_format
}
/**
+ * 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 (!empty($this->data['birthday'])) {
+ $tags[] = 'x-has-birthday';
+ }
+
+ return $tags;
+ }
+
+ /**
* Helper method to copy contents of an Address vector to the contact data object
*/
private function read_addresses($addresses, &$object, $type = null)
diff --git a/lib/plugins/libkolab/lib/kolab_format_distributionlist.php b/lib/plugins/libkolab/lib/kolab_format_distributionlist.php
index 46dda01..88c6f7b 100644
--- a/lib/plugins/libkolab/lib/kolab_format_distributionlist.php
+++ b/lib/plugins/libkolab/lib/kolab_format_distributionlist.php
@@ -44,17 +44,29 @@ class kolab_format_distributionlist extends kolab_format
$this->obj->setName($object['name']);
+ $seen = array();
$members = new vectorcontactref;
- foreach ((array)$object['member'] as $member) {
- if ($member['uid'])
+ foreach ((array)$object['member'] as $i => $member) {
+ if ($member['uid']) {
+ $key = 'uid:' . $member['uid'];
$m = new ContactReference(ContactReference::UidReference, $member['uid']);
- else if ($member['email'])
+ }
+ else if ($member['email']) {
+ $key = 'mailto:' . $member['email'];
$m = new ContactReference(ContactReference::EmailReference, $member['email']);
- else
+ $m->setName($member['name']);
+ }
+ else {
continue;
-
- $m->setName($member['name']);
- $members->push($m);
+ }
+
+ if (!$seen[$key]++) {
+ $members->push($m);
+ }
+ else {
+ // remove dupes for caching
+ unset($object['member'][$i]);
+ }
}
$this->obj->setMembers($members);
diff --git a/lib/plugins/libkolab/lib/kolab_format_note.php b/lib/plugins/libkolab/lib/kolab_format_note.php
index 04a8421..1f49dee 100644
--- a/lib/plugins/libkolab/lib/kolab_format_note.php
+++ b/lib/plugins/libkolab/lib/kolab_format_note.php
@@ -31,6 +31,11 @@ class kolab_format_note extends kolab_format
protected $read_func = 'readNote';
protected $write_func = 'writeNote';
+ protected $sensitivity_map = array(
+ 'public' => kolabformat::ClassPublic,
+ 'private' => kolabformat::ClassPrivate,
+ 'confidential' => kolabformat::ClassConfidential,
+ );
/**
* Set properties to the kolabformat object
@@ -42,7 +47,12 @@ class kolab_format_note extends kolab_format
// set common object properties
parent::set($object);
- // TODO: set object propeties
+ $this->obj->setSummary($object['title']);
+ $this->obj->setDescription($object['description']);
+ $this->obj->setClassification($this->sensitivity_map[$object['sensitivity']]);
+ $this->obj->setCategories(self::array2vector($object['categories']));
+
+ $this->set_attachments($object);
// cache this data
$this->data = $object;
@@ -73,10 +83,35 @@ class kolab_format_note extends kolab_format
// read common object props into local data object
$object = parent::to_array($data);
- // TODO: read object properties
+ $sensitivity_map = array_flip($this->sensitivity_map);
- $this->data = $object;
- return $this->data;
+ // read object properties
+ $object += array(
+ 'sensitivity' => $sensitivity_map[$this->obj->classification()],
+ 'categories' => self::vector2array($this->obj->categories()),
+ 'title' => $this->obj->summary(),
+ 'description' => $this->obj->description(),
+ );
+
+ $this->get_attachments($object);
+
+ 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;
}
}
diff --git a/lib/plugins/libkolab/lib/kolab_format_xcal.php b/lib/plugins/libkolab/lib/kolab_format_xcal.php
index 500dfa2..a2544f4 100644
--- a/lib/plugins/libkolab/lib/kolab_format_xcal.php
+++ b/lib/plugins/libkolab/lib/kolab_format_xcal.php
@@ -137,6 +137,16 @@ abstract class kolab_format_xcal extends kolab_format
$attendee = $attvec->get($i);
$cr = $attendee->contact();
if ($cr->email() != $object['organizer']['email']) {
+ $delegators = $delegatees = array();
+ $vdelegators = $attendee->delegatedFrom();
+ for ($j=0; $j < $vdelegators->size(); $j++) {
+ $delegators[] = $vdelegators->get($j)->email();
+ }
+ $vdelegatees = $attendee->delegatedTo();
+ for ($j=0; $j < $vdelegatees->size(); $j++) {
+ $delegatees[] = $vdelegatees->get($j)->email();
+ }
+
$object['attendees'][] = array(
'role' => $role_map[$attendee->role()],
'cutype' => $cutype_map[$attendee->cutype()],
@@ -144,6 +154,8 @@ abstract class kolab_format_xcal extends kolab_format
'rsvp' => $attendee->rsvp(),
'email' => $cr->email(),
'name' => $cr->name(),
+ 'delegated-from' => $delegators,
+ 'delegated-to' => $delegatees,
);
}
}
@@ -191,6 +203,13 @@ abstract class kolab_format_xcal extends kolab_format
}
}
+ if ($rdates = $this->obj->recurrenceDates()) {
+ for ($i=0; $i < $rdates->size(); $i++) {
+ if ($rdate = self::php_datetime($rdates->get($i)))
+ $object['recurrence']['RDATE'][] = $rdate;
+ }
+ }
+
// read alarm
$valarms = $this->obj->alarms();
$alarm_types = array_flip($this->alarm_type_map);
@@ -218,26 +237,7 @@ abstract class kolab_format_xcal extends kolab_format
}
}
- // 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();
- $content = $attach->data();
- $object['_attachments'][$name] = array(
- 'name' => $name,
- 'mimetype' => $attach->mimetype(),
- 'size' => strlen($content),
- 'content' => $content,
- );
- }
- else if (substr($attach->uri(), 0, 4) == 'http') {
- $object['links'][] = $attach->uri();
- }
- }
+ $this->get_attachments($object);
return $object;
}
@@ -287,6 +287,21 @@ abstract class kolab_format_xcal extends kolab_format
$att->setCutype($this->cutype_map[$attendee['cutype']] ? $this->cutype_map[$attendee['cutype']] : kolabformat::CutypeIndividual);
$att->setRSVP((bool)$attendee['rsvp']);
+ if (!empty($attendee['delegated-from'])) {
+ $vdelegators = new vectorcontactref;
+ foreach ((array)$attendee['delegated-from'] as $delegator) {
+ $vdelegators->push(new ContactReference(ContactReference::EmailReference, $delegator));
+ }
+ $att->setDelegatedFrom($vdelegators);
+ }
+ if (!empty($attendee['delegated-to'])) {
+ $vdelegatees = new vectorcontactref;
+ foreach ((array)$attendee['delegated-to'] as $delegatee) {
+ $vdelegatees->push(new ContactReference(ContactReference::EmailReference, $delegatee));
+ }
+ $att->setDelegatedTo($vdelegatees);
+ }
+
if ($att->isValid()) {
$attendees->push($att);
}
@@ -311,7 +326,7 @@ abstract class kolab_format_xcal extends kolab_format
$rr = new RecurrenceRule;
$rr->setFrequency(RecurrenceRule::FreqNone);
- if ($object['recurrence']) {
+ if ($object['recurrence'] && !empty($object['recurrence']['FREQ'])) {
$rr->setFrequency($this->rrule_type_map[$object['recurrence']['FREQ']]);
if ($object['recurrence']['INTERVAL'])
@@ -368,6 +383,14 @@ abstract class kolab_format_xcal extends kolab_format
$this->obj->setRecurrenceRule($rr);
+ // save recurrence dates (aka RDATE)
+ if (!empty($object['recurrence']['RDATE'])) {
+ $rdates = new vectordatetime;
+ foreach ((array)$object['recurrence']['RDATE'] as $rdate)
+ $rdates->push(self::get_datetime($rdate, null, true));
+ $this->obj->setRecurrenceDates($rdates);
+ }
+
// save alarm
$valarms = new vectoralarm;
if ($object['alarms']) {
@@ -401,24 +424,7 @@ abstract class kolab_format_xcal extends kolab_format
}
$this->obj->setAlarms($valarms);
- // 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, 'unknown');
- $vattach->push($attach);
- }
-
- $this->obj->setAttachments($vattach);
+ $this->set_attachments($object);
}
/**
diff --git a/lib/plugins/libkolab/lib/kolab_storage_cache.php b/lib/plugins/libkolab/lib/kolab_storage_cache.php
index db174e5..9c1368f 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_cache.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache.php
@@ -24,12 +24,13 @@
class kolab_storage_cache
{
+ const DB_DATE_FORMAT = 'Y-m-d H:i:s';
+
protected $db;
protected $imap;
protected $folder;
protected $uid2msg;
protected $objects;
- protected $index = array();
protected $metadata = array();
protected $folder_id;
protected $resource_uri;
@@ -43,6 +44,8 @@ class kolab_storage_cache
protected $max_sync_lock_time = 600;
protected $binary_items = array();
protected $extra_cols = array();
+ protected $order_by = null;
+ protected $limit = null;
/**
@@ -58,8 +61,10 @@ class kolab_storage_cache
rcube::raise_error(array(
'code' => 900,
'type' => 'php',
- 'message' => "No kolab_storage_cache class found for folder of type " . $storage_folder->type
+ '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);
}
}
@@ -85,6 +90,24 @@ class kolab_storage_cache
$this->set_folder($storage_folder);
}
+ /**
+ * Direct access to cache by folder_id
+ * (only for internal use)
+ */
+ public function select_by_id($folder_id)
+ {
+ $folders_table = $this->db->table_name('kolab_folders');
+ $sql_arr = $this->db->fetch_assoc($this->db->query("SELECT * FROM $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
@@ -104,7 +127,7 @@ class kolab_storage_cache
$this->resource_uri = $this->folder->get_resource_uri();
$this->folders_table = $this->db->table_name('kolab_folders');
$this->cache_table = $this->db->table_name('kolab_cache_' . $this->folder->type);
- $this->ready = $this->enabled;
+ $this->ready = $this->enabled && !empty($this->folder->type);
$this->folder_id = null;
}
@@ -137,64 +160,71 @@ class kolab_storage_cache
// increase time limit
@set_time_limit($this->max_sync_lock_time);
- // read cached folder metadata
- $this->_read_folder_data();
-
- // check cache status hash first ($this->metadata is set in _read_folder_data())
- if ($this->metadata['ctag'] != $this->folder->get_ctag()) {
- // lock synchronization for this folder or wait if locked
- $this->_sync_lock();
+ if (!$this->ready) {
+ // kolab cache is disabled, synchronize IMAP mailbox cache only
+ $this->imap->folder_sync($this->folder->name);
+ }
+ else {
+ // read cached folder metadata
+ $this->_read_folder_data();
- // disable messages cache if configured to do so
- $this->bypass(true);
+ // check cache status hash first ($this->metadata is set in _read_folder_data())
+ if ($this->metadata['ctag'] != $this->folder->get_ctag()) {
+ // lock synchronization for this folder or wait if locked
+ $this->_sync_lock();
- // synchronize IMAP mailbox cache
- $this->imap->folder_sync($this->folder->name);
+ // disable messages cache if configured to do so
+ $this->bypass(true);
- // compare IMAP index with object cache index
- $imap_index = $this->imap->index($this->folder->name, null, null, true, true);
- $this->index = $imap_index->get();
+ // synchronize IMAP mailbox cache
+ $this->imap->folder_sync($this->folder->name);
- // determine objects to fetch or to invalidate
- if ($this->ready) {
- // read cache index
- $sql_result = $this->db->query(
- "SELECT msguid, uid FROM $this->cache_table WHERE folder_id=?",
- $this->folder_id
- );
+ // compare IMAP index with object cache index
+ $imap_index = $this->imap->index($this->folder->name, null, null, true, true);
- $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'];
- }
+ // determine objects to fetch or to invalidate
+ if (!$imap_index->is_error()) {
+ $imap_index = $imap_index->get();
- // 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 $this->cache_table WHERE folder_id=? AND msguid IN ($quoted_ids)",
+ // read cache index
+ $sql_result = $this->db->query(
+ "SELECT msguid, uid FROM $this->cache_table WHERE folder_id=?",
$this->folder_id
);
- }
- // update ctag value (will be written to database in _sync_unlock())
- $this->metadata['ctag'] = $this->folder->get_ctag();
- }
+ $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'];
+ }
- $this->bypass(false);
+ // fetch new objects from imap
+ foreach (array_diff($imap_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, $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())
+ $this->metadata['ctag'] = $this->folder->get_ctag();
+ }
- // remove lock
- $this->_sync_unlock();
+ $this->bypass(false);
+
+ // remove lock
+ $this->_sync_unlock();
+ }
}
$this->synched = time();
@@ -229,14 +259,14 @@ class kolab_storage_cache
);
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
- $this->objects[$msguid] = $this->_unserialize($sql_arr);
+ $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])) {
$result = $this->_fetch(array($msguid), $type, $foldername);
- $this->objects[$msguid] = $result[0];
+ $this->objects = array($msguid => $result[0]); // store only this object in memory (#2827)
}
}
@@ -272,7 +302,7 @@ class kolab_storage_cache
if ($object) {
// insert new object data...
- $this->insert($msguid, $object);
+ $this->save($msguid, $object);
}
else {
// ...or set in-memory cache to false
@@ -282,41 +312,48 @@ class kolab_storage_cache
/**
- * Insert a cache entry
+ * Insert (or update) a cache entry
*
- * @param string Related IMAP message UID
+ * @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 insert($msguid, $object)
+ 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'];
- $extra_cols = $this->extra_cols ? ', ' . join(', ', $this->extra_cols) : '';
- $extra_fields = $this->extra_cols ? str_repeat(', ?', count($this->extra_cols)) : '';
+ $args = array();
+ $cols = array('folder_id', 'msguid', 'uid', 'changed', 'data', 'xml', 'tags', 'words');
+ $cols = array_merge($cols, $this->extra_cols);
- $args = array(
- "INSERT INTO $this->cache_table ".
- " (folder_id, msguid, uid, created, changed, data, xml, tags, words $extra_cols)".
- " VALUES (?, ?, ?, " . $this->db->now() . ", ?, ?, ?, ?, ? $extra_fields)",
- $this->folder_id,
- $msguid,
- $object['uid'],
- $sql_data['changed'],
- $sql_data['data'],
- $sql_data['xml'],
- $sql_data['tags'],
- $sql_data['words'],
- );
+ foreach ($cols as $idx => $col) {
+ $cols[$idx] = $this->db->quote_identifier($col);
+ $args[] = $sql_data[$col];
+ }
- foreach ($this->extra_cols as $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 = call_user_func_array(array($this->db, 'query'), $args);
+ $result = $this->db->query($query, $args);
if (!$this->db->affected_rows($result)) {
rcube::raise_error(array(
@@ -341,22 +378,27 @@ class kolab_storage_cache
*/
public function move($msguid, $uid, $target_folder)
{
- $target = kolab_storage::get_folder($target_folder);
+ if ($this->ready) {
+ $target = kolab_storage::get_folder($target_folder);
- // resolve new message UID in target folder
- if ($new_msguid = $target->cache->uid2msguid($uid)) {
- $this->_read_folder_data();
+ // resolve new message UID in target folder
+ if ($new_msguid = $target->cache->uid2msguid($uid)) {
+ $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
- );
+ $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();
+ }
}
- else {
+
+ if (empty($result)) {
// just clear cache entry
$this->set($msguid, false);
}
@@ -370,12 +412,17 @@ class kolab_storage_cache
*/
public function purge($type = null)
{
+ 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);
}
@@ -386,6 +433,10 @@ class kolab_storage_cache
*/
public function rename($new_folder)
{
+ if (!$this->ready) {
+ return;
+ }
+
$target = kolab_storage::get_folder($new_folder);
// resolve new message UID in target folder
@@ -407,51 +458,67 @@ class kolab_storage_cache
*/
public function select($query = array(), $uids = false)
{
- $result = array();
+ $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();
- $sql_result = $this->db->query(
- "SELECT " . ($uids ? 'msguid, uid' : '*') . " FROM $this->cache_table ".
- "WHERE folder_id=? " . $this->_sql_where($query),
- $this->folder_id
- );
+ // fetch full object data on one query if a small result set is expected
+ $fetchall = !$uids && ($this->limit ? $this->limit[0] : $this->count($query)) < 500;
+ $sql_query = "SELECT " . ($fetchall ? '*' : 'msguid AS _msguid, uid') . " FROM $this->cache_table ".
+ "WHERE folder_id=? " . $this->_sql_where($query);
+ if (!empty($this->order_by)) {
+ $sql_query .= ' 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)) {
- return null;
+ 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'];
+ $this->uid2msg[$sql_arr['uid']] = $sql_arr['_msguid'];
$result[] = $sql_arr['uid'];
}
- else if ($object = $this->_unserialize($sql_arr)) {
+ else if ($fetchall && ($object = $this->_unserialize($sql_arr))) {
$result[] = $object;
}
+ else {
+ // only add msguid to dataset index
+ $result[] = $sql_arr;
+ }
}
}
+ // use IMAP
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
+ if ($filter['type']) {
$search = 'UNDELETED HEADER X-Kolab-Type ' . kolab_format::KTYPE_PREFIX . $filter['type'];
- $index = $this->imap->search_once($this->folder->name, $search)->get();
+ $index = $this->imap->search_once($this->folder->name, $search);
+ }
+ else {
+ $index = $this->imap->index($this->folder->name, null, null, true, true);
}
if ($index->is_error()) {
- return null;
+ if ($uids) {
+ return null;
+ }
+ $result->set_error(true);
+ return $result;
}
- // fetch all messages in $index from IMAP
- $result = $uids ? $this->_fetch_uids($index, $filter['type']) : $this->_fetch($index, $filter['type']);
+ $index = $index->get();
+ $result = $uids ? $index : $this->_fetch($index, $filter['type']);
// TODO: post-filter result according to query
}
@@ -461,7 +528,7 @@ class kolab_storage_cache
if (!$uids && count($result) == 1) {
if ($msguid = $result[0]['_msguid']) {
$this->uid2msg[$result[0]['uid']] = $msguid;
- $this->objects[$msguid] = $result[0];
+ $this->objects = array($msguid => $result[0]);
}
}
@@ -477,8 +544,8 @@ class kolab_storage_cache
*/
public function count($query = array())
{
- // cache is in sync, we can count records in local DB
- if ($this->synched) {
+ // read from local cache DB (assume it to be synchronized)
+ if ($this->ready) {
$this->_read_folder_data();
$sql_result = $this->db->query(
@@ -494,22 +561,50 @@ class kolab_storage_cache
$sql_arr = $this->db->fetch_assoc($sql_result);
$count = intval($sql_arr['numrows']);
}
+ // use IMAP
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);
+
+ 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);
+ }
if ($index->is_error()) {
return null;
}
+ // TODO: post-filter result according to query
+
$count = $index->count();
}
return $count;
}
+ /**
+ * Define ORDER BY clause for cache queries
+ */
+ public function set_order_by($sortcols)
+ {
+ if (!empty($sortcols)) {
+ $this->order_by = join(', ', (array)$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
@@ -580,7 +675,7 @@ class kolab_storage_cache
*/
protected function _fetch($index, $type = null, $folder = null)
{
- $results = array();
+ $results = new kolab_storage_dataset($this);
foreach ((array)$index as $msguid) {
if ($object = $this->folder->read_object($msguid, $type, $folder)) {
$results[] = $object;
@@ -591,43 +686,6 @@ class kolab_storage_cache
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
- */
- protected function _fetch_uids($index, $type = null)
- {
- if (!$type)
- $type = $this->folder->type;
-
- $this->bypass(true);
-
- $results = array();
- $headers = $this->imap->fetch_headers($this->folder->name, $index, false);
-
- $this->bypass(false);
-
- foreach ((array)$headers 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
*/
@@ -783,7 +841,7 @@ class kolab_storage_cache
protected function _read_folder_data()
{
// already done
- if (!empty($this->folder_id))
+ if (!empty($this->folder_id) || !$this->ready)
return;
$sql_arr = $this->db->fetch_assoc($this->db->query("SELECT folder_id, synclock, ctag FROM $this->folders_table WHERE resource=?", $this->resource_uri));
diff --git a/lib/plugins/libkolab/lib/kolab_storage_cache_contact.php b/lib/plugins/libkolab/lib/kolab_storage_cache_contact.php
index e17923d..9666a39 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_cache_contact.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache_contact.php
@@ -23,7 +23,7 @@
class kolab_storage_cache_contact extends kolab_storage_cache
{
- protected $extra_cols = array('type');
+ protected $extra_cols = array('type','name','firstname','surname','email');
protected $binary_items = array(
'photo' => '|<photo><uri>[^;]+;base64,([^<]+)</uri></photo>|i',
'pgppublickey' => '|<key><uri>date:application/pgp-keys;base64,([^<]+)</uri></key>|i',
@@ -40,6 +40,20 @@ class kolab_storage_cache_contact extends kolab_storage_cache
$sql_data = parent::_serialize($object);
$sql_data['type'] = $object['_type'];
+ // columns for sorting
+ $sql_data['name'] = rcube_charset::clean($object['name'] . $object['prefix']);
+ $sql_data['firstname'] = rcube_charset::clean($object['firstname'] . $object['middlename'] . $object['surname']);
+ $sql_data['surname'] = rcube_charset::clean($object['surname'] . $object['firstname'] . $object['middlename']);
+ $sql_data['email'] = rcube_charset::clean(is_array($object['email']) ? $object['email'][0] : $object['email']);
+
+ if (is_array($sql_data['email'])) {
+ $sql_data['email'] = $sql_data['email']['address'];
+ }
+ // avoid value being null
+ if (empty($sql_data['email'])) {
+ $sql_data['email'] = '';
+ }
+
return $sql_data;
}
} \ No newline at end of file
diff --git a/lib/plugins/libkolab/lib/kolab_storage_cache_event.php b/lib/plugins/libkolab/lib/kolab_storage_cache_event.php
index 876c3b4..5fc44cd 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_cache_event.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache_event.php
@@ -34,14 +34,14 @@ class kolab_storage_cache_event extends kolab_storage_cache
{
$sql_data = parent::_serialize($object);
- // 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']);
+ $sql_data['dtstart'] = is_object($object['start']) ? $object['start']->format(self::DB_DATE_FORMAT) : date(self::DB_DATE_FORMAT, $object['start']);
+ $sql_data['dtend'] = is_object($object['end']) ? $object['end']->format(self::DB_DATE_FORMAT) : date(self::DB_DATE_FORMAT, $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 +10 years'));
+ $dtend = $recurrence->end() ?: new DateTime('now +10 years');
+ $sql_data['dtend'] = $dtend->format(self::DB_DATE_FORMAT);
}
return $sql_data;
diff --git a/lib/plugins/libkolab/lib/kolab_storage_cache_task.php b/lib/plugins/libkolab/lib/kolab_storage_cache_task.php
index a1953f6..7bf5c79 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_cache_task.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_cache_task.php
@@ -35,9 +35,9 @@ class kolab_storage_cache_task extends kolab_storage_cache
$sql_data = parent::_serialize($object) + array('dtstart' => null, 'dtend' => null);
if ($object['start'])
- $sql_data['dtstart'] = date('Y-m-d H:i:s', is_object($object['start']) ? $object['start']->format('U') : $object['start']);
+ $sql_data['dtstart'] = is_object($object['start']) ? $object['start']->format(self::DB_DATE_FORMAT) : date(self::DB_DATE_FORMAT, $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']);
+ $sql_data['dtend'] = is_object($object['due']) ? $object['due']->format(self::DB_DATE_FORMAT) : date(self::DB_DATE_FORMAT, $object['due']);
return $sql_data;
}
diff --git a/lib/plugins/libkolab/lib/kolab_storage_dataset.php b/lib/plugins/libkolab/lib/kolab_storage_dataset.php
new file mode 100644
index 0000000..9ddf3f9
--- /dev/null
+++ b/lib/plugins/libkolab/lib/kolab_storage_dataset.php
@@ -0,0 +1,154 @@
+<?php
+
+/**
+ * Dataset class providing the results of a select operation on a kolab_storage_folder.
+ *
+ * Can be used as a normal array as well as an iterator in foreach() loops.
+ *
+ * @version @package_version@
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
+ *
+ * Copyright (C) 2014, 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_dataset implements Iterator, ArrayAccess, Countable
+{
+ private $cache; // kolab_storage_cache instance to use for fetching data
+ private $memlimit = 0;
+ private $buffer = false;
+ private $index = array();
+ private $data = array();
+ private $iteratorkey = 0;
+ private $error = null;
+
+ /**
+ * Default constructor
+ *
+ * @param object kolab_storage_cache instance to be used for fetching objects upon access
+ */
+ public function __construct($cache)
+ {
+ $this->cache = $cache;
+
+ // enable in-memory buffering up until 1/5 of the available memory
+ if (function_exists('memory_get_usage')) {
+ $this->memlimit = parse_bytes(ini_get('memory_limit')) / 5;
+ $this->buffer = true;
+ }
+ }
+
+ /**
+ * Return error state
+ */
+ public function is_error()
+ {
+ return !empty($this->error);
+ }
+
+ /**
+ * Set error state
+ */
+ public function set_error($err)
+ {
+ $this->error = $err;
+ }
+
+
+ /*** Implement PHP Countable interface ***/
+
+ public function count()
+ {
+ return count($this->index);
+ }
+
+
+ /*** Implement PHP ArrayAccess interface ***/
+
+ public function offsetSet($offset, $value)
+ {
+ $uid = $value['_msguid'];
+
+ if (is_null($offset)) {
+ $offset = count($this->index);
+ $this->index[] = $uid;
+ }
+ else {
+ $this->index[$offset] = $uid;
+ }
+
+ // keep full payload data in memory if possible
+ if ($this->memlimit && $this->buffer && isset($value['_mailbox'])) {
+ $this->data[$offset] = $value;
+
+ // check memory usage and stop buffering
+ if ($offset % 10 == 0) {
+ $this->buffer = memory_get_usage() < $this->memlimit;
+ }
+ }
+ }
+
+ public function offsetExists($offset)
+ {
+ return isset($this->index[$offset]);
+ }
+
+ public function offsetUnset($offset)
+ {
+ unset($this->index[$offset]);
+ }
+
+ public function offsetGet($offset)
+ {
+ if (isset($this->data[$offset])) {
+ return $this->data[$offset];
+ }
+ else if ($msguid = $this->index[$offset]) {
+ return $this->cache->get($msguid);
+ }
+
+ return null;
+ }
+
+
+ /*** Implement PHP Iterator interface ***/
+
+ public function current()
+ {
+ return $this->offsetGet($this->iteratorkey);
+ }
+
+ public function key()
+ {
+ return $this->iteratorkey;
+ }
+
+ public function next()
+ {
+ $this->iteratorkey++;
+ return $this->valid();
+ }
+
+ public function rewind()
+ {
+ $this->iteratorkey = 0;
+ }
+
+ public function valid()
+ {
+ return !empty($this->index[$this->iteratorkey]);
+ }
+
+}
diff --git a/lib/plugins/libkolab/lib/kolab_storage_folder.php b/lib/plugins/libkolab/lib/kolab_storage_folder.php
index 12da5e9..1580314 100644
--- a/lib/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/lib/plugins/libkolab/lib/kolab_storage_folder.php
@@ -74,9 +74,9 @@ class kolab_storage_folder
* @param string The folder name/path
* @param string Optional folder type if known
*/
- public function set_folder($name, $ftype = null)
+ public function set_folder($name, $type = null)
{
- $this->type_annotation = $ftype ? $ftype : kolab_storage::folder_type($name);
+ $this->type_annotation = $type ? $type : kolab_storage::folder_type($name);
$oldtype = $this->type;
list($this->type, $suffix) = explode('.', $this->type_annotation);
@@ -92,7 +92,6 @@ class kolab_storage_folder
$this->cache->set_folder($this);
}
-
/**
*
*/
@@ -424,6 +423,21 @@ class kolab_storage_folder
return $this->cache->select($this->_prepare_query($query), true);
}
+ /**
+ * Setter for ORDER BY and LIMIT parameters for cache queries
+ *
+ * @param array List of columns to order by
+ * @param integer Limit result set to this length
+ * @param integer Offset row
+ */
+ public function set_order_and_limit($sortcols, $length = null, $offset = 0)
+ {
+ $this->cache->set_order_by($sortcols);
+
+ if ($length !== null) {
+ $this->cache->set_limit($length, $offset);
+ }
+ }
/**
* Helper method to sanitize query arguments
@@ -753,24 +767,27 @@ class kolab_storage_folder
$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->cache->bypass(true);
- $this->imap->delete_message($object['_msguid'], $object['_mailbox']);
- $this->cache->bypass(false);
- $this->cache->set($object['_msguid'], false, $object['_mailbox']);
- }
-
// update cache with new UID
if ($result) {
+ $old_uid = $object['_msguid'];
+
$object['_msguid'] = $result;
$object['_mailbox'] = $this->name;
- $this->cache->insert($result, $object);
- // remove temp file
- if ($body_file) {
- @unlink($body_file);
+ if ($old_uid) {
+ // delete old message
+ $this->cache->bypass(true);
+ $this->imap->delete_message($old_uid, $object['_mailbox']);
+ $this->cache->bypass(false);
}
+
+ // insert/update message in cache
+ $this->cache->save($result, $object, $old_uid);
+ }
+
+ // remove temp file
+ if ($body_file) {
+ @unlink($body_file);
}
}
@@ -806,7 +823,7 @@ class kolab_storage_folder
$recurrence = new kolab_date_recurrence($object['_formatobj']);
if ($end = $recurrence->end()) {
unset($exception['recurrence']['COUNT']);
- $exception['recurrence']['UNTIL'] = new DateTime('@'.$end);
+ $exception['recurrence']['UNTIL'] = $end;
}
}