Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F256709
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
108 KB
Referenced Files
None
Subscribers
None
View Options
diff --git a/plugins/kolab_addressbook/drivers/kolab/kolab_contacts.php b/plugins/kolab_addressbook/drivers/kolab/kolab_contacts.php
index 369309ef..c125e334 100644
--- a/plugins/kolab_addressbook/drivers/kolab/kolab_contacts.php
+++ b/plugins/kolab_addressbook/drivers/kolab/kolab_contacts.php
@@ -1,1403 +1,1404 @@
<?php
/**
* Backend class for a custom address book
*
* This part of the Roundcube+Kolab integration and connects the
* rcube_addressbook interface with the kolab_storage wrapper from libkolab
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2011, 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/>.
*
* @see rcube_addressbook
*/
class kolab_contacts extends rcube_addressbook
{
public $primary_key = 'ID';
public $rights = 'lrs';
public $readonly = true;
public $undelete = true;
public $groups = true;
public $coltypes = array(
'name' => array('limit' => 1),
'firstname' => array('limit' => 1),
'surname' => array('limit' => 1),
'middlename' => array('limit' => 1),
'prefix' => array('limit' => 1),
'suffix' => array('limit' => 1),
'nickname' => array('limit' => 1),
'jobtitle' => array('limit' => 1),
'organization' => array('limit' => 1),
'department' => array('limit' => 1),
'email' => array('subtypes' => array('home','work','other')),
'phone' => array(),
'address' => array('subtypes' => array('home','work','office')),
'website' => array('subtypes' => array('homepage','blog')),
'im' => array('subtypes' => null),
'gender' => array('limit' => 1),
'birthday' => array('limit' => 1),
'anniversary' => array('limit' => 1),
'profession' => array(
'type' => 'text',
'size' => 40,
'maxlength' => 80,
'limit' => 1,
'label' => 'kolab_addressbook.profession',
'category' => 'personal'
),
'manager' => array('limit' => null),
'assistant' => array('limit' => null),
'spouse' => array('limit' => 1),
'children' => array(
'type' => 'text',
'size' => 40,
'maxlength' => 80,
'limit' => null,
'label' => 'kolab_addressbook.children',
'category' => 'personal'
),
'freebusyurl' => array(
'type' => 'text',
'size' => 40,
'limit' => 1,
'label' => 'kolab_addressbook.freebusyurl'
),
'pgppublickey' => array(
'type' => 'textarea',
'size' => 70,
'rows' => 10,
'limit' => 1,
'label' => 'kolab_addressbook.pgppublickey'
),
'pkcs7publickey' => array(
'type' => 'textarea',
'size' => 70,
'rows' => 10,
'limit' => 1,
'label' => 'kolab_addressbook.pkcs7publickey'
),
'notes' => array('limit' => 1),
'photo' => array('limit' => 1),
// TODO: define more Kolab-specific fields such as: language, latitude, longitude, crypto settings
);
/**
* vCard additional fields mapping
*/
public $vcard_map = array(
'profession' => 'X-PROFESSION',
'officelocation' => 'X-OFFICE-LOCATION',
'initials' => 'X-INITIALS',
'children' => 'X-CHILDREN',
'freebusyurl' => 'X-FREEBUSY-URL',
'pgppublickey' => 'KEY',
);
/**
* List of date type fields
*/
public $date_cols = array('birthday', 'anniversary');
private $gid;
private $storagefolder;
private $dataset;
private $sortindex;
private $contacts;
private $distlists;
private $groupmembers;
private $filter;
private $result;
private $namespace;
private $imap_folder = 'INBOX/Contacts';
private $action;
// list of fields used for searching in "All fields" mode
private $search_fields = array(
'name',
'firstname',
'surname',
'middlename',
'prefix',
'suffix',
'nickname',
'jobtitle',
'organization',
'department',
'email',
'phone',
'address',
'profession',
'manager',
'assistant',
'spouse',
'children',
'notes',
);
public function __construct($imap_folder = null)
{
if ($imap_folder) {
$this->imap_folder = $imap_folder;
}
// extend coltypes configuration
$format = kolab_format::factory('contact');
$this->coltypes['phone']['subtypes'] = array_keys($format->phonetypes);
$this->coltypes['address']['subtypes'] = array_keys($format->addresstypes);
$rcube = rcube::get_instance();
// set localized labels for proprietary cols
foreach ($this->coltypes as $col => $prop) {
if (!empty($prop['label']) && is_string($prop['label'])) {
$this->coltypes[$col]['label'] = $rcube->gettext($prop['label']);
}
}
// fetch objects from the given IMAP folder
$this->storagefolder = kolab_storage::get_folder($this->imap_folder);
$this->ready = $this->storagefolder && !PEAR::isError($this->storagefolder);
// Set readonly and rights flags according to folder permissions
if ($this->ready) {
if ($this->storagefolder->get_owner() == $_SESSION['username']) {
$this->readonly = false;
$this->rights = 'lrswikxtea';
}
else {
$rights = $this->storagefolder->get_myrights();
if ($rights && !PEAR::isError($rights)) {
$this->rights = $rights;
if (strpos($rights, 'i') !== false && strpos($rights, 't') !== false) {
$this->readonly = false;
}
}
}
}
$this->action = rcube::get_instance()->action;
}
/**
* Getter for the address book name to be displayed
*
* @return string Name of this address book
*/
public function get_name()
{
return $this->storagefolder->get_name();
}
/**
* Wrapper for kolab_storage_folder::get_foldername()
*/
public function get_foldername()
{
return $this->storagefolder->get_foldername();
}
/**
* Getter for the IMAP folder name
*
* @return string Name of the IMAP folder
*/
public function get_realname()
{
return $this->imap_folder;
}
/**
* Getter for the name of the namespace to which the IMAP folder belongs
*
* @return string Name of the namespace (personal, other, shared)
*/
public function get_namespace()
{
if ($this->namespace === null && $this->ready) {
$this->namespace = $this->storagefolder->get_namespace();
}
return $this->namespace;
}
/**
* Getter for parent folder path
*
* @return string Full path to parent folder
*/
public function get_parent()
{
return $this->storagefolder->get_parent();
}
/**
* Check subscription status of this folder
*
* @return boolean True if subscribed, false if not
*/
public function is_subscribed()
{
return kolab_storage::folder_is_subscribed($this->imap_folder);
}
/**
* Compose an URL for CardDAV access to this address book (if configured)
*/
public function get_carddav_url()
{
$rcmail = rcmail::get_instance();
if ($template = $rcmail->config->get('kolab_addressbook_carddav_url', null)) {
return strtr($template, array(
'%h' => $_SERVER['HTTP_HOST'],
'%u' => urlencode($rcmail->get_user_name()),
'%i' => urlencode($this->storagefolder->get_uid()),
'%n' => urlencode($this->imap_folder),
));
}
return false;
}
/**
* Setter for the current group
*/
public function set_group($gid)
{
$this->gid = $gid;
}
/**
* Save a search string for future listings
*
* @param mixed Search params to use in listing method, obtained by get_search_set()
*/
public function set_search_set($filter)
{
$this->filter = $filter;
}
/**
* Getter for saved search properties
*
* @return mixed Search properties used by this class
*/
public function get_search_set()
{
return $this->filter;
}
/**
* Reset saved results and search parameters
*/
public function reset()
{
$this->result = null;
$this->filter = null;
}
/**
* List all active contact groups of this source
*
* @param string Optional search string to match group name
* @param int Search mode. Sum of self::SEARCH_*
*
* @return array Indexed list of contact groups, each a hash array
*/
function list_groups($search = null, $mode = 0)
{
$this->_fetch_groups();
$groups = array();
foreach ((array)$this->distlists as $group) {
if (!$search || strstr(mb_strtolower($group['name']), mb_strtolower($search))) {
$groups[$group['ID']] = array('ID' => $group['ID'], 'name' => $group['name']);
}
}
// sort groups by name
uasort($groups, function($a, $b) { return strcoll($a['name'], $b['name']); });
return array_values($groups);
}
/**
* List the current set of contact records
*
* @param array List of cols to show
* @param int Only return this number of records, use negative values for tail
* @param bool True to skip the count query (select only)
*
* @return array Indexed list of contact records, each a hash array
*/
public function list_records($cols = null, $subset = 0, $nocount = false)
{
$this->result = new rcube_result_set(0, ($this->list_page-1) * $this->page_size);
$fetch_all = false;
$fast_mode = !empty($cols) && is_array($cols);
// list member of the selected group
if ($this->gid) {
$this->_fetch_groups();
$this->sortindex = array();
$this->contacts = array();
$local_sortindex = array();
$uids = array();
// get members with email specified
foreach ((array)$this->distlists[$this->gid]['member'] as $member) {
// skip member that don't match the search filter
if (!empty($this->filter['ids']) && array_search($member['ID'], $this->filter['ids']) === false) {
continue;
}
if (!empty($member['uid'])) {
$uids[] = $member['uid'];
}
else if (!empty($member['email'])) {
$this->contacts[$member['ID']] = $member;
$local_sortindex[$member['ID']] = $this->_sort_string($member);
$fetch_all = true;
}
}
// get members by UID
if (!empty($uids)) {
$this->_fetch_contacts($query = array(array('uid', '=', $uids)), $fetch_all ? false : count($uids), $fast_mode);
$this->sortindex = array_merge($this->sortindex, $local_sortindex);
}
}
else if ($this->filter && is_array($this->filter['ids'])) {
$ids = $this->filter['ids'];
if (count($ids)) {
$uids = array_map(array($this, 'id2uid'), $this->filter['ids']);
$this->_fetch_contacts($query = array(array('uid', '=', $uids)), count($ids), $fast_mode);
}
}
else {
$this->_fetch_contacts($query = 'contact', true, $fast_mode);
}
if ($fetch_all) {
// sort results (index only)
asort($this->sortindex, SORT_LOCALE_STRING);
$ids = array_keys($this->sortindex);
// fill contact data into the current result set
$this->result->count = count($ids);
$start_row = $subset < 0 ? $this->result->first + $this->page_size + $subset : $this->result->first;
$last_row = min($subset != 0 ? $start_row + abs($subset) : $this->result->first + $this->page_size, $this->result->count);
for ($i = $start_row; $i < $last_row; $i++) {
if (array_key_exists($i, $ids)) {
$idx = $ids[$i];
$this->result->add($this->contacts[$idx] ?: $this->_to_rcube_contact($this->dataset[$idx]));
}
}
}
else if (!empty($this->dataset)) {
// get all records count, skip the query if possible
if (!isset($query) || count($this->dataset) < $this->page_size) {
$this->result->count = count($this->dataset) + $this->page_size * ($this->list_page - 1);
}
else {
$this->result->count = $this->storagefolder->count($query);
}
$start_row = $subset < 0 ? $this->page_size + $subset : 0;
$last_row = min($subset != 0 ? $start_row + abs($subset) : $this->page_size, $this->result->count);
for ($i = $start_row; $i < $last_row; $i++) {
$this->result->add($this->_to_rcube_contact($this->dataset[$i]));
}
}
return $this->result;
}
/**
* Search records
*
* @param mixed $fields The field name of array of field names to search in
* @param mixed $value Search value (or array of values when $fields is array)
* @param int $mode Matching mode:
* 0 - partial (*abc*),
* 1 - strict (=),
* 2 - prefix (abc*)
* 4 - include groups (if supported)
* @param bool $select True if results are requested, False if count only
* @param bool $nocount True to skip the count query (select only)
* @param array $required List of fields that cannot be empty
*
* @return rcube_result_set List of contact records and 'count' value
*/
public function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array())
{
// search by ID
if ($fields == $this->primary_key) {
$ids = !is_array($value) ? explode(',', $value) : $value;
$result = new rcube_result_set();
foreach ($ids as $id) {
if ($rec = $this->get_record($id, true)) {
$result->add($rec);
$result->count++;
}
}
return $result;
}
else if ($fields == '*') {
$fields = $this->search_fields;
}
if (!is_array($fields)) {
$fields = array($fields);
}
if (!is_array($required) && !empty($required)) {
$required = array($required);
}
// advanced search
+ $advanced = false;
if (is_array($value)) {
$advanced = true;
$value = array_map('mb_strtolower', $value);
}
else {
$value = mb_strtolower($value);
}
$scount = count($fields);
// build key name regexp
$regexp = '/^(' . implode('|', $fields) . ')(?:.*)$/';
// pass query to storage if only indexed cols are involved
// NOTE: this is only some rough pre-filtering but probably includes false positives
$squery = $this->_search_query($fields, $value, $mode);
// add magic selector to select contacts with birthday dates only
if (in_array('birthday', $required)) {
$squery[] = array('tags', '=', 'x-has-birthday');
}
$squery[] = array('type', '=', 'contact');
// get all/matching records
$this->_fetch_contacts($squery);
// save searching conditions
$this->filter = array('fields' => $fields, 'value' => $value, 'mode' => $mode, 'ids' => array());
// search by iterating over all records in dataset
foreach ($this->dataset as $record) {
$contact = $this->_to_rcube_contact($record);
$id = $contact['ID'];
// check if current contact has required values, otherwise skip it
if ($required) {
foreach ($required as $f) {
// required field might be 'email', but contact might contain 'email:home'
if (!($v = rcube_addressbook::get_col_values($f, $contact, true)) || empty($v)) {
continue 2;
}
}
}
$found = array();
$contents = '';
foreach (preg_grep($regexp, array_keys($contact)) as $col) {
$pos = strpos($col, ':');
$colname = $pos ? substr($col, 0, $pos) : $col;
foreach ((array)$contact[$col] as $val) {
if ($advanced) {
$found[$colname] = $this->compare_search_value($colname, $val, $value[array_search($colname, $fields)], $mode);
}
else {
$contents .= ' ' . join(' ', (array)$val);
}
}
}
// compare matches
if (($advanced && count($found) >= $scount) ||
(!$advanced && rcube_utils::words_match(mb_strtolower($contents), $value))) {
$this->filter['ids'][] = $id;
}
}
// dummy result with contacts count
if (!$select) {
return new rcube_result_set(count($this->filter['ids']), ($this->list_page-1) * $this->page_size);
}
// list records (now limited by $this->filter)
return $this->list_records();
}
/**
* Refresh saved search results after data has changed
*/
public function refresh_search()
{
if ($this->filter) {
$this->search($this->filter['fields'], $this->filter['value'], $this->filter['mode']);
}
return $this->get_search_set();
}
/**
* Count number of available contacts in database
*
* @return rcube_result_set Result set with values for 'count' and 'first'
*/
public function count()
{
if ($this->gid) {
$this->_fetch_groups();
$count = count($this->distlists[$this->gid]['member']);
}
else if (isset($this->filter['ids']) && is_array($this->filter['ids'])) {
$count = count($this->filter['ids']);
}
else {
$count = $this->storagefolder->count('contact');
}
return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
}
/**
* Return the last result set
*
* @return rcube_result_set Current result set or NULL if nothing selected yet
*/
public function get_result()
{
return $this->result;
}
/**
* Get a specific contact record
*
* @param mixed Record identifier(s)
* @param bool True to return record as associative array, otherwise a result set is returned
*
* @return mixed Result object with all record fields or False if not found
*/
public function get_record($id, $assoc = false)
{
$rec = null;
$uid = $this->id2uid($id);
$rev = rcube_utils::get_input_value('_rev', rcube_utils::INPUT_GPC);
if (strpos($uid, 'mailto:') === 0) {
$this->_fetch_groups(true);
$rec = $this->contacts[$id];
$this->readonly = true; // set source to read-only
}
else if (!empty($rev)) {
$rcmail = rcube::get_instance();
$plugin = $rcmail->plugins->get_plugin('kolab_addressbook');
if ($plugin && ($object = $plugin->get_revision($id, kolab_storage::id_encode($this->imap_folder), $rev))) {
$rec = $this->_to_rcube_contact($object);
$rec['rev'] = $rev;
}
$this->readonly = true; // set source to read-only
}
else if ($object = $this->storagefolder->get_object($uid)) {
$rec = $this->_to_rcube_contact($object);
}
if ($rec) {
$this->result = new rcube_result_set(1);
$this->result->add($rec);
return $assoc ? $rec : $this->result;
}
return false;
}
/**
* Get group assignments of a specific contact record
*
* @param mixed Record identifier
*
* @return array List of assigned groups as ID=>Name pairs
*/
public function get_record_groups($id)
{
$out = array();
$this->_fetch_groups();
if (!empty($this->groupmembers[$id])) {
foreach ((array) $this->groupmembers[$id] as $gid) {
if (!empty($this->distlists[$gid])) {
$group = $this->distlists[$gid];
$out[$gid] = $group['name'];
}
}
}
return $out;
}
/**
* Create a new contact record
*
* @param array Associative array with save data
* Keys: Field name with optional section in the form FIELD:SECTION
* Values: Field value. Can be either a string or an array of strings for multiple values
* @param bool True to check for duplicates first
*
* @return mixed The created record ID on success, False on error
*/
public function insert($save_data, $check=false)
{
if (!is_array($save_data)) {
return false;
}
$insert_id = $existing = false;
// check for existing records by e-mail comparison
if ($check) {
foreach ($this->get_col_values('email', $save_data, true) as $email) {
if (($res = $this->search('email', $email, true, false)) && $res->count) {
$existing = true;
break;
}
}
}
if (!$existing) {
// remove existing id attributes (#1101)
unset($save_data['ID'], $save_data['uid']);
// generate new Kolab contact item
$object = $this->_from_rcube_contact($save_data);
$saved = $this->storagefolder->save($object, 'contact');
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving contact object to Kolab server"),
true, false);
}
else {
$insert_id = $this->uid2id($object['uid']);
}
}
return $insert_id;
}
/**
* Update a specific contact record
*
* @param mixed Record identifier
* @param array Associative array with save data
* Keys: Field name with optional section in the form FIELD:SECTION
* Values: Field value. Can be either a string or an array of strings for multiple values
*
* @return bool True on success, False on error
*/
public function update($id, $save_data)
{
$updated = false;
if ($old = $this->storagefolder->get_object($this->id2uid($id))) {
$object = $this->_from_rcube_contact($save_data, $old);
if (!$this->storagefolder->save($object, 'contact', $old['uid'])) {
rcube::raise_error(array(
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving contact object to Kolab server"
),
true, false
);
}
else {
$updated = true;
// TODO: update data in groups this contact is member of
}
}
return $updated;
}
/**
* Mark one or more contact records as deleted
*
* @param array Record identifiers
* @param bool Remove record(s) irreversible (mark as deleted otherwise)
*
* @return int Number of records deleted
*/
public function delete($ids, $force=true)
{
$this->_fetch_groups();
if (!is_array($ids)) {
$ids = explode(',', $ids);
}
$count = 0;
foreach ($ids as $id) {
if ($uid = $this->id2uid($id)) {
$is_mailto = strpos($uid, 'mailto:') === 0;
$deleted = $is_mailto || $this->storagefolder->delete($uid, $force);
if (!$deleted) {
rcube::raise_error(array(
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Error deleting a contact object $uid from the Kolab server"
),
true, false
);
}
else {
// remove from distribution lists
foreach ((array) $this->groupmembers[$id] as $gid) {
if (!$is_mailto || $gid == $this->gid) {
$this->remove_from_group($gid, $id);
}
}
// clear internal cache
unset($this->groupmembers[$id]);
$count++;
}
}
}
return $count;
}
/**
* Undelete one or more contact records.
* Only possible just after delete (see 2nd argument of delete() method).
*
* @param array Record identifiers
*
* @return int Number of records restored
*/
public function undelete($ids)
{
if (!is_array($ids)) {
$ids = explode(',', $ids);
}
$count = 0;
foreach ($ids as $id) {
$uid = $this->id2uid($id);
if ($this->storagefolder->undelete($uid)) {
$count++;
}
else {
rcube::raise_error(array(
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Error undeleting a contact object $uid from the Kolab server"
),
true, false
);
}
}
return $count;
}
/**
* Remove all records from the database
*
* @param bool $with_groups Remove also groups
*/
public function delete_all($with_groups = false)
{
if ($this->storagefolder->delete_all()) {
$this->contacts = array();
$this->sortindex = array();
$this->dataset = null;
$this->result = null;
}
}
/**
* Close connection to source
* Called on script shutdown
*/
public function close()
{
}
/**
* Create a contact group with the given name
*
* @param string The group name
*
* @return mixed False on error, array with record props in success
*/
function create_group($name)
{
$this->_fetch_groups();
$result = false;
$list = array(
'name' => $name,
'member' => array(),
);
$saved = $this->storagefolder->save($list, 'distribution-list');
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list object to Kolab server"
),
true, false
);
return false;
}
else {
$id = $this->uid2id($list['uid']);
$this->distlists[$id] = $list;
$result = array('id' => $id, 'name' => $name);
}
return $result;
}
/**
* Delete the given group and all linked group members
*
* @param string Group identifier
*
* @return bool True on success, false if no data was changed
*/
function delete_group($gid)
{
$this->_fetch_groups();
$result = false;
if ($list = $this->distlists[$gid]) {
$deleted = $this->storagefolder->delete($list['uid']);
}
if (!$deleted) {
rcube::raise_error(array(
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Error deleting distribution-list object from the Kolab server"
),
true, false
);
}
else {
$result = true;
}
return $result;
}
/**
* Rename a specific contact group
*
* @param string Group identifier
* @param string New name to set for this group
* @param string New group identifier (if changed, otherwise don't set)
*
* @return bool New name on success, false if no data was changed
*/
function rename_group($gid, $newname, &$newid)
{
$this->_fetch_groups();
$list = $this->distlists[$gid];
if ($newname != $list['name']) {
$list['name'] = $newname;
$saved = $this->storagefolder->save($list, 'distribution-list', $list['uid']);
}
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list object to Kolab server"
),
true, false
);
return false;
}
return $newname;
}
/**
* Add the given contact records the a certain group
*
* @param string Group identifier
* @param array List of contact identifiers to be added
* @return int Number of contacts added
*/
function add_to_group($gid, $ids)
{
if (!is_array($ids)) {
$ids = explode(',', $ids);
}
$this->_fetch_groups(true);
$list = $this->distlists[$gid];
$added = 0;
$uids = array();
$exists = array();
foreach ((array)$list['member'] as $member) {
$exists[] = $member['ID'];
}
// substract existing assignments from list
$ids = array_unique(array_diff($ids, $exists));
// add mailto: members
foreach ($ids as $contact_id) {
$uid = $this->id2uid($contact_id);
if (strpos($uid, 'mailto:') === 0 && ($contact = $this->contacts[$contact_id])) {
$list['member'][] = array(
'email' => $contact['email'],
'name' => $contact['name'],
);
$this->groupmembers[$contact_id][] = $gid;
$added++;
}
else {
$uids[$uid] = $contact_id;
}
}
// add members with UID
if (!empty($uids)) {
foreach ($uids as $uid => $contact_id) {
$list['member'][] = array('uid' => $uid);
$this->groupmembers[$contact_id][] = $gid;
$added++;
}
}
if ($added) {
$saved = $this->storagefolder->save($list, 'distribution-list', $list['uid']);
}
else {
$saved = true;
}
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list to Kolab server"
),
true, false
);
$added = false;
$this->set_error(self::ERROR_SAVING, 'errorsaving');
}
else {
$this->distlists[$gid] = $list;
}
return $added;
}
/**
* Remove the given contact records from a certain group
*
* @param string Group identifier
* @param array List of contact identifiers to be removed
* @return int Number of deleted group members
*/
function remove_from_group($gid, $ids)
{
if (!is_array($ids)) {
$ids = explode(',', $ids);
}
$this->_fetch_groups();
if (!($list = $this->distlists[$gid])) {
return false;
}
$new_member = array();
foreach ((array)$list['member'] as $member) {
if (!in_array($member['ID'], $ids)) {
$new_member[] = $member;
}
}
// write distribution list back to server
$list['member'] = $new_member;
$saved = $this->storagefolder->save($list, 'distribution-list', $list['uid']);
if (!$saved) {
rcube::raise_error(array(
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving distribution-list object to Kolab server"
),
true, false
);
}
else {
// remove group assigments in local cache
foreach ($ids as $id) {
$j = array_search($gid, $this->groupmembers[$id]);
unset($this->groupmembers[$id][$j]);
}
$this->distlists[$gid] = $list;
return true;
}
return false;
}
/**
* Check the given data before saving.
* If input not valid, the message to display can be fetched using get_error()
*
* @param array Associative array with contact data to save
* @param bool Attempt to fix/complete data automatically
*
* @return bool True if input is valid, False if not.
*/
public function validate(&$save_data, $autofix = false)
{
// validate e-mail addresses
$valid = parent::validate($save_data);
// require at least one e-mail address if there's no name
// (syntax check is already done)
if ($valid) {
if (!strlen($save_data['name'])
&& !strlen($save_data['organization'])
&& !array_filter($this->get_col_values('email', $save_data, true))
) {
$this->set_error('warning', 'kolab_addressbook.noemailnamewarning');
$valid = false;
}
}
return $valid;
}
/**
* Query storage layer and store records in private member var
*/
private function _fetch_contacts($query = array(), $limit = false, $fast_mode = false)
{
if (!isset($this->dataset) || !empty($query)) {
if ($limit) {
$size = is_int($limit) && $limit < $this->page_size ? $limit : $this->page_size;
$this->storagefolder->set_order_and_limit($this->_sort_columns(), $size, ($this->list_page-1) * $this->page_size);
}
$this->sortindex = array();
$this->dataset = $this->storagefolder->select($query, $fast_mode);
foreach ($this->dataset as $idx => $record) {
$contact = $this->_to_rcube_contact($record);
$this->sortindex[$idx] = $this->_sort_string($contact);
}
}
}
/**
* Extract a string for sorting from the given contact record
*/
private function _sort_string($rec)
{
$str = '';
switch ($this->sort_col) {
case 'name':
$str = ($rec['name'] ?? '') . ($rec['prefix'] ?? '');
case 'firstname':
$str .= ($rec['firstname'] ?? '') . ($rec['middlename'] ?? '') . ($rec['surname'] ?? '');
break;
case 'surname':
$str = ($rec['surname'] ?? '') . ($rec['firstname'] ?? '') . ($rec['middlename'] ?? '');
break;
default:
$str = $rec[$this->sort_col];
break;
}
if (!empty($rec['email'])) {
$str .= is_array($rec['email']) ? $rec['email'][0] : $rec['email'];
}
return mb_strtolower($str);
}
/**
* Return the cache table columns to order by
*/
private function _sort_columns()
{
$sortcols = array();
switch ($this->sort_col) {
case 'name':
$sortcols[] = 'name';
case 'firstname':
$sortcols[] = 'firstname';
break;
case 'surname':
$sortcols[] = 'surname';
break;
}
$sortcols[] = 'email';
return $sortcols;
}
/**
* Read distribution-lists AKA groups from server
*/
private function _fetch_groups($with_contacts = false)
{
if (!isset($this->distlists)) {
$this->distlists = $this->groupmembers = array();
foreach ($this->storagefolder->select('distribution-list', true) as $record) {
$record['ID'] = $this->uid2id($record['uid']);
foreach ((array)$record['member'] as $i => $member) {
$mid = $this->uid2id($member['uid'] ? $member['uid'] : 'mailto:' . $member['email']);
$record['member'][$i]['ID'] = $mid;
$record['member'][$i]['readonly'] = empty($member['uid']);
$this->groupmembers[$mid][] = $record['ID'];
if ($with_contacts && empty($member['uid'])) {
$this->contacts[$mid] = $record['member'][$i];
}
}
$this->distlists[$record['ID']] = $record;
}
}
}
/**
* Encode object UID into a safe identifier
*/
public function uid2id($uid)
{
return rtrim(strtr(base64_encode($uid), '+/', '-_'), '=');
}
/**
* Convert Roundcube object identifier back into the original UID
*/
public function id2uid($id)
{
return base64_decode(str_pad(strtr($id, '-_', '+/'), strlen($id) % 4, '=', STR_PAD_RIGHT));
}
/**
* Build SQL query for fulltext matches
*/
private function _search_query($fields, $value, $mode)
{
$query = array();
$cols = array();
// $fulltext_cols might contain composite field names e.g. 'email:address' while $fields not
foreach (kolab_format_contact::$fulltext_cols as $col) {
if ($pos = strpos($col, ':')) {
$col = substr($col, 0, $pos);
}
if (in_array($col, $fields)) {
$cols[] = $col;
}
}
if (count($cols) == count($fields)) {
if ($mode & rcube_addressbook::SEARCH_STRICT) {
$prefix = '^'; $suffix = '$';
}
else if ($mode & rcube_addressbook::SEARCH_PREFIX) {
$prefix = '^'; $suffix = '';
}
else {
$prefix = ''; $suffix = '';
}
$search_string = is_array($value) ? join(' ', $value) : $value;
foreach (rcube_utils::normalize_string($search_string, true) as $word) {
$query[] = array('words', 'LIKE', $prefix . $word . $suffix);
}
}
return $query;
}
/**
* Map fields from internal Kolab_Format to Roundcube contact format
*/
private function _to_rcube_contact($record)
{
$record['ID'] = $this->uid2id($record['uid']);
// convert email, website, phone values
foreach (array('email'=>'address', 'website'=>'url', 'phone'=>'number') as $col => $propname) {
if (isset($record[$col]) && is_array($record[$col])) {
$values = $record[$col];
unset($record[$col]);
foreach ((array)$values as $i => $val) {
$key = $col . ($val['type'] ? ':' . $val['type'] : '');
$record[$key][] = $val[$propname];
}
}
}
if (isset($record['address']) && is_array($record['address'])) {
$addresses = $record['address'];
unset($record['address']);
foreach ($addresses as $i => $adr) {
$key = 'address' . ($adr['type'] ? ':' . $adr['type'] : '');
$record[$key][] = array(
'street' => $adr['street'],
'locality' => $adr['locality'],
'zipcode' => $adr['code'],
'region' => $adr['region'],
'country' => $adr['country'],
);
}
}
// photo is stored as separate attachment
if (!empty($record['photo']) && strlen($record['photo']) < 255 && !empty($record['_attachments'][$record['photo']])) {
$att = $record['_attachments'][$record['photo']];
// only fetch photo content if requested
if ($this->action == 'photo') {
if (!empty($att['content'])) {
$record['photo'] = $att['content'];
}
else {
$record['photo'] = $this->storagefolder->get_attachment($record['uid'], $att['id']);
}
}
}
// truncate publickey value for display
if (!empty($record['pgppublickey']) && $this->action == 'show') {
$record['pgppublickey'] = substr($record['pgppublickey'], 0, 140) . '...';
}
// remove empty fields
$record = array_filter($record);
// remove kolab_storage internal data
unset($record['_msguid'], $record['_formatobj'], $record['_mailbox'], $record['_type'], $record['_size']);
return $record;
}
/**
* Map fields from Roundcube format to internal kolab_format_contact properties
*/
private function _from_rcube_contact($contact, $old = array())
{
if (empty($contact['uid']) && !empty($contact['ID'])) {
$contact['uid'] = $this->id2uid($contact['ID']);
}
else if (empty($contact['uid']) && !empty($old['uid'])) {
$contact['uid'] = $old['uid'];
}
$contact['im'] = array_filter($this->get_col_values('im', $contact, true));
// convert email, website, phone values
foreach (array('email'=>'address', 'website'=>'url', 'phone'=>'number') as $col => $propname) {
$col_values = $this->get_col_values($col, $contact);
$contact[$col] = array();
foreach ($col_values as $type => $values) {
foreach ((array)$values as $val) {
if (!empty($val)) {
$contact[$col][] = array($propname => $val, 'type' => $type);
}
}
unset($contact[$col.':'.$type]);
}
}
$addresses = array();
foreach ($this->get_col_values('address', $contact) as $type => $values) {
foreach ((array)$values as $adr) {
// skip empty address
$adr = array_filter($adr);
if (empty($adr)) {
continue;
}
$addresses[] = array(
'type' => $type,
'street' => $adr['street'],
'locality' => $adr['locality'],
'code' => $adr['zipcode'],
'region' => $adr['region'],
'country' => $adr['country'],
);
}
unset($contact['address:'.$type]);
}
$contact['address'] = $addresses;
// categories are not supported in the web client but should be preserved (#2608)
$contact['categories'] = $old['categories'] ?? null;
// copy meta data (starting with _) from old object
foreach ((array)$old as $key => $val) {
if (!isset($contact[$key]) && $key[0] == '_') {
$contact[$key] = $val;
}
}
// convert one-item-array elements into string element
// this is needed e.g. to properly import birthday field
foreach ($this->coltypes as $type => $col_def) {
if (($col_def['limit'] ?? 0) == 1 && !empty($contact[$type]) && is_array($contact[$type])) {
$contact[$type] = array_shift(array_filter($contact[$type]));
}
}
// When importing contacts 'vcard' data is added, we don't need it (Bug #1711)
unset($contact['vcard']);
// add empty values for some fields which can be removed in the UI
return array_filter($contact) + array(
'nickname' => '',
'birthday' => '',
'anniversary' => '',
'freebusyurl' => '',
'photo' => $contact['photo']
);
}
}
diff --git a/plugins/libkolab/lib/kolab_format_contact.php b/plugins/libkolab/lib/kolab_format_contact.php
index bf82aa03..82e5bee3 100644
--- a/plugins/libkolab/lib/kolab_format_contact.php
+++ b/plugins/libkolab/lib/kolab_format_contact.php
@@ -1,482 +1,483 @@
<?php
/**
* Kolab Contact model class
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_format_contact extends kolab_format
{
public $CTYPE = 'application/vcard+xml';
public $CTYPEv2 = 'application/x-vnd.kolab.contact';
protected $objclass = 'Contact';
protected $read_func = 'readContact';
protected $write_func = 'writeContact';
public static $fulltext_cols = array('name', 'firstname', 'surname', 'middlename', 'email:address');
public $phonetypes = array(
'home' => Telephone::Home,
'work' => Telephone::Work,
'text' => Telephone::Text,
'main' => Telephone::Voice,
'homefax' => Telephone::Fax,
'workfax' => Telephone::Fax,
'mobile' => Telephone::Cell,
'video' => Telephone::Video,
'pager' => Telephone::Pager,
'car' => Telephone::Car,
'other' => Telephone::Textphone,
);
public $emailtypes = array(
'home' => Email::Home,
'work' => Email::Work,
'other' => Email::NoType,
);
public $addresstypes = array(
'home' => Address::Home,
'work' => Address::Work,
'office' => 0,
);
private $gendermap = array(
'female' => Contact::Female,
'male' => Contact::Male,
);
private $relatedmap = array(
'manager' => Related::Manager,
'assistant' => Related::Assistant,
'spouse' => Related::Spouse,
'children' => Related::Child,
);
/**
* Default constructor
*/
function __construct($xmldata = null, $version = 3.0)
{
parent::__construct($xmldata, $version);
// complete phone types
$this->phonetypes['homefax'] |= Telephone::Home;
$this->phonetypes['workfax'] |= Telephone::Work;
}
/**
* Set contact properties to the kolabformat object
*
* @param array Contact data as hash array
*/
public function set(&$object)
{
// set common object properties
parent::set($object);
// do the hard work of setting object values
$nc = new NameComponents;
$nc->setSurnames(self::array2vector($object['surname'] ?? null));
$nc->setGiven(self::array2vector($object['firstname'] ?? null));
$nc->setAdditional(self::array2vector($object['middlename'] ?? null));
$nc->setPrefixes(self::array2vector($object['prefix'] ?? null));
$nc->setSuffixes(self::array2vector($object['suffix'] ?? null));
$this->obj->setNameComponents($nc);
$this->obj->setName($object['name'] ?? null);
$this->obj->setCategories(self::array2vector($object['categories'] ?? null));
if (isset($object['nickname']))
$this->obj->setNickNames(self::array2vector($object['nickname']));
if (isset($object['jobtitle']))
$this->obj->setTitles(self::array2vector($object['jobtitle']));
// organisation related properties (affiliation)
$org = new Affiliation;
$offices = new vectoraddress;
if (!empty($object['organization']))
$org->setOrganisation($object['organization']);
if (!empty($object['department']))
$org->setOrganisationalUnits(self::array2vector($object['department']));
if (!empty($object['profession']))
$org->setRoles(self::array2vector($object['profession']));
$rels = new vectorrelated;
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);
// im, email, url
$this->obj->setIMaddresses(self::array2vector($object['im'] ?? null));
if (class_exists('vectoremail')) {
$vemails = new vectoremail;
foreach ((array)($object['email'] ?? []) as $email) {
$type = $this->emailtypes[$email['type']];
$vemails->push(new Email($email['address'], intval($type)));
}
}
else {
$vemails = self::array2vector(array_map(function($v){ return $v['address']; }, $object['email']));
}
$this->obj->setEmailAddresses($vemails);
$vurls = new vectorurl;
foreach ((array)($object['website'] ?? []) as $url) {
$type = $url['type'] == 'blog' ? Url::Blog : Url::NoType;
$vurls->push(new Url($url['url'], $type));
}
$this->obj->setUrls($vurls);
// addresses
$adrs = new vectoraddress;
foreach ((array)($object['address'] ?? [])as $address) {
$adr = new Address;
$type = $this->addresstypes[$address['type']];
if (isset($type))
$adr->setTypes($type);
else if (!empty($address['type']))
$adr->setLabel($address['type']);
if (!empty($address['street']))
$adr->setStreet($address['street']);
if (!empty($address['locality']))
$adr->setLocality($address['locality']);
if (!empty($address['code']))
$adr->setCode($address['code']);
if (!empty($address['region']))
$adr->setRegion($address['region']);
if (!empty($address['country']))
$adr->setCountry($address['country']);
if (($address['type'] ?? null) == 'office')
$offices->push($adr);
else
$adrs->push($adr);
}
$this->obj->setAddresses($adrs);
$org->setAddresses($offices);
// add org affiliation after addresses are set
$orgs = new vectoraffiliation;
$orgs->push($org);
$this->obj->setAffiliations($orgs);
// telephones
$tels = new vectortelephone;
foreach ((array)($object['phone'] ?? []) as $phone) {
$tel = new Telephone;
if (isset($this->phonetypes[$phone['type'] ?? null]))
$tel->setTypes($this->phonetypes[$phone['type']]);
$tel->setNumber($phone['number'] ?? null);
$tels->push($tel);
}
$this->obj->setTelephones($tels);
if (isset($object['gender']))
$this->obj->setGender($this->gendermap[$object['gender']] ? $this->gendermap[$object['gender']] : Contact::NotSet);
if (isset($object['notes']))
$this->obj->setNote($object['notes']);
if (isset($object['freebusyurl']))
$this->obj->setFreeBusyUrl($object['freebusyurl']);
if (isset($object['lang']))
$this->obj->setLanguages(self::array2vector($object['lang']));
if (isset($object['birthday']))
$this->obj->setBDay(self::get_datetime($object['birthday'], false, true));
if (isset($object['anniversary']))
$this->obj->setAnniversary(self::get_datetime($object['anniversary'], false, true));
if (!empty($object['photo'])) {
if ($type = rcube_mime::image_content_type($object['photo']))
$this->obj->setPhoto($object['photo'], $type);
}
else if (isset($object['photo']))
$this->obj->setPhoto('','');
else if ($this->obj->photoMimetype()) // load saved photo for caching
$object['photo'] = $this->obj->photo();
// spouse and children are relateds
$rels = new vectorrelated;
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));
}
}
}
// add other relateds
if (!empty($object['related']) && is_array($object['related'])) {
foreach ($object['related'] as $value) {
$rels->push(new Related(Related::Text, $value));
}
}
$this->obj->setRelateds($rels);
// insert/replace crypto keys
$pgp_index = $pkcs7_index = -1;
$keys = $this->obj->keys();
for ($i=0; $i < $keys->size(); $i++) {
$key = $keys->get($i);
if ($pgp_index < 0 && $key->type() == Key::PGP)
$pgp_index = $i;
else if ($pkcs7_index < 0 && $key->type() == Key::PKCS7_MIME)
$pkcs7_index = $i;
}
$pgpkey = !empty($object['pgppublickey']) ? new Key($object['pgppublickey'], Key::PGP) : new Key();
$pkcs7key = !empty($object['pkcs7publickey']) ? new Key($object['pkcs7publickey'], Key::PKCS7_MIME) : new Key();
if ($pgp_index >= 0)
$keys->set($pgp_index, $pgpkey);
else if (!empty($object['pgppublickey']))
$keys->push($pgpkey);
if ($pkcs7_index >= 0)
$keys->set($pkcs7_index, $pkcs7key);
else if (!empty($object['pkcs7publickey']))
$keys->push($pkcs7key);
$this->obj->setKeys($keys);
// TODO: handle language, gpslocation, etc.
// set type property for proper caching
$object['_type'] = 'contact';
// cache this data
$this->data = $object;
unset($this->data['_formatobj']);
}
/**
*
*/
public function is_valid()
{
return !$this->formaterror && ($this->data || (is_object($this->obj) && $this->obj->uid() /*$this->obj->isValid()*/));
}
/**
* Convert the Contact object into a hash array data structure
*
* @param array Additional data for merge
*
* @return array Contact data as hash array
*/
public function to_array($data = array())
{
// return cached result
if (!empty($this->data))
return $this->data;
// read common object props into local data object
$object = parent::to_array($data);
$object['name'] = $this->obj->name();
$nc = $this->obj->nameComponents();
$object['surname'] = join(' ', self::vector2array($nc->surnames()));
$object['firstname'] = join(' ', self::vector2array($nc->given()));
$object['middlename'] = join(' ', self::vector2array($nc->additional()));
$object['prefix'] = join(' ', self::vector2array($nc->prefixes()));
$object['suffix'] = join(' ', self::vector2array($nc->suffixes()));
$object['nickname'] = join(' ', self::vector2array($this->obj->nickNames()));
$object['jobtitle'] = join(' ', self::vector2array($this->obj->titles()));
$object['categories'] = self::vector2array($this->obj->categories());
// organisation related properties (affiliation)
$orgs = $this->obj->affiliations();
+ $org = null;
if ($orgs->size()) {
$org = $orgs->get(0);
$object['organization'] = $org->organisation();
$object['profession'] = join(' ', self::vector2array($org->roles()));
$object['department'] = join(' ', self::vector2array($org->organisationalUnits()));
$this->read_relateds($org->relateds(), $object);
}
$object['im'] = self::vector2array($this->obj->imAddresses());
$emails = $this->obj->emailAddresses();
if ($emails instanceof vectoremail) {
$emailtypes = array_flip($this->emailtypes);
for ($i=0; $i < $emails->size(); $i++) {
$email = $emails->get($i);
$object['email'][] = array('address' => $email->address(), 'type' => $emailtypes[$email->types()]);
}
}
else {
$object['email'] = self::vector2array($emails);
}
$urls = $this->obj->urls();
for ($i=0; $i < $urls->size(); $i++) {
$url = $urls->get($i);
$subtype = $url->type() == Url::Blog ? 'blog' : 'homepage';
$object['website'][] = array('url' => $url->url(), 'type' => $subtype);
}
// addresses
$this->read_addresses($this->obj->addresses(), $object);
if ($org && ($offices = $org->addresses()))
$this->read_addresses($offices, $object, 'office');
// telehones
$tels = $this->obj->telephones();
$teltypes = array_flip($this->phonetypes);
for ($i=0; $i < $tels->size(); $i++) {
$tel = $tels->get($i);
$object['phone'][] = array('number' => $tel->number(), 'type' => $teltypes[$tel->types()]);
}
$object['notes'] = $this->obj->note();
$object['freebusyurl'] = $this->obj->freeBusyUrl();
$object['lang'] = self::vector2array($this->obj->languages());
if ($bday = self::php_datetime($this->obj->bDay()))
$object['birthday'] = $bday;
if ($anniversary = self::php_datetime($this->obj->anniversary()))
$object['anniversary'] = $anniversary;
$gendermap = array_flip($this->gendermap);
if (($g = $this->obj->gender()) && $gendermap[$g])
$object['gender'] = $gendermap[$g];
if ($this->obj->photoMimetype())
$object['photo'] = $this->obj->photo();
else if ($this->xmlobject && ($photo_name = $this->xmlobject->pictureAttachmentName()))
$object['photo'] = $photo_name;
// relateds -> spouse, children
$this->read_relateds($this->obj->relateds(), $object, 'related');
// crypto settings: currently only key values are supported
$keys = $this->obj->keys();
for ($i=0; is_object($keys) && $i < $keys->size(); $i++) {
$key = $keys->get($i);
if ($key->type() == Key::PGP)
$object['pgppublickey'] = $key->key();
else if ($key->type() == Key::PKCS7_MIME)
$object['pkcs7publickey'] = $key->key();
}
$this->data = $object;
return $this->data;
}
/**
* Callback for kolab_storage_cache to get words to index for fulltext search
*
* @return array List of words to save in cache
*/
public function get_words()
{
$data = '';
foreach (self::$fulltext_cols as $colname) {
list($col, $field) = array_pad(explode(':', $colname), 2, null);
if ($field) {
$a = array();
foreach ((array)($this->data[$col] ?? []) as $attr)
$a[] = $attr[$field];
$val = join(' ', $a);
}
else {
$val = is_array($this->data[$col] ?? null) ? join(' ', $this->data[$col] ?? null) : ($this->data[$col] ?? null);
}
if (strlen($val))
$data .= $val . ' ';
}
return array_unique(rcube_utils::normalize_string($data, true));
}
/**
* 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)
{
$adrtypes = array_flip($this->addresstypes);
for ($i=0; $i < $addresses->size(); $i++) {
$adr = $addresses->get($i);
$object['address'][] = array(
'type' => $type ? $type : ($adrtypes[$adr->types()] ? $adrtypes[$adr->types()] : ''), /*$adr->label()),*/
'street' => $adr->street(),
'code' => $adr->code(),
'locality' => $adr->locality(),
'region' => $adr->region(),
'country' => $adr->country()
);
}
}
/**
* Helper method to map contents of a Related vector to the contact data object
*/
private function read_relateds($rels, &$object, $catchall = null)
{
$typemap = array_flip($this->relatedmap);
for ($i=0; $i < $rels->size(); $i++) {
$rel = $rels->get($i);
if ($rel->type() != Related::Text) // we can't handle UID relations yet
continue;
$known = false;
$types = $rel->relationTypes();
foreach ($typemap as $t => $field) {
if ($types & $t) {
$object[$field][] = $rel->text();
$known = true;
break;
}
}
if (!$known && $catchall) {
$object[$catchall][] = $rel->text();
}
}
}
}
diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php
index 4a30b8ba..55457621 100644
--- a/plugins/libkolab/lib/kolab_storage_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_folder.php
@@ -1,1175 +1,1175 @@
<?php
/**
* The kolab_storage_folder class represents an IMAP folder on the Kolab server.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2012-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_folder extends kolab_storage_folder_api
{
/**
* The kolab_storage_cache instance for caching operations
* @var object
*/
public $cache;
/**
* Indicate validity status
* @var boolean
*/
public $valid = false;
protected $error = 0;
protected $resource_uri;
/**
* Default constructor
*
* @param string The folder name/path
* @param string Expected folder type
* @param string Optional folder type if known
*/
function __construct($name, $type = null, $type_annotation = null)
{
parent::__construct($name);
$this->set_folder($name, $type, $type_annotation);
}
/**
* Set the IMAP folder this instance connects to
*
* @param string The folder name/path
* @param string Expected folder type
* @param string Optional folder type if known
*/
public function set_folder($name, $type = null, $type_annotation = null)
{
$this->name = $name;
if (empty($type_annotation)) {
$type_annotation = $this->get_type();
}
$oldtype = $this->type;
- list($this->type, $suffix) = explode('.', $type_annotation);
+ list($this->type, $suffix) = array_pad(explode('.', $type_annotation), 2, null);
$this->default = $suffix == 'default';
$this->subtype = $this->default ? '' : $suffix;
$this->id = kolab_storage::folder_id($name);
$this->valid = !empty($this->type) && $this->type != 'mail' && (!$type || $this->type == $type);
if (!$this->valid) {
$this->error = $this->imap->get_error_code() < 0 ? kolab_storage::ERROR_IMAP_CONN : kolab_storage::ERROR_INVALID_FOLDER;
}
// reset cached object properties
$this->owner = $this->namespace = $this->resource_uri = $this->info = $this->idata = null;
// get a new cache instance if folder type changed
if (!$this->cache || $this->type != $oldtype)
$this->cache = kolab_storage_cache::factory($this);
else
$this->cache->set_folder($this);
$this->imap->set_folder($this->name);
}
/**
* Returns code of last error
*
* @return int Error code
*/
public function get_error()
{
return $this->error ?: $this->cache->get_error();
}
/**
* Check IMAP connection error state
*/
public 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;
}
}
return $this->error;
}
/**
* Compose a unique resource URI for this IMAP folder
*/
public function get_resource_uri()
{
if (!empty($this->resource_uri)) {
return $this->resource_uri;
}
// strip namespace prefix from folder name
$ns = $this->get_namespace();
$nsdata = $this->imap->get_namespace($ns);
if (is_array($nsdata[0]) && strlen($nsdata[0][0]) && strpos($this->name, $nsdata[0][0]) === 0) {
$subpath = substr($this->name, strlen($nsdata[0][0]));
if ($ns == 'other') {
list($user, $suffix) = explode($nsdata[0][1], $subpath, 2);
$subpath = $suffix;
}
}
else {
$subpath = $this->name;
}
// compose fully qualified ressource uri for this instance
$this->resource_uri = 'imap://' . urlencode($this->get_owner(true)) . '@' . $this->imap->options['host'] . '/' . $subpath;
return $this->resource_uri;
}
/**
* Helper method to extract folder UID metadata
*
* @return string Folder's UID
*/
public function get_uid()
{
// UID is defined in folder METADATA
$metakeys = array(kolab_storage::UID_KEY_SHARED, kolab_storage::UID_KEY_CYRUS);
$metadata = $this->get_metadata();
if ($metadata !== null) {
foreach ($metakeys as $key) {
if ($uid = ($metadata[$key] ?? null)) {
return $uid;
}
}
// generate a folder UID and set it to IMAP
$uid = rtrim(chunk_split(md5($this->name . $this->get_owner() . uniqid('-', true)), 12, '-'), '-');
if ($this->set_uid($uid)) {
return $uid;
}
}
$this->check_error();
// create hash from folder name if we can't write the UID metadata
return md5($this->name . $this->get_owner());
}
/**
* Helper method to set an UID value to the given IMAP folder instance
*
* @param string Folder's UID
* @return boolean True on succes, False on failure
*/
public function set_uid($uid)
{
$success = $this->set_metadata(array(kolab_storage::UID_KEY_SHARED => $uid));
$this->check_error();
return $success;
}
/**
* Compose a folder Etag identifier
*/
public function get_ctag()
{
$fdata = $this->get_imap_data();
$this->check_error();
return sprintf('%d-%d-%d', $fdata['UIDVALIDITY'] ?? 0, $fdata['HIGHESTMODSEQ'] ?? 0, $fdata['UIDNEXT'] ?? 0);
}
/**
* Check activation status of this folder
*
* @return boolean True if enabled, false if not
*/
public function is_active()
{
return kolab_storage::folder_is_active($this->name);
}
/**
* Change activation status of this folder
*
* @param boolean The desired subscription status: true = active, false = not active
*
* @return True on success, false on error
*/
public function activate($active)
{
return $active ? kolab_storage::folder_activate($this->name) : kolab_storage::folder_deactivate($this->name);
}
/**
* Check subscription status of this folder
*
* @return boolean True if subscribed, false if not
*/
public function is_subscribed()
{
return kolab_storage::folder_is_subscribed($this->name);
}
/**
* Change subscription status of this folder
*
* @param boolean The desired subscription status: true = subscribed, false = not subscribed
*
* @return True on success, false on error
*/
public function subscribe($subscribed)
{
return $subscribed ? kolab_storage::folder_subscribe($this->name) : kolab_storage::folder_unsubscribe($this->name);
}
/**
* Get number of objects stored in this folder
*
* @param mixed Pseudo-SQL query as list of filter parameter triplets
* or string with object type (e.g. contact, event, todo, journal, note, configuration)
*
* @return integer The number of objects of the given type
* @see self::select()
*/
public function count($query = null)
{
if (!$this->valid) {
return 0;
}
// synchronize cache first
$this->cache->synchronize();
return $this->cache->count($this->_prepare_query($query));
}
/**
* 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_object($uid)
{
if (!$this->valid || !$uid) {
return false;
}
// synchronize caches
$this->cache->synchronize();
return $this->cache->get_by_uid($uid);
}
/**
* List Kolab objects matching the given query
*
* @param mixed Pseudo-SQL query as list of filter parameter triplets
* or string with object type (e.g. contact, event, todo, journal, note, configuration)
*
* @return array List of Kolab data objects (each represented as hash array)
* @deprecated Use select()
*/
public function get_objects($query = array())
{
return $this->select($query);
}
/**
* Select Kolab objects matching the given query
*
* @param mixed Pseudo-SQL query as list of filter parameter triplets
* or string with object type (e.g. contact, event, todo, journal, note, configuration)
* @param boolean Use fast mode to fetch only minimal set of information
* (no xml fetching and parsing, etc.)
*
* @return array List of Kolab data objects (each represented as hash array)
*/
public function select($query = array(), $fast = false)
{
if (!$this->valid) {
return array();
}
// synchronize caches
$this->cache->synchronize();
// fetch objects from cache
return $this->cache->select($this->_prepare_query($query), false, $fast);
}
/**
* Getter for object UIDs only
*
* @param array Pseudo-SQL query as list of filter parameter triplets
* @return array List of Kolab object UIDs
*/
public function get_uids($query = array())
{
if (!$this->valid) {
return array();
}
// synchronize caches
$this->cache->synchronize();
// fetch UIDs from cache
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
*/
private function _prepare_query($query)
{
// string equals type query
// FIXME: should not be called this way!
if (is_string($query)) {
return $this->cache->has_type_col() && !empty($query) ? array(array('type','=',$query)) : array();
}
foreach ((array)$query as $i => $param) {
if ($param[0] == 'type' && !$this->cache->has_type_col()) {
unset($query[$i]);
}
else if (($param[0] == 'dtstart' || $param[0] == 'dtend' || $param[0] == 'changed')) {
if (is_object($param[2]) && $param[2] instanceof DateTimeInterface) {
$param[2] = $param[2]->format('U');
}
if (is_numeric($param[2])) {
$query[$i][2] = date('Y-m-d H:i:s', $param[2]);
}
}
}
return $query;
}
/**
* Fetch a Kolab object attachment which is stored in a separate part
* of the mail MIME message that represents the Kolab record.
*
* @param string Object's UID
* @param string The attachment's mime number
* @param string IMAP folder where message is stored;
* If set, that also implies that the given UID is an IMAP UID
* @param bool True to print the part content
* @param resource File pointer to save the message part
* @param boolean Disables charset conversion
*
* @return mixed The attachment content as binary string
*/
public function get_attachment($uid, $part, $mailbox = null, $print = false, $fp = null, $skip_charset_conv = false)
{
if ($this->valid && ($msguid = ($mailbox ? $uid : $this->cache->uid2msguid($uid)))) {
$this->imap->set_folder($mailbox ? $mailbox : $this->name);
if (substr($part, 0, 2) == 'i:') {
// attachment data is stored in XML
if ($object = $this->cache->get($msguid)) {
// load data from XML (attachment content is not stored in cache)
if ($object['_formatobj'] && isset($object['_size'])) {
$object['_attachments'] = array();
$object['_formatobj']->get_attachments($object);
}
foreach ($object['_attachments'] as $attach) {
if ($attach['id'] == $part) {
if ($print) echo $attach['content'];
else if ($fp) fwrite($fp, $attach['content']);
else return $attach['content'];
return true;
}
}
}
}
else {
// return message part from IMAP directly
// TODO: We could improve performance if we cache part's encoding
// without 3rd argument get_message_part() will request BODYSTRUCTURE from IMAP
return $this->imap->get_message_part($msguid, $part, null, $print, $fp, $skip_charset_conv);
}
}
return null;
}
/**
* Fetch the mime message from the storage server and extract
* the Kolab groupware object from it
*
* @param string The IMAP message UID to fetch
* @param string The object type expected (use wildcard '*' to accept all types)
* @param string The folder name where the message is stored
*
* @return mixed Hash array representing the Kolab object, a kolab_format instance or false if not found
*/
public function read_object($msguid, $type = null, $folder = null)
{
if (!$this->valid) {
return false;
}
if (!$type) $type = $this->type;
if (!$folder) $folder = $this->name;
$this->imap->set_folder($folder);
$this->cache->imap_mode(true);
$message = new rcube_message($msguid);
$this->cache->imap_mode(false);
// Message doesn't exist?
if (empty($message->headers)) {
return false;
}
// extract the X-Kolab-Type header from the XML attachment part if missing
if (empty($message->headers->others['x-kolab-type'])) {
foreach ((array)$message->attachments as $part) {
if (strpos($part->mimetype, kolab_format::KTYPE_PREFIX) === 0) {
$message->headers->others['x-kolab-type'] = $part->mimetype;
break;
}
}
}
// fix buggy messages stating the X-Kolab-Type header twice
else if (is_array($message->headers->others['x-kolab-type'])) {
$message->headers->others['x-kolab-type'] = reset($message->headers->others['x-kolab-type']);
}
// no object type header found: abort
if (empty($message->headers->others['x-kolab-type'])) {
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__,
'line' => __LINE__,
'message' => "No X-Kolab-Type information found in message $msguid ($this->name).",
), true);
return false;
}
$object_type = kolab_format::mime2object_type($message->headers->others['x-kolab-type']);
$content_type = kolab_format::KTYPE_PREFIX . $object_type;
// check object type header and abort on mismatch
if ($type != '*' && strpos($object_type, $type) !== 0 && !($object_type == 'distribution-list' && $type == 'contact')) {
return false;
}
$attachments = array();
// get XML part
$xml = null;
foreach ((array)$message->attachments as $part) {
if (!$xml && ($part->mimetype == $content_type || preg_match('!application/([a-z.]+\+)?xml!i', $part->mimetype))) {
$xml = $message->get_part_body($part->mime_id, true);
}
else if ($part->filename || $part->content_id) {
$key = $part->content_id ? trim($part->content_id, '<>') : $part->filename;
$size = null;
// Use Content-Disposition 'size' as for the Kolab Format spec.
if (isset($part->d_parameters['size'])) {
$size = $part->d_parameters['size'];
}
// we can trust part size only if it's not encoded
else if ($part->encoding == 'binary' || $part->encoding == '7bit' || $part->encoding == '8bit') {
$size = $part->size;
}
$attachments[$key] = array(
'id' => $part->mime_id,
'name' => $part->filename,
'mimetype' => $part->mimetype,
'size' => $size,
);
}
}
if (!$xml) {
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__,
'line' => __LINE__,
'message' => "Could not find Kolab data part in message $msguid ($this->name).",
), true);
return false;
}
// check kolab format version
$format_version = $message->headers->others['x-kolab-mime-version'];
if (empty($format_version)) {
list($xmltype, $subtype) = explode('.', $object_type);
$xmlhead = substr($xml, 0, 512);
// detect old Kolab 2.0 format
if (strpos($xmlhead, '<' . $xmltype) !== false && strpos($xmlhead, 'xmlns=') === false)
$format_version = '2.0';
else
$format_version = '3.0'; // assume 3.0
}
// get Kolab format handler for the given type
$format = kolab_format::factory($object_type, $format_version);
if (is_a($format, 'PEAR_Error'))
return false;
// load Kolab object from XML part
$format->load($xml);
if ($format->is_valid()) {
$object = $format->to_array(array('_attachments' => $attachments));
$object['_type'] = $object_type;
$object['_msguid'] = $msguid;
$object['_mailbox'] = $this->name;
$object['_formatobj'] = $format;
$object['_size'] = strlen($xml);
return $object;
}
else {
// try to extract object UID from XML block
if (preg_match('!<uid>(.+)</uid>!Uims', $xml, $m))
$msgadd = " UID = " . trim(strip_tags($m[1]));
rcube::raise_error(array(
'code' => 600,
'type' => 'php',
'file' => __FILE__,
'line' => __LINE__,
'message' => "Could not parse Kolab object data in message $msguid ($this->name)." . $msgadd,
), true);
self::save_user_xml("$msguid.xml", $xml);
}
return false;
}
/**
* Save an object in this folder.
*
* @param array $object The array that holds the data of the object.
* @param string $type The type of the kolab object.
* @param string $uid The UID of the old object if it existed before
*
* @return mixed False on error or IMAP message UID on success
*/
public function save(&$object, $type = null, $uid = null)
{
if (!$this->valid || empty($object)) {
return false;
}
if (!$type)
$type = $this->type;
// copy attachments from old message
$copyfrom = $object['_copyfrom'] ?? ($object['_msguid'] ?? null);
if (!empty($copyfrom) && ($old = $this->cache->get($copyfrom, $type, $object['_mailbox']))) {
foreach ((array)$old['_attachments'] as $key => $att) {
if (!isset($object['_attachments'][$key])) {
$object['_attachments'][$key] = $old['_attachments'][$key];
}
// unset deleted attachment entries
if ($object['_attachments'][$key] == false) {
unset($object['_attachments'][$key]);
}
// load photo.attachment from old Kolab2 format to be directly embedded in xcard block
else if ($type == 'contact' && ($key == 'photo.attachment' || $key == 'kolab-picture.png') && $att['id']) {
if (!isset($object['photo']))
$object['photo'] = $this->get_attachment($copyfrom, $att['id'], $object['_mailbox']);
unset($object['_attachments'][$key]);
}
}
}
// save contact photo to attachment for Kolab2 format
if (kolab_storage::$version == '2.0' && $object['photo']) {
$attkey = 'kolab-picture.png'; // this file name is hard-coded in libkolab/kolabformatV2/contact.cpp
$object['_attachments'][$attkey] = array(
'mimetype'=> rcube_mime::image_content_type($object['photo']),
'content' => preg_match('![^a-z0-9/=+-]!i', $object['photo']) ? $object['photo'] : base64_decode($object['photo']),
);
}
// process attachments
if (!empty($object['_attachments'])) {
$numatt = count($object['_attachments']);
foreach ($object['_attachments'] as $key => $attachment) {
// FIXME: kolab_storage and Roundcube attachment hooks use different fields!
if (empty($attachment['content']) && !empty($attachment['data'])) {
$attachment['content'] = $attachment['data'];
unset($attachment['data'], $object['_attachments'][$key]['data']);
}
// make sure size is set, so object saved in cache contains this info
if (!isset($attachment['size'])) {
if (!empty($attachment['content'])) {
if (is_resource($attachment['content'])) {
// this need to be a seekable resource, otherwise
// fstat() failes and we're unable to determine size
// here nor in rcube_imap_generic before IMAP APPEND
$stat = fstat($attachment['content']);
$attachment['size'] = $stat ? $stat['size'] : 0;
}
else {
$attachment['size'] = strlen($attachment['content']);
}
}
else if (!empty($attachment['path'])) {
$attachment['size'] = filesize($attachment['path']);
}
$object['_attachments'][$key] = $attachment;
}
// generate unique keys (used as content-id) for attachments
if (is_numeric($key) && $key < $numatt) {
// derrive content-id from attachment file name
$ext = preg_match('/(\.[a-z0-9]{1,6})$/i', $attachment['name'], $m) ? $m[1] : null;
$basename = preg_replace('/[^a-z0-9_.-]/i', '', basename($attachment['name'], $ext)); // to 7bit ascii
if (!$basename) $basename = 'noname';
$cid = $basename . '.' . microtime(true) . $key . $ext;
$object['_attachments'][$cid] = $attachment;
unset($object['_attachments'][$key]);
}
}
}
// save recurrence exceptions as individual objects due to lack of support in Kolab v2 format
if (kolab_storage::$version == '2.0' && $object['recurrence']['EXCEPTIONS']) {
$this->save_recurrence_exceptions($object, $type);
}
// check IMAP BINARY extension support for 'file' objects
// allow configuration to workaround bug in Cyrus < 2.4.17
$rcmail = rcube::get_instance();
$binary = $type == 'file' && !$rcmail->config->get('kolab_binary_disable') && $this->imap->get_capability('BINARY');
// generate and save object message
if ($raw_msg = $this->build_message($object, $type, $binary, $body_file)) {
// resolve old msguid before saving
if ($uid && empty($object['_msguid']) && ($msguid = $this->cache->uid2msguid($uid))) {
$object['_msguid'] = $msguid;
$object['_mailbox'] = $this->name;
}
$result = $this->imap->save_message($this->name, $raw_msg, null, false, null, null, $binary);
// update cache with new UID
if ($result) {
$old_uid = $object['_msguid'] ?? null;
$object['_msguid'] = $result;
$object['_mailbox'] = $this->name;
if ($old_uid) {
// delete old message
$this->cache->imap_mode(true);
$this->imap->delete_message($old_uid, $object['_mailbox']);
$this->cache->imap_mode(false);
}
// insert/update message in cache
$this->cache->save($result, $object, $old_uid);
}
// remove temp file
if ($body_file) {
@unlink($body_file);
}
}
return $result;
}
/**
* Save recurrence exceptions as individual objects.
* The Kolab v2 format doesn't allow us to save fully embedded exception objects.
*
* @param array Hash array with event properties
* @param string Object type
*/
private function save_recurrence_exceptions(&$object, $type = null)
{
if ($object['recurrence']['EXCEPTIONS']) {
$exdates = [];
foreach ((array) $object['recurrence']['EXDATE'] as $exdate) {
$key = $exdate instanceof DateTimeInterface ? $exdate->format('Y-m-d') : strval($exdate);
$exdates[$key] = 1;
}
// save every exception as individual object
foreach ((array) $object['recurrence']['EXCEPTIONS'] as $exception) {
$exception['uid'] = self::recurrence_exception_uid($object['uid'], $exception['start']->format('Ymd'));
$exception['sequence'] = $object['sequence'] + 1;
if ($exception['thisandfuture']) {
$exception['recurrence'] = $object['recurrence'];
// adjust the recurrence duration of the exception
if ($object['recurrence']['COUNT']) {
$recurrence = new kolab_date_recurrence($object['_formatobj']);
if ($end = $recurrence->end()) {
unset($exception['recurrence']['COUNT']);
$exception['recurrence']['UNTIL'] = $end;
}
}
// set UNTIL date if we have a thisandfuture exception
$untildate = clone $exception['start'];
$untildate->sub(new DateInterval('P1D'));
$object['recurrence']['UNTIL'] = $untildate;
unset($object['recurrence']['COUNT']);
}
else {
if (!$exdates[$exception['start']->format('Y-m-d')])
$object['recurrence']['EXDATE'][] = clone $exception['start'];
unset($exception['recurrence']);
}
unset($exception['recurrence']['EXCEPTIONS'], $exception['_formatobj'], $exception['_msguid']);
$this->save($exception, $type, $exception['uid']);
}
unset($object['recurrence']['EXCEPTIONS']);
}
}
/**
* Generate an object UID with the given recurrence-ID in a way that it is
* unique (the original UID is not a substring) but still recoverable.
*/
private static function recurrence_exception_uid($uid, $recurrence_id)
{
$offset = -2;
return substr($uid, 0, $offset) . '-' . $recurrence_id . '-' . substr($uid, $offset);
}
/**
* Delete the specified object from this folder.
*
* @param mixed $object The Kolab object to delete or object UID
* @param boolean $expunge Should the folder be expunged?
*
* @return boolean True if successful, false on error
*/
public function delete($object, $expunge = true)
{
if (!$this->valid) {
return false;
}
$msguid = is_array($object) ? $object['_msguid'] : $this->cache->uid2msguid($object);
$success = false;
$this->cache->imap_mode(true);
if ($msguid && $expunge) {
$success = $this->imap->delete_message($msguid, $this->name);
}
else if ($msguid) {
$success = $this->imap->set_flag($msguid, 'DELETED', $this->name);
}
$this->cache->imap_mode(false);
if ($success) {
$this->cache->set($msguid, false);
}
return $success;
}
/**
*
*/
public function delete_all()
{
if (!$this->valid) {
return false;
}
$this->cache->purge();
$this->cache->imap_mode(true);
$result = $this->imap->clear_folder($this->name);
$this->cache->imap_mode(false);
return $result;
}
/**
* Restore a previously deleted object
*
* @param string Object UID
* @return mixed Message UID on success, false on error
*/
public function undelete($uid)
{
if (!$this->valid) {
return false;
}
if ($msguid = $this->cache->uid2msguid($uid, true)) {
$this->cache->imap_mode(true);
$result = $this->imap->set_flag($msguid, 'UNDELETED', $this->name);
$this->cache->imap_mode(false);
if ($result) {
return $msguid;
}
}
return false;
}
/**
* Move a Kolab object message to another IMAP folder
*
* @param string Object UID
* @param string IMAP folder to move object to
* @return boolean True on success, false on failure
*/
public function move($uid, $target_folder)
{
if (!$this->valid) {
return false;
}
if (is_string($target_folder))
$target_folder = kolab_storage::get_folder($target_folder);
if ($msguid = $this->cache->uid2msguid($uid)) {
$this->cache->imap_mode(true);
$result = $this->imap->move_message($msguid, $target_folder->name, $this->name);
$this->cache->imap_mode(false);
if ($result) {
$new_uid = ($copyuid = $this->imap->conn->data['COPYUID']) ? $copyuid[1] : null;
$this->cache->move($msguid, $uid, $target_folder, $new_uid);
return true;
}
else {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed to move message $msguid to $target_folder: " . $this->imap->get_error_str(),
), true);
}
}
return false;
}
/**
* Creates source of the configuration object message
*
* @param array $object The array that holds the data of the object.
* @param string $type The type of the kolab object.
* @param bool $binary Enables use of binary encoding of attachment(s)
* @param string $body_file Reference to filename of message body
*
* @return mixed Message as string or array with two elements
* (one for message file path, second for message headers)
*/
private function build_message(&$object, $type, $binary, &$body_file)
{
// load old object to preserve data we don't understand/process
$format = null;
if (is_object($object['_formatobj'] ?? null))
$format = $object['_formatobj'];
else if (!empty($object['_msguid']) && ($old = $this->cache->get($object['_msguid'], $type, $object['_mailbox'] ?? null)))
$format = $old['_formatobj'] ?? null;
// create new kolab_format instance
if (!$format)
$format = kolab_format::factory($type, kolab_storage::$version);
if (PEAR::isError($format))
return false;
$format->set($object);
$xml = $format->write(kolab_storage::$version);
$object['uid'] = $format->uid; // read UID from format
$object['_formatobj'] = $format;
if (empty($xml) || !$format->is_valid() || empty($object['uid'])) {
return false;
}
$mime = new Mail_mime("\r\n");
$rcmail = rcube::get_instance();
$headers = array();
$files = array();
$part_id = 1;
$encoding = $binary ? 'binary' : 'base64';
if ($user_email = $rcmail->get_user_email()) {
$headers['From'] = $user_email;
$headers['To'] = $user_email;
}
$headers['Date'] = date('r');
$headers['X-Kolab-Type'] = kolab_format::KTYPE_PREFIX . $type;
$headers['X-Kolab-Mime-Version'] = kolab_storage::$version;
$headers['Subject'] = $object['uid'];
// $headers['Message-ID'] = $rcmail->gen_message_id();
$headers['User-Agent'] = $rcmail->config->get('useragent');
// Check if we have enough memory to handle the message in it
// It's faster than using files, so we'll do this if we only can
if (!empty($object['_attachments']) && ($mem_limit = parse_bytes(ini_get('memory_limit'))) > 0) {
$memory = function_exists('memory_get_usage') ? memory_get_usage() : 16*1024*1024; // safe value: 16MB
foreach ($object['_attachments'] as $attachment) {
$memory += $attachment['size'];
}
// 1.33 is for base64, we need at least 4x more memory than the message size
if ($memory * ($binary ? 1 : 1.33) * 4 > $mem_limit) {
$marker = '%%%~~~' . md5(microtime(true) . $memory) . '~~~%%%';
$is_file = true;
$temp_dir = unslashify($rcmail->config->get('temp_dir'));
$mime->setParam('delay_file_io', true);
}
}
$mime->headers($headers);
$mime->setTXTBody("This is a Kolab Groupware object. "
. "To view this object you will need an email client that understands the Kolab Groupware format. "
. "For a list of such email clients please visit http://www.kolab.org/\n\n");
$ctype = kolab_storage::$version == '2.0' ? $format->CTYPEv2 : $format->CTYPE;
// Convert new lines to \r\n, to wrokaround "NO Message contains bare newlines"
// when APPENDing from temp file
$xml = preg_replace('/\r?\n/', "\r\n", $xml);
$mime->addAttachment($xml, // file
$ctype, // content-type
'kolab.xml', // filename
false, // is_file
'8bit', // encoding
'attachment', // disposition
RCUBE_CHARSET // charset
);
$part_id++;
$is_file = false;
// save object attachments as separate parts
foreach ((array)($object['_attachments'] ?? []) as $key => $att) {
if (empty($att['content']) && !empty($att['id'])) {
// @TODO: use IMAP CATENATE to skip attachment fetch+push operation
$msguid = $object['_copyfrom'] ?: ($object['_msguid'] ?: $object['uid']);
if ($is_file) {
$att['path'] = tempnam($temp_dir, 'rcmAttmnt');
if (($fp = fopen($att['path'], 'w')) && $this->get_attachment($msguid, $att['id'], $object['_mailbox'], false, $fp, true)) {
fclose($fp);
}
else {
return false;
}
}
else {
$att['content'] = $this->get_attachment($msguid, $att['id'], $object['_mailbox'], false, null, true);
}
}
$headers = array('Content-ID' => Mail_mimePart::encodeHeader('Content-ID', '<' . $key . '>', RCUBE_CHARSET, 'quoted-printable'));
$name = !empty($att['name']) ? $att['name'] : $key;
// To store binary files we can use faster method
// without writting full message content to a temporary file but
// directly to IMAP, see rcube_imap_generic::append().
// I.e. use file handles where possible
if (!empty($att['path'])) {
if ($is_file && $binary) {
$files[] = fopen($att['path'], 'r');
$mime->addAttachment($marker, $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
}
else {
$mime->addAttachment($att['path'], $att['mimetype'], $name, true, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
}
}
else {
if (is_resource($att['content']) && $is_file && $binary) {
$files[] = $att['content'];
$mime->addAttachment($marker, $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
}
else {
if (is_resource($att['content'])) {
@rewind($att['content']);
$att['content'] = stream_get_contents($att['content']);
}
$mime->addAttachment($att['content'], $att['mimetype'], $name, false, $encoding, 'attachment', '', '', '', null, null, '', RCUBE_CHARSET, $headers);
}
}
$object['_attachments'][$key]['id'] = ++$part_id;
}
if (!$is_file || !empty($files)) {
$message = $mime->getMessage();
}
// parse message and build message array with
// attachment file pointers in place of file markers
if (!empty($files)) {
$message = explode($marker, $message);
$tmp = array();
foreach ($message as $msg_part) {
$tmp[] = $msg_part;
if ($file = array_shift($files)) {
$tmp[] = $file;
}
}
$message = $tmp;
}
// write complete message body into temp file
else if ($is_file) {
// use common temp dir
$body_file = tempnam($temp_dir, 'rcmMsg');
if (PEAR::isError($mime_result = $mime->saveMessageBody($body_file))) {
rcube::raise_error(array('code' => 650, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Could not create message: ".$mime_result->getMessage()),
true, false);
return false;
}
$message = array(trim($mime->txtHeaders()) . "\r\n\r\n", fopen($body_file, 'r'));
}
return $message;
}
/**
* Triggers any required updates after changes within the
* folder. This is currently only required for handling free/busy
* information with Kolab.
*
* @return boolean|PEAR_Error True if successfull.
*/
public function trigger()
{
$owner = $this->get_owner();
$result = false;
switch($this->type) {
case 'event':
if ($this->get_namespace() == 'personal') {
$result = $this->trigger_url(
sprintf('%s/trigger/%s/%s.pfb',
kolab_storage::get_freebusy_server(),
urlencode($owner),
urlencode($this->imap->mod_folder($this->name))
),
$this->imap->options['user'],
$this->imap->options['password']
);
}
break;
default:
return true;
}
if ($result && is_object($result) && is_a($result, 'PEAR_Error')) {
return PEAR::raiseError(
sprintf("Failed triggering folder %s. Error was: %s", $this->name, $result->getMessage())
);
}
return $result;
}
/**
* Triggers a URL.
*
* @param string $url The URL to be triggered.
* @param string $auth_user Username to authenticate with
* @param string $auth_passwd Password for basic auth
* @return boolean|PEAR_Error True if successfull.
*/
private function trigger_url($url, $auth_user = null, $auth_passwd = null)
{
try {
$request = libkolab::http_request($url);
// set authentication credentials
if ($auth_user && $auth_passwd)
$request->setAuth($auth_user, $auth_passwd);
$result = $request->send();
// rcube::write_log('trigger', $result->getBody());
}
catch (Exception $e) {
return PEAR::raiseError($e->getMessage());
}
return true;
}
/**
* Log content to a file in per_user_loggin dir if configured
*/
private static function save_user_xml($filename, $content)
{
$rcmail = rcube::get_instance();
if ($rcmail->config->get('kolab_format_error_log')) {
$log_dir = $rcmail->config->get('log_dir', RCUBE_INSTALL_PATH . 'logs');
$user_name = $rcmail->get_user_name();
$log_dir = $log_dir . '/' . $user_name;
if (!empty($user_name) && is_writable($log_dir)) {
file_put_contents("$log_dir/$filename", $content);
}
}
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Mon, Jun 9, 7:41 AM (1 d, 11 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
196777
Default Alt Text
(108 KB)
Attached To
Mode
R14 roundcubemail-plugins-kolab
Attached
Detach File
Event Timeline
Log In to Comment