Page MenuHomePhorge

No OneTemporary

diff --git a/plugins/kolab_addressbook/config.inc.php.dist b/plugins/kolab_addressbook/config.inc.php.dist
index ba8f60d1..bf4dc1d5 100644
--- a/plugins/kolab_addressbook/config.inc.php.dist
+++ b/plugins/kolab_addressbook/config.inc.php.dist
@@ -1,35 +1,39 @@
<?php
+// Backend type (kolab, carddav)
+$config['kolab_addressbook_driver'] = "kolab";
+
+// CalDAV server location (required when kolab_addressbook_driver = carddav)
+$config['kolab_addressbook_carddav_server'] = "http://localhost";
// This option allows to set addressbooks priority or to disable some
// of them. Disabled addressbooks will be not shown in the UI. Default: 0.
// 0 - "Global address book(s) first". Use all address books, starting with the global (LDAP)
// 1 - "Personal address book(s) first". Use all address books, starting with the personal (Kolab)
// 2 - "Global address book(s) only". Use the global (LDAP) addressbook. Disable the personal.
// 3 - "Personal address book(s) only". Use the personal (Kolab) addressbook(s). Disable the global.
$config['kolab_addressbook_prio'] = 0;
// Base URL to build fully qualified URIs to access address books via CardDAV
// The following replacement variables are supported:
// %h - Current HTTP host
// %u - Current webmail user name
// %n - Folder name
// %i - Folder UUID
-// $config['kolab_addressbook_carddav_url'] = 'http://%h/iRony/addressbooks/%u/%i';
+// For example: 'http://%h/iRony/addressbooks/%u/%i'
+$config['kolab_addressbook_carddav_url'] = null;
// Name of LDAP addressbook (a key in ldap_public configuration array) for which
// the CardDAV URI will be displayed if kolab_addressbook_carddav_url is set.
// Use it when iRony's kolabdav_ldap_directory is enabled.
// Note: kolab_addressbook_carddav_url must use %i and not %n.
//
// WARNING: There's limitations with volume and performance:
// CardDAV does a full sync of the entire contact resource.
// For LDAP this means that all entries matching the base_dn/filter are synced to every client.
// It's thus only recommended for small setups with a couple hundred LDAP entries.
// Other than that, the ldap-directory exposed in iRony is strictly read-only.
// Although correctly stated in the CardDAV properties, some clients (e.g. the Thunderbird SoGO connector)
// ignore these properties and allow modifications which then result in sync errors because the server
// denies such updates.
$config['kolab_addressbook_carddav_ldap'] = '';
-
-?>
diff --git a/plugins/kolab_addressbook/kolab_addressbook.php b/plugins/kolab_addressbook/kolab_addressbook.php
index efbf74c8..a5c6e446 100644
--- a/plugins/kolab_addressbook/kolab_addressbook.php
+++ b/plugins/kolab_addressbook/kolab_addressbook.php
@@ -1,1200 +1,1178 @@
<?php
/**
* Kolab address book
*
* Sample plugin to add a new address book source with data from Kolab storage
* It provides also a possibilities to manage contact folders
* (create/rename/delete/acl) directly in Addressbook UI.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2011-2015, 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_addressbook extends rcube_plugin
{
public $task = '?(?!logout).*';
+ public $driver;
+ public $bonnie_api = false;
+
private $sources;
- private $folders;
private $rc;
private $ui;
-
- public $bonnie_api = false;
+ private $driver_class;
const GLOBAL_FIRST = 0;
const PERSONAL_FIRST = 1;
const GLOBAL_ONLY = 2;
const PERSONAL_ONLY = 3;
/**
* Startup method of a Roundcube plugin
*/
public function init()
{
$this->rc = rcube::get_instance();
// load required plugin
$this->require_plugin('libkolab');
- $driver = $this->rc->config->get('kolab_addressbook_driver') ?: 'kolab';
- require_once(dirname(__FILE__) . '/lib/rcube_' . $driver . '_contacts.php');
+ $this->load_config();
+
+ $this->driver = $this->rc->config->get('kolab_addressbook_driver') ?: 'kolab';
+ $this->driver_class = 'rcube_' . $this->driver . '_contacts';
+ require_once(dirname(__FILE__) . '/lib/' . $this->driver_class . '.php');
// register hooks
$this->add_hook('addressbooks_list', array($this, 'address_sources'));
$this->add_hook('addressbook_get', array($this, 'get_address_book'));
$this->add_hook('config_get', array($this, 'config_get'));
if ($this->rc->task == 'addressbook') {
$this->add_texts('localization');
$this->add_hook('contact_form', array($this, 'contact_form'));
$this->add_hook('contact_photo', array($this, 'contact_photo'));
$this->add_hook('template_object_directorylist', array($this, 'directorylist_html'));
// Plugin actions
$this->register_action('plugin.book', array($this, 'book_actions'));
$this->register_action('plugin.book-save', array($this, 'book_save'));
$this->register_action('plugin.book-search', array($this, 'book_search'));
$this->register_action('plugin.book-subscribe', array($this, 'book_subscribe'));
$this->register_action('plugin.contact-changelog', array($this, 'contact_changelog'));
$this->register_action('plugin.contact-diff', array($this, 'contact_diff'));
$this->register_action('plugin.contact-restore', array($this, 'contact_restore'));
// get configuration for the Bonnie API
$this->bonnie_api = libkolab::get_bonnie_api();
// Load UI elements
if ($this->api->output->type == 'html') {
- $this->load_config();
require_once($this->home . '/lib/kolab_addressbook_ui.php');
$this->ui = new kolab_addressbook_ui($this);
if ($this->bonnie_api) {
$this->add_button(array(
'command' => 'contact-history-dialog',
'class' => 'history contact-history disabled',
'classact' => 'history contact-history active',
'innerclass' => 'icon inner',
'label' => 'kolab_addressbook.showhistory',
'type' => 'link-menuitem'
), 'contactmenu');
}
}
}
else if ($this->rc->task == 'settings') {
$this->add_texts('localization');
$this->add_hook('preferences_list', array($this, 'prefs_list'));
$this->add_hook('preferences_save', array($this, 'prefs_save'));
}
- $this->add_hook('folder_delete', array($this, 'prefs_folder_delete'));
- $this->add_hook('folder_rename', array($this, 'prefs_folder_rename'));
- $this->add_hook('folder_update', array($this, 'prefs_folder_update'));
+ if ($this->driver == 'kolab') {
+ $this->add_hook('folder_delete', array($this, 'prefs_folder_delete'));
+ $this->add_hook('folder_rename', array($this, 'prefs_folder_rename'));
+ $this->add_hook('folder_update', array($this, 'prefs_folder_update'));
+ }
}
/**
* Handler for the addressbooks_list hook.
*
* This will add all instances of available Kolab-based address books
* to the list of address sources of Roundcube.
* This will also hide some addressbooks according to kolab_addressbook_prio setting.
*
* @param array $p Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function address_sources($p)
{
$abook_prio = $this->addressbook_prio();
// Disable all global address books
// Assumes that all non-kolab_addressbook sources are global
if ($abook_prio == self::PERSONAL_ONLY) {
$p['sources'] = array();
}
$sources = array();
foreach ($this->_list_sources() as $abook_id => $abook) {
// register this address source
$sources[$abook_id] = $this->abook_prop($abook_id, $abook);
// flag folders with 'i' right as writeable
if ($this->rc->action == 'add' && strpos($abook->rights, 'i') !== false) {
$sources[$abook_id]['readonly'] = false;
}
}
// Add personal address sources to the list
if ($abook_prio == self::PERSONAL_FIRST) {
// $p['sources'] = array_merge($sources, $p['sources']);
// Don't use array_merge(), because if you have folders name
// that resolve to numeric identifier it will break output array keys
foreach ($p['sources'] as $idx => $value)
$sources[$idx] = $value;
$p['sources'] = $sources;
}
else {
// $p['sources'] = array_merge($p['sources'], $sources);
foreach ($sources as $idx => $value)
$p['sources'][$idx] = $value;
}
return $p;
}
/**
* Helper method to build a hash array of address book properties
*/
protected function abook_prop($id, $abook)
{
if ($abook->virtual) {
return array(
'id' => $id,
'name' => $abook->get_name(),
'listname' => $abook->get_foldername(),
'group' => $abook instanceof kolab_storage_folder_user ? 'user' : $abook->get_namespace(),
'readonly' => true,
'rights' => 'l',
'kolab' => true,
'virtual' => true,
);
}
else {
return array(
'id' => $id,
'name' => $abook->get_name(),
'listname' => $abook->get_foldername(),
'readonly' => $abook->readonly,
'rights' => $abook->rights,
'groups' => $abook->groups,
'undelete' => $abook->undelete && $this->rc->config->get('undo_timeout'),
'realname' => rcube_charset::convert($abook->get_realname(), 'UTF7-IMAP'), // IMAP folder name
'group' => $abook->get_namespace(),
'subscribed' => $abook->is_subscribed(),
'carddavurl' => $abook->get_carddav_url(),
'removable' => true,
'kolab' => true,
'audittrail' => !empty($this->bonnie_api),
);
}
}
/**
*
*/
public function directorylist_html($args)
{
$out = '';
$jsdata = array();
$sources = (array)$this->rc->get_address_sources();
// list all non-kolab sources first (also exclude hidden sources)
$filter = function($source){ return empty($source['kolab']) && empty($source['hidden']); };
foreach (array_filter($sources, $filter) as $j => $source) {
$id = strval(strlen($source['id']) ? $source['id'] : $j);
$out .= $this->addressbook_list_item($id, $source, $jsdata) . '</li>';
}
+ $filter = function($source) { return !empty($source['kolab']) && empty($source['hidden']); };
+ $folders = array_filter($sources, $filter);
+
// render a hierarchical list of kolab contact folders
- kolab_storage::folder_hierarchy($this->folders, $tree);
- if ($tree && !empty($tree->children)) {
- $out .= $this->folder_tree_html($tree, $sources, $jsdata);
+ // TODO: Move this to the drivers
+ if ($this->driver == 'kolab') {
+ kolab_storage::folder_hierarchy($folders, $tree);
+ if ($tree && !empty($tree->children)) {
+ $out .= $this->folder_tree_html($tree, $sources, $jsdata);
+ }
+ }
+ else {
+ foreach ($folders as $j => $source) {
+ $id = strval(strlen($source['id']) ? $source['id'] : $j);
+ $out .= $this->addressbook_list_item($id, $source, $jsdata) . '</li>';
+ }
}
$this->rc->output->set_env('contactgroups', array_filter($jsdata, function($src){ return $src['type'] == 'group'; }));
$this->rc->output->set_env('address_sources', array_filter($jsdata, function($src){ return $src['type'] != 'group'; }));
$args['content'] = html::tag('ul', $args, $out, html::$common_attrib);
return $args;
}
/**
* Return html for a structured list <ul> for the folder tree
*/
public function folder_tree_html($node, $data, &$jsdata)
{
$out = '';
foreach ($node->children as $folder) {
$id = $folder->id;
$source = $data[$id];
$is_collapsed = strpos($this->rc->config->get('collapsed_abooks',''), '&'.rawurlencode($id).'&') !== false;
if ($folder->virtual) {
$source = $this->abook_prop($folder->id, $folder);
}
else if (empty($source)) {
$this->sources[$id] = new rcube_kolab_contacts($folder->name);
$source = $this->abook_prop($id, $this->sources[$id]);
}
$content = $this->addressbook_list_item($id, $source, $jsdata);
if (!empty($folder->children)) {
$child_html = $this->folder_tree_html($folder, $data, $jsdata);
// copy group items...
if (preg_match('!<ul[^>]*>(.*)</ul>\n*$!Ums', $content, $m)) {
$child_html = $m[1] . $child_html;
$content = substr($content, 0, -strlen($m[0]) - 1);
}
// ... and re-create the subtree
if (!empty($child_html)) {
$content .= html::tag('ul', array('class' => 'groups', 'style' => ($is_collapsed ? "display:none;" : null)), $child_html);
}
}
$out .= $content . '</li>';
}
return $out;
}
/**
*
*/
protected function addressbook_list_item($id, $source, &$jsdata, $search_mode = false)
{
$current = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC);
if (!$source['virtual']) {
$jsdata[$id] = $source;
$jsdata[$id]['name'] = html_entity_decode($source['name'], ENT_NOQUOTES, RCUBE_CHARSET);
}
// set class name(s)
$classes = array('addressbook');
if ($source['group'])
$classes[] = $source['group'];
if ($current === $id)
$classes[] = 'selected';
if ($source['readonly'])
$classes[] = 'readonly';
if ($source['virtual'])
$classes[] = 'virtual';
if ($source['class_name'])
$classes[] = $source['class_name'];
$name = !empty($source['listname']) ? $source['listname'] : (!empty($source['name']) ? $source['name'] : $id);
$label_id = 'kabt:' . $id;
$inner = ($source['virtual'] ?
html::a(array('tabindex' => '0'), $name) :
html::a(array(
'href' => $this->rc->url(array('_source' => $id)),
'rel' => $source['id'],
'id' => $label_id,
'onclick' => "return " . rcmail_output::JS_OBJECT_NAME.".command('list','" . rcube::JQ($id) . "',this)",
), $name)
);
- if (isset($source['subscribed'])) {
+ if ($this->driver == 'kolab' && isset($source['subscribed'])) {
$inner .= html::span(array(
'class' => 'subscribed',
'title' => $this->gettext('foldersubscribe'),
'role' => 'checkbox',
'aria-checked' => $source['subscribed'] ? 'true' : 'false',
), '');
}
// don't wrap in <li> but add a checkbox for search results listing
if ($search_mode) {
$jsdata[$id]['group'] = join(' ', $classes);
if (!$source['virtual']) {
$inner .= html::tag('input', array(
'type' => 'checkbox',
'name' => '_source[]',
'value' => $id,
'checked' => false,
'aria-labelledby' => $label_id,
));
}
return html::div(null, $inner);
}
$out .= html::tag('li', array(
'id' => 'rcmli' . rcube_utils::html_identifier($id, true),
'class' => join(' ', $classes),
'noclose' => true,
),
html::div($source['subscribed'] ? 'subscribed' : null, $inner)
);
$groupdata = array('out' => '', 'jsdata' => $jsdata, 'source' => $id);
if ($source['groups'] && function_exists('rcmail_contact_groups')) {
$groupdata = rcmail_contact_groups($groupdata);
}
$jsdata = $groupdata['jsdata'];
$out .= $groupdata['out'];
return $out;
}
/**
* Sets autocomplete_addressbooks option according to
* kolab_addressbook_prio setting extending list of address sources
* to be used for autocompletion.
*/
public function config_get($args)
{
if ($args['name'] != 'autocomplete_addressbooks' || $this->recurrent) {
return $args;
}
$abook_prio = $this->addressbook_prio();
// Get the original setting, use temp flag to prevent from an infinite recursion
$this->recurrent = true;
$sources = $this->rc->config->get('autocomplete_addressbooks');
$this->recurrent = false;
// Disable all global address books
// Assumes that all non-kolab_addressbook sources are global
if ($abook_prio == self::PERSONAL_ONLY) {
$sources = array();
}
if (!is_array($sources)) {
$sources = array();
}
$kolab_sources = array();
foreach (array_keys($this->_list_sources()) as $abook_id) {
if (!in_array($abook_id, $sources))
$kolab_sources[] = $abook_id;
}
// Add personal address sources to the list
if (!empty($kolab_sources)) {
if ($abook_prio == self::PERSONAL_FIRST) {
$sources = array_merge($kolab_sources, $sources);
}
else {
$sources = array_merge($sources, $kolab_sources);
}
}
$args['result'] = $sources;
return $args;
}
-
/**
* Getter for the rcube_addressbook instance
*
* @param array $p Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function get_address_book($p)
{
if ($p['id']) {
- $id = kolab_storage::id_decode($p['id']);
- $folder = kolab_storage::get_folder($id);
-
- // try with unencoded (old-style) identifier
- if ((!$folder || $folder->type != 'contact') && $id != $p['id']) {
- $folder = kolab_storage::get_folder($p['id']);
- }
-
- if ($folder && $folder->type == 'contact') {
- $p['instance'] = new rcube_kolab_contacts($folder->name);
+ if ($source = $this->driver_class::get_address_book($p['id'])) {
+ $p['instance'] = $source;
// flag source as writeable if 'i' right is given
if ($p['writeable'] && $this->rc->action == 'save' && strpos($p['instance']->rights, 'i') !== false) {
$p['instance']->readonly = false;
}
else if ($this->rc->action == 'delete' && strpos($p['instance']->rights, 't') !== false) {
$p['instance']->readonly = false;
}
}
}
return $p;
}
-
+ /**
+ * List addressbook sources list
+ */
private function _list_sources()
{
// already read sources
- if (isset($this->sources))
+ if (isset($this->sources)) {
return $this->sources;
+ }
- kolab_storage::$encode_ids = true;
- $this->sources = array();
- $this->folders = array();
+ $this->sources = [];
$abook_prio = $this->addressbook_prio();
// Personal address source(s) disabled?
- if ($abook_prio == self::GLOBAL_ONLY) {
+ if ($abook_prio == kolab_addressbook::GLOBAL_ONLY) {
return $this->sources;
}
// get all folders that have "contact" type
- $folders = kolab_storage::sort_folders(kolab_storage::get_folders('contact'));
-
- if (PEAR::isError($folders)) {
- rcube::raise_error(array(
- 'code' => 600, 'type' => 'php',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Failed to list contact folders from Kolab server:" . $folders->getMessage()),
- true, false);
- }
- else {
- // we need at least one folder to prevent from errors in Roundcube core
- // when there's also no sql nor ldap addressbook (Bug #2086)
- if (empty($folders)) {
- if ($folder = kolab_storage::create_default_folder('contact')) {
- $folders = array(new kolab_storage_folder($folder, 'contact'));
- }
- }
-
- // convert to UTF8 and sort
- foreach ($folders as $folder) {
- // create instance of rcube_contacts
- $abook_id = $folder->id;
- $abook = new rcube_kolab_contacts($folder->name);
- $this->sources[$abook_id] = $abook;
- $this->folders[$abook_id] = $folder;
- }
+ foreach ($this->driver_class::list_folders() as $id => $source) {
+ $this->sources[$id] = $source;
}
return $this->sources;
}
-
/**
* Plugin hook called before rendering the contact form or detail view
*
* @param array $p Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function contact_form($p)
{
// none of our business
if (!is_object($GLOBALS['CONTACTS']) || !is_a($GLOBALS['CONTACTS'], 'rcube_kolab_contacts'))
return $p;
// extend the list of contact fields to be displayed in the 'personal' section
if (is_array($p['form']['personal'])) {
$p['form']['personal']['content']['profession'] = array('size' => 40);
$p['form']['personal']['content']['children'] = array('size' => 40);
$p['form']['personal']['content']['freebusyurl'] = array('size' => 40);
$p['form']['personal']['content']['pgppublickey'] = array('size' => 70);
$p['form']['personal']['content']['pkcs7publickey'] = array('size' => 70);
// re-order fields according to the coltypes list
$p['form']['contact']['content'] = $this->_sort_form_fields($p['form']['contact']['content'], $GLOBALS['CONTACTS']);
$p['form']['personal']['content'] = $this->_sort_form_fields($p['form']['personal']['content'], $GLOBALS['CONTACTS']);
/* define a separate section 'settings'
$p['form']['settings'] = array(
'name' => $this->gettext('settings'),
'content' => array(
'freebusyurl' => array('size' => 40, 'visible' => true),
'pgppublickey' => array('size' => 70, 'visible' => true),
'pkcs7publickey' => array('size' => 70, 'visible' => false),
)
);
*/
}
if ($this->bonnie_api && $this->rc->action == 'show' && empty($p['record']['rev'])) {
$this->rc->output->set_env('kolab_audit_trail', true);
}
return $p;
}
/**
* Plugin hook for the contact photo image
*/
public function contact_photo($p)
{
// add photo data from old revision inline as data url
if (!empty($p['record']['rev']) && !empty($p['data'])) {
$p['url'] = 'data:image/gif;base64,' . base64_encode($p['data']);
}
return $p;
}
/**
* Handler for contact audit trail changelog requests
*/
public function contact_changelog()
{
if (empty($this->bonnie_api)) {
return false;
}
$contact = rcube_utils::get_input_value('cid', rcube_utils::INPUT_POST, true);
$source = rcube_utils::get_input_value('source', rcube_utils::INPUT_POST);
list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($contact, $source);
$result = $uid && $mailbox ? $this->bonnie_api->changelog('contact', $uid, $mailbox, $msguid) : null;
if (is_array($result) && $result['uid'] == $uid) {
if (is_array($result['changes'])) {
$rcmail = $this->rc;
$dtformat = $this->rc->config->get('date_format') . ' ' . $this->rc->config->get('time_format');
array_walk($result['changes'], function(&$change) use ($rcmail, $dtformat) {
if ($change['date']) {
$dt = rcube_utils::anytodatetime($change['date']);
if ($dt instanceof DateTime) {
$change['date'] = $rcmail->format_date($dt, $dtformat);
}
}
});
}
$this->rc->output->command('contact_render_changelog', $result['changes']);
}
else {
$this->rc->output->command('contact_render_changelog', false);
}
$this->rc->output->send();
}
/**
* Handler for audit trail diff view requests
*/
public function contact_diff()
{
if (empty($this->bonnie_api)) {
return false;
}
$contact = rcube_utils::get_input_value('cid', rcube_utils::INPUT_POST, true);
$source = rcube_utils::get_input_value('source', rcube_utils::INPUT_POST);
$rev1 = rcube_utils::get_input_value('rev1', rcube_utils::INPUT_POST);
$rev2 = rcube_utils::get_input_value('rev2', rcube_utils::INPUT_POST);
list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($contact, $source);
$result = $this->bonnie_api->diff('contact', $uid, $rev1, $rev2, $mailbox, $msguid);
if (is_array($result) && $result['uid'] == $uid) {
$result['rev1'] = $rev1;
$result['rev2'] = $rev2;
$result['cid'] = $contact;
// convert some properties, similar to rcube_kolab_contacts::_to_rcube_contact()
$keymap = array(
'lastmodified-date' => 'changed',
'additional' => 'middlename',
'fn' => 'name',
'tel' => 'phone',
'url' => 'website',
'bday' => 'birthday',
'note' => 'notes',
'role' => 'profession',
'title' => 'jobtitle',
);
$propmap = array('email' => 'address', 'website' => 'url', 'phone' => 'number');
$date_format = $this->rc->config->get('date_format', 'Y-m-d');
// map kolab object properties to keys and values the client expects
array_walk($result['changes'], function(&$change, $i) use ($keymap, $propmap, $date_format) {
if (array_key_exists($change['property'], $keymap)) {
$change['property'] = $keymap[$change['property']];
}
// format date-time values
if ($change['property'] == 'created' || $change['property'] == 'changed') {
if ($old_ = rcube_utils::anytodatetime($change['old'])) {
$change['old_'] = $this->rc->format_date($old_);
}
if ($new_ = rcube_utils::anytodatetime($change['new'])) {
$change['new_'] = $this->rc->format_date($new_);
}
}
// format dates
else if ($change['property'] == 'birthday' || $change['property'] == 'anniversary') {
if ($old_ = rcube_utils::anytodatetime($change['old'])) {
$change['old_'] = $this->rc->format_date($old_, $date_format);
}
if ($new_ = rcube_utils::anytodatetime($change['new'])) {
$change['new_'] = $this->rc->format_date($new_, $date_format);
}
}
// convert email, website, phone values
else if (array_key_exists($change['property'], $propmap)) {
$propname = $propmap[$change['property']];
foreach (array('old','new') as $k) {
$k_ = $k . '_';
if (!empty($change[$k])) {
$change[$k_] = html::quote($change[$k][$propname] ?: '--');
if ($change[$k]['type']) {
$change[$k_] .= '&nbsp;' . html::span('subtype', rcmail_get_type_label($change[$k]['type']));
}
$change['ishtml'] = true;
}
}
}
// serialize address structs
if ($change['property'] == 'address') {
foreach (array('old','new') as $k) {
$k_ = $k . '_';
$change[$k]['zipcode'] = $change[$k]['code'];
$template = $this->rc->config->get('address_template', '{'.join('} {', array_keys($change[$k])).'}');
$composite = array();
foreach ($change[$k] as $p => $val) {
if (strlen($val))
$composite['{'.$p.'}'] = $val;
}
$change[$k_] = preg_replace('/\{\w+\}/', '', strtr($template, $composite));
if ($change[$k]['type']) {
$change[$k_] .= html::div('subtype', rcmail_get_type_label($change[$k]['type']));
}
$change['ishtml'] = true;
}
$change['diff_'] = libkolab::html_diff($change['old_'], $change['new_'], true);
}
// localize gender values
else if ($change['property'] == 'gender') {
if ($change['old']) $change['old_'] = $this->rc->gettext($change['old']);
if ($change['new']) $change['new_'] = $this->rc->gettext($change['new']);
}
// translate 'key' entries in individual properties
else if ($change['property'] == 'key') {
$p = $change['old'] ?: $change['new'];
$t = $p['type'];
$change['property'] = $t . 'publickey';
$change['old'] = $change['old'] ? $change['old']['key'] : '';
$change['new'] = $change['new'] ? $change['new']['key'] : '';
}
// compute a nice diff of notes
else if ($change['property'] == 'notes') {
$change['diff_'] = libkolab::html_diff($change['old'], $change['new'], false);
}
});
$this->rc->output->command('contact_show_diff', $result);
}
else {
$this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error');
}
$this->rc->output->send();
}
/**
* Handler for audit trail revision restore requests
*/
public function contact_restore()
{
if (empty($this->bonnie_api)) {
return false;
}
$success = false;
$contact = rcube_utils::get_input_value('cid', rcube_utils::INPUT_POST, true);
$source = rcube_utils::get_input_value('source', rcube_utils::INPUT_POST);
$rev = rcube_utils::get_input_value('rev', rcube_utils::INPUT_POST);
list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($contact, $source, $folder);
if ($folder && ($raw_msg = $this->bonnie_api->rawdata('contact', $uid, $rev, $mailbox))) {
$imap = $this->rc->get_storage();
// insert $raw_msg as new message
if ($imap->save_message($folder->name, $raw_msg, null, false)) {
$success = true;
// delete old revision from imap and cache
$imap->delete_message($msguid, $folder->name);
$folder->cache->set($msguid, false);
$this->cache = array();
}
}
if ($success) {
$this->rc->output->command('display_message', $this->gettext(array('name' => 'objectrestoresuccess', 'vars' => array('rev' => $rev))), 'confirmation');
$this->rc->output->command('close_contact_history_dialog', $contact);
}
else {
$this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error');
}
$this->rc->output->send();
}
/**
* Get a previous revision of the given contact record from the Bonnie API
*/
public function get_revision($cid, $source, $rev)
{
if (empty($this->bonnie_api)) {
return false;
}
list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($cid, $source);
// call Bonnie API
$result = $this->bonnie_api->get('contact', $uid, $rev, $mailbox, $msguid);
if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) {
$format = kolab_format::factory('contact');
$format->load($result['xml']);
$rec = $format->to_array();
if ($format->is_valid()) {
$rec['rev'] = $result['rev'];
return $rec;
}
}
return false;
}
/**
* Helper method to resolved the given contact identifier into uid and mailbox
*
* @return array (uid,mailbox,msguid) tuple
*/
private function _resolve_contact_identity($id, $abook, &$folder = null)
{
$mailbox = $msguid = null;
$source = $this->get_address_book(array('id' => $abook));
if ($source['instance']) {
$uid = $source['instance']->id2uid($id);
$list = kolab_storage::id_decode($abook);
}
else {
return array(null, $mailbox, $msguid);
}
// get resolve message UID and mailbox identifier
if ($folder = kolab_storage::get_folder($list)) {
$mailbox = $folder->get_mailbox_id();
$msguid = $folder->cache->uid2msguid($uid);
}
return array($uid, $mailbox, $msguid);
}
/**
*
*/
private function _sort_form_fields($contents, $source)
{
- $block = array();
+ $block = [];
- foreach (array_keys($source->coltypes) as $col) {
- if (isset($contents[$col]))
- $block[$col] = $contents[$col];
- }
+ foreach (array_keys($source->coltypes) as $col) {
+ if (isset($contents[$col])) {
+ $block[$col] = $contents[$col];
+ }
+ }
- return $block;
+ return $block;
}
-
/**
* Handler for user preferences form (preferences_list hook)
*
* @param array $args Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function prefs_list($args)
{
if ($args['section'] != 'addressbook') {
return $args;
}
$ldap_public = $this->rc->config->get('ldap_public');
// Hide option if there's no global addressbook
if (empty($ldap_public)) {
return $args;
}
// Check that configuration is not disabled
$dont_override = (array) $this->rc->config->get('dont_override', array());
$prio = $this->addressbook_prio();
if (!in_array('kolab_addressbook_prio', $dont_override)) {
// Load localization
$this->add_texts('localization');
$field_id = '_kolab_addressbook_prio';
$select = new html_select(array('name' => $field_id, 'id' => $field_id));
$select->add($this->gettext('globalfirst'), self::GLOBAL_FIRST);
$select->add($this->gettext('personalfirst'), self::PERSONAL_FIRST);
$select->add($this->gettext('globalonly'), self::GLOBAL_ONLY);
$select->add($this->gettext('personalonly'), self::PERSONAL_ONLY);
$args['blocks']['main']['options']['kolab_addressbook_prio'] = array(
'title' => html::label($field_id, rcube::Q($this->gettext('addressbookprio'))),
'content' => $select->show($prio),
);
}
return $args;
}
/**
* Handler for user preferences save (preferences_save hook)
*
* @param array $args Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function prefs_save($args)
{
if ($args['section'] != 'addressbook') {
return $args;
}
// Check that configuration is not disabled
$dont_override = (array) $this->rc->config->get('dont_override', array());
$key = 'kolab_addressbook_prio';
if (!in_array('kolab_addressbook_prio', $dont_override) || !isset($_POST['_'.$key])) {
$args['prefs'][$key] = (int) rcube_utils::get_input_value('_'.$key, rcube_utils::INPUT_POST);
}
return $args;
}
/**
* Handler for plugin actions
*/
public function book_actions()
{
$action = trim(rcube_utils::get_input_value('_act', rcube_utils::INPUT_GPC));
if ($action == 'create') {
$this->ui->book_edit();
}
else if ($action == 'edit') {
$this->ui->book_edit();
}
else if ($action == 'delete') {
$this->book_delete();
}
}
/**
* Handler for address book create/edit form submit
*/
public function book_save()
{
$prop = array(
'name' => trim(rcube_utils::get_input_value('_name', rcube_utils::INPUT_POST)),
'oldname' => trim(rcube_utils::get_input_value('_oldname', rcube_utils::INPUT_POST, true)), // UTF7-IMAP
'parent' => trim(rcube_utils::get_input_value('_parent', rcube_utils::INPUT_POST, true)), // UTF7-IMAP
'type' => 'contact',
'subscribed' => true,
);
$result = $error = false;
$type = strlen($prop['oldname']) ? 'update' : 'create';
$prop = $this->rc->plugins->exec_hook('addressbook_'.$type, $prop);
if (!$prop['abort']) {
if ($newfolder = kolab_storage::folder_update($prop)) {
$folder = $newfolder;
$result = true;
}
else {
$error = kolab_storage::$last_error;
}
}
else {
$result = $prop['result'];
$folder = $prop['name'];
}
if ($result) {
$kolab_folder = kolab_storage::get_folder($folder);
// get folder/addressbook properties
$abook = new rcube_kolab_contacts($folder);
$props = $this->abook_prop(kolab_storage::folder_id($folder, true), $abook);
$props['parent'] = kolab_storage::folder_id($kolab_folder->get_parent(), true);
$this->rc->output->show_message('kolab_addressbook.book'.$type.'d', 'confirmation');
$this->rc->output->command('book_update', $props, kolab_storage::folder_id($prop['oldname'], true));
}
else {
if (!$error) {
$error = $plugin['message'] ? $plugin['message'] : 'kolab_addressbook.book'.$type.'error';
}
$this->rc->output->show_message($error, 'error');
}
$this->rc->output->send('iframe');
}
/**
*
*/
public function book_search()
{
- $results = array();
- $query = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC);
- $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);
+ $results = [];
+ $query = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC);
+ $source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);
kolab_storage::$encode_ids = true;
$search_more_results = false;
$this->sources = array();
$this->folders = array();
// find unsubscribed IMAP folders that have "event" type
if ($source == 'folders') {
foreach ((array)kolab_storage::search_folders('contact', $query, array('other')) as $folder) {
$this->folders[$folder->id] = $folder;
$this->sources[$folder->id] = new rcube_kolab_contacts($folder->name);
}
}
// search other user's namespace via LDAP
else if ($source == 'users') {
$limit = $this->rc->config->get('autocomplete_max', 15) * 2; // we have slightly more space, so display twice the number
foreach (kolab_storage::search_users($query, 0, array(), $limit * 10) as $user) {
$folders = array();
// search for contact folders shared by this user
foreach (kolab_storage::list_user_folders($user, 'contact', false) as $foldername) {
$folders[] = new kolab_storage_folder($foldername, 'contact');
}
if (count($folders)) {
$userfolder = new kolab_storage_folder_user($user['kolabtargetfolder'], '', $user);
$this->folders[$userfolder->id] = $userfolder;
$this->sources[$userfolder->id] = $userfolder;
foreach ($folders as $folder) {
$this->folders[$folder->id] = $folder;
$this->sources[$folder->id] = new rcube_kolab_contacts($folder->name);;
$count++;
}
}
if ($count >= $limit) {
$search_more_results = true;
break;
}
}
}
$delim = $this->rc->get_storage()->get_hierarchy_delimiter();
// build results list
foreach ($this->sources as $id => $source) {
$folder = $this->folders[$id];
$imap_path = explode($delim, $folder->name);
// find parent
do {
array_pop($imap_path);
$parent_id = kolab_storage::folder_id(join($delim, $imap_path));
}
while (count($imap_path) > 1 && !$this->folders[$parent_id]);
// restore "real" parent ID
if ($parent_id && !$this->folders[$parent_id]) {
$parent_id = kolab_storage::folder_id($folder->get_parent());
}
$prop = $this->abook_prop($id, $source);
$prop['parent'] = $parent_id;
$html = $this->addressbook_list_item($id, $prop, $jsdata, true);
unset($prop['group']);
$prop += (array)$jsdata[$id];
$prop['html'] = $html;
$results[] = $prop;
}
// report more results available
if ($search_more_results) {
$this->rc->output->show_message('autocompletemore', 'notice');
}
$this->rc->output->command('multi_thread_http_response', $results, rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC));
}
/**
*
*/
public function book_subscribe()
{
$success = false;
$id = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC);
if ($id && ($folder = kolab_storage::get_folder(kolab_storage::id_decode($id)))) {
if (isset($_POST['_permanent']))
$success |= $folder->subscribe(intval($_POST['_permanent']));
if (isset($_POST['_active']))
$success |= $folder->activate(intval($_POST['_active']));
// list groups for this address book
if (!empty($_POST['_groups'])) {
$abook = new rcube_kolab_contacts($folder->name);
foreach ((array)$abook->list_groups() as $prop) {
$prop['source'] = $id;
$prop['id'] = $prop['ID'];
unset($prop['ID']);
$this->rc->output->command('insert_contact_group', $prop);
}
}
}
if ($success) {
$this->rc->output->show_message('successfullysaved', 'confirmation');
}
else {
$this->rc->output->show_message($this->gettext('errorsaving'), 'error');
}
$this->rc->output->send();
}
/**
* Handler for address book delete action (AJAX)
*/
private function book_delete()
{
$folder = trim(rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC, true, 'UTF7-IMAP'));
if (kolab_storage::folder_delete($folder)) {
$storage = $this->rc->get_storage();
$delimiter = $storage->get_hierarchy_delimiter();
$this->rc->output->show_message('kolab_addressbook.bookdeleted', 'confirmation');
$this->rc->output->set_env('pagecount', 0);
$this->rc->output->command('set_rowcount', rcmail_get_rowcount_text(new rcube_result_set()));
$this->rc->output->command('set_env', 'delimiter', $delimiter);
$this->rc->output->command('list_contacts_clear');
$this->rc->output->command('book_delete_done', kolab_storage::folder_id($folder, true));
}
else {
$this->rc->output->show_message('kolab_addressbook.bookdeleteerror', 'error');
}
$this->rc->output->send();
}
/**
* Returns value of kolab_addressbook_prio setting
*/
private function addressbook_prio()
{
- // Load configuration
- if (!$this->config_loaded) {
- $this->load_config();
- $this->config_loaded = true;
- }
-
$abook_prio = (int) $this->rc->config->get('kolab_addressbook_prio');
// Make sure any global addressbooks are defined
if ($abook_prio == 0 || $abook_prio == 2) {
$ldap_public = $this->rc->config->get('ldap_public');
if (empty($ldap_public)) {
$abook_prio = 1;
}
}
return $abook_prio;
}
/**
* Hook for (contact) folder deletion
*/
function prefs_folder_delete($args)
{
// ignore...
if ($args['abort'] && !$args['result']) {
return $args;
}
$this->_contact_folder_rename($args['name'], false);
}
/**
* Hook for (contact) folder renaming
*/
function prefs_folder_rename($args)
{
// ignore...
if ($args['abort'] && !$args['result']) {
return $args;
}
$this->_contact_folder_rename($args['oldname'], $args['newname']);
}
/**
* Hook for (contact) folder updates. Forward to folder_rename handler if name was changed
*/
function prefs_folder_update($args)
{
// ignore...
if ($args['abort'] && !$args['result']) {
return $args;
}
if ($args['record']['name'] != $args['record']['oldname']) {
$this->_contact_folder_rename($args['record']['oldname'], $args['record']['name']);
}
}
/**
* Apply folder renaming or deletion to the registered birthday calendar address books
*/
private function _contact_folder_rename($oldname, $newname = false)
{
$update = false;
$delimiter = $this->rc->get_storage()->get_hierarchy_delimiter();
$bday_addressbooks = (array)$this->rc->config->get('calendar_birthday_adressbooks', array());
foreach ($bday_addressbooks as $i => $id) {
$folder_name = kolab_storage::id_decode($id);
if ($oldname === $folder_name || strpos($folder_name, $oldname.$delimiter) === 0) {
if ($newname) { // rename
$new_folder = $newname . substr($folder_name, strlen($oldname));
$bday_addressbooks[$i] = kolab_storage::id_encode($new_folder);
}
else { // delete
unset($bday_addressbooks[$i]);
}
$update = true;
}
}
if ($update) {
$this->rc->user->save_prefs(array('calendar_birthday_adressbooks' => $bday_addressbooks));
}
}
}
diff --git a/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php b/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php
index 710b5562..b6bad178 100644
--- a/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php
+++ b/plugins/kolab_addressbook/lib/kolab_addressbook_ui.php
@@ -1,256 +1,260 @@
<?php
/**
* Kolab address book UI
*
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_addressbook_ui
{
private $plugin;
private $rc;
/**
* Class constructor
*
* @param kolab_addressbook $plugin Plugin object
*/
public function __construct($plugin)
{
$this->rc = rcube::get_instance();
$this->plugin = $plugin;
$this->init_ui();
}
/**
* Adds folders management functionality to Addressbook UI
*/
private function init_ui()
{
if (!empty($this->rc->action) && !preg_match('/^plugin\.book/', $this->rc->action) && $this->rc->action != 'show') {
return;
}
// Include script
$this->plugin->include_script('kolab_addressbook.js');
if (empty($this->rc->action)) {
// Include stylesheet (for directorylist)
$this->plugin->include_stylesheet($this->plugin->local_skin_path().'/kolab_addressbook.css');
+ if ($this->plugin->driver != 'kolab') {
+ return;
+ }
+
// include kolab folderlist widget if available
if (in_array('libkolab', $this->plugin->api->loaded_plugins())) {
$this->plugin->api->include_script('libkolab/libkolab.js');
}
$this->rc->output->add_footer($this->rc->output->parse('kolab_addressbook.search_addon', false, false));
// Add actions on address books
$options = array('book-create', 'book-edit', 'book-delete', 'book-remove');
$idx = 0;
if ($dav_url = $this->rc->config->get('kolab_addressbook_carddav_url')) {
- $options[] = 'book-showurl';
- $this->rc->output->set_env('kolab_addressbook_carddav_url', true);
-
- // set CardDAV URI for specified ldap addressbook
- if ($ldap_abook = $this->rc->config->get('kolab_addressbook_carddav_ldap')) {
- $dav_ldap_url = strtr($dav_url, array(
- '%h' => $_SERVER['HTTP_HOST'],
- '%u' => urlencode($this->rc->get_user_name()),
- '%i' => 'ldap-directory',
- '%n' => '',
- ));
- $this->rc->output->set_env('kolab_addressbook_carddav_ldap', $ldap_abook);
- $this->rc->output->set_env('kolab_addressbook_carddav_ldap_url', $dav_ldap_url);
- }
+ $options[] = 'book-showurl';
+ $this->rc->output->set_env('kolab_addressbook_carddav_url', true);
+
+ // set CardDAV URI for specified ldap addressbook
+ if ($ldap_abook = $this->rc->config->get('kolab_addressbook_carddav_ldap')) {
+ $dav_ldap_url = strtr($dav_url, array(
+ '%h' => $_SERVER['HTTP_HOST'],
+ '%u' => urlencode($this->rc->get_user_name()),
+ '%i' => 'ldap-directory',
+ '%n' => '',
+ ));
+ $this->rc->output->set_env('kolab_addressbook_carddav_ldap', $ldap_abook);
+ $this->rc->output->set_env('kolab_addressbook_carddav_ldap_url', $dav_ldap_url);
+ }
}
foreach ($options as $command) {
$content = html::tag('li', $idx ? null : array('class' => 'separator_above'),
$this->plugin->api->output->button(array(
'label' => 'kolab_addressbook.'.str_replace('-', '', $command),
'domain' => $this->ID,
'class' => str_replace('-', ' ', $command) . ' disabled',
'classact' => str_replace('-', ' ', $command) . ' active',
'command' => $command,
'type' => 'link'
)));
$this->plugin->api->add_content($content, 'groupoptions');
$idx++;
}
// Link to Settings/Folders
$content = html::tag('li', array('class' => 'separator_above'),
$this->plugin->api->output->button(array(
'label' => 'managefolders',
'type' => 'link',
'class' => 'folders disabled',
'classact' => 'folders active',
'command' => 'folders',
'task' => 'settings',
)));
$this->plugin->api->add_content($content, 'groupoptions');
$this->rc->output->add_label(
'kolab_addressbook.bookdeleteconfirm',
'kolab_addressbook.bookdeleting',
'kolab_addressbook.carddavurldescription',
'kolab_addressbook.bookdelete',
'kolab_addressbook.bookshowurl',
'kolab_addressbook.bookedit',
'kolab_addressbook.bookcreate',
'kolab_addressbook.nobooknamewarning',
'kolab_addressbook.booksaving',
'kolab_addressbook.findaddressbooks',
'kolab_addressbook.searchterms',
'kolab_addressbook.foldersearchform',
'kolab_addressbook.listsearchresults',
'kolab_addressbook.nraddressbooksfound',
'kolab_addressbook.noaddressbooksfound',
'kolab_addressbook.foldersubscribe',
'resetsearch'
);
if ($this->plugin->bonnie_api) {
$this->rc->output->set_env('kolab_audit_trail', true);
$this->plugin->api->include_script('libkolab/libkolab.js');
$this->rc->output->add_label(
'kolab_addressbook.showhistory',
'kolab_addressbook.objectchangelog',
'kolab_addressbook.objectdiff',
'kolab_addressbook.objectdiffnotavailable',
'kolab_addressbook.objectchangelognotavailable',
'kolab_addressbook.revisionrestoreconfirm'
);
$this->plugin->add_hook('render_page', array($this, 'render_audittrail_page'));
$this->plugin->register_handler('plugin.object_changelog_table', array('libkolab', 'object_changelog_table'));
}
}
// include stylesheet for audit trail
else if ($this->rc->action == 'show' && $this->plugin->bonnie_api) {
$this->plugin->include_stylesheet($this->plugin->local_skin_path().'/kolab_addressbook.css', true);
$this->rc->output->add_label('kolab_addressbook.showhistory');
}
}
/**
* Handler for address book create/edit action
*/
public function book_edit()
{
$this->rc->output->set_env('pagetitle', $this->plugin->gettext('bookproperties'));
$this->rc->output->add_handler('folderform', array($this, 'book_form'));
$this->rc->output->send('libkolab.folderform');
}
/**
* Handler for 'bookdetails' object returning form content for book create/edit
*
* @param array $attr Object attributes
*
* @return string HTML output
*/
public function book_form($attrib)
{
$action = trim(rcube_utils::get_input_value('_act', rcube_utils::INPUT_GPC));
$folder = trim(rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC, true)); // UTF8
$hidden_fields[] = array('name' => '_source', 'value' => $folder);
$folder = rcube_charset::convert($folder, RCUBE_CHARSET, 'UTF7-IMAP');
$storage = $this->rc->get_storage();
$delim = $storage->get_hierarchy_delimiter();
if ($action == 'edit') {
$path_imap = explode($delim, $folder);
$name = rcube_charset::convert(array_pop($path_imap), 'UTF7-IMAP');
$path_imap = implode($delim, $path_imap);
}
else { // create
$path_imap = $folder;
$name = '';
$folder = '';
}
// Store old name, get folder options
if (strlen($folder)) {
$hidden_fields[] = array('name' => '_oldname', 'value' => $folder);
$options = $storage->folder_info($folder);
}
$form = array();
// General tab
$form['properties'] = array(
'name' => $this->rc->gettext('properties'),
'fields' => array(),
);
if (!empty($options) && ($options['norename'] || $options['protected'])) {
$foldername = rcube::Q(str_replace($delim, ' &raquo; ', kolab_storage::object_name($folder)));
}
else {
$foldername = new html_inputfield(array('name' => '_name', 'id' => '_name', 'size' => 30));
$foldername = $foldername->show($name);
}
$form['properties']['fields']['name'] = array(
'label' => $this->plugin->gettext('bookname'),
'value' => $foldername,
'id' => '_name',
);
if (!empty($options) && ($options['norename'] || $options['protected'])) {
// prevent user from moving folder
$hidden_fields[] = array('name' => '_parent', 'value' => $path_imap);
}
else {
$prop = array('name' => '_parent', 'id' => '_parent');
$select = kolab_storage::folder_selector('contact', $prop, $folder);
$form['properties']['fields']['parent'] = array(
'label' => $this->plugin->gettext('parentbook'),
'value' => $select->show(strlen($folder) ? $path_imap : ''),
'id' => '_parent',
);
}
$form_html = kolab_utils::folder_form($form, $folder, 'calendar', $hidden_fields);
return html::tag('form', $attrib + array('action' => 'plugin.book-save', 'method' => 'post', 'id' => 'bookpropform'), $form_html);
}
/**
*
*/
public function render_audittrail_page($p)
{
// append audit trail UI elements to contact page
if ($p['template'] === 'addressbook' && !$p['kolab-audittrail']) {
$this->rc->output->add_footer($this->rc->output->parse('kolab_addressbook.audittrail', false, false));
$p['kolab-audittrail'] = true;
}
return $p;
}
}
diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
index 1f9ac291..be1a2b02 100644
--- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
+++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
@@ -1,1400 +1,1458 @@
<?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 rcube_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 (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 addressbook sources (folders)
+ */
+ public static function list_folders()
+ {
+ kolab_storage::$encode_ids = true;
+
+ // get all folders that have "contact" type
+ $folders = kolab_storage::sort_folders(kolab_storage::get_folders('contact'));
+
+ if (PEAR::isError($folders)) {
+ rcube::raise_error([
+ 'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Failed to list contact folders from Kolab server:" . $folders->getMessage()
+ ],
+ true, false);
+
+ return [];
+ }
+
+ // we need at least one folder to prevent from errors in Roundcube core
+ // when there's also no sql nor ldap addressbook (Bug #2086)
+ if (empty($folders)) {
+ if ($folder = kolab_storage::create_default_folder('contact')) {
+ $folders = [new kolab_storage_folder($folder, 'contact')];
+ }
+ }
+
+ $sources = [];
+ foreach ($folders as $folder) {
+ $sources[$folder->id] = new rcube_kolab_contacts($folder->name);
+ }
+
+ return $sources;
+ }
+
+ /**
+ * Getter for the rcube_addressbook instance
+ *
+ * @param string $id Addressbook (folder) ID
+ *
+ * @return ?rcube_kolab_contacts
+ */
+ public static function get_address_book($id)
+ {
+ $folderId = kolab_storage::id_decode($id);
+ $folder = kolab_storage::get_folder($folderId);
+
+ // try with unencoded (old-style) identifier
+ if ((!$folder || $folder->type != 'contact') && $folderId != $id) {
+ $folder = kolab_storage::get_folder($id);
+ }
+
+ if ($folder && $folder->type == 'contact') {
+ return new rcube_kolab_contacts($folder->name);
+ }
+ }
+
/**
* 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 (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
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 (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;
}
$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 (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 (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 ($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 (!$contact['uid'] && $contact['ID']) {
$contact['uid'] = $this->id2uid($contact['ID']);
}
else if (!$contact['uid'] && $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'];
// 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'] == 1 && 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_dav_client.php b/plugins/libkolab/lib/kolab_dav_client.php
index d9ae88da..e6962f53 100644
--- a/plugins/libkolab/lib/kolab_dav_client.php
+++ b/plugins/libkolab/lib/kolab_dav_client.php
@@ -1,538 +1,571 @@
<?php
/**
* A *DAV client.
*
* @author Aleksander Machniak <machniak@apheleia-it.ch>
*
* Copyright (C) 2022, Apheleia IT AG <contact@apheleia-it.ch>
*
* 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_dav_client
{
public $url;
protected $user;
protected $password;
protected $rc;
protected $responseHeaders = [];
/**
* Object constructor
*/
public function __construct($url)
{
$this->rc = rcube::get_instance();
$parsedUrl = parse_url($url);
if (!empty($parsedUrl['user']) && !empty($parsedUrl['pass'])) {
$this->user = rawurldecode($parsedUrl['user']);
$this->password = rawurldecode($parsedUrl['pass']);
$url = str_replace(rawurlencode($this->user) . ':' . rawurlencode($this->password) . '@', '', $url);
}
else {
$this->user = $this->rc->user->get_username();
$this->password = $this->rc->decrypt($_SESSION['password']);
}
$this->url = $url;
}
/**
* Execute HTTP request to a DAV server
*/
protected function request($path, $method, $body = '', $headers = [])
{
$rcube = rcube::get_instance();
$debug = (array) $rcube->config->get('dav_debug');
$request_config = [
'store_body' => true,
'follow_redirects' => true,
];
$this->responseHeaders = [];
if ($path && ($rootPath = parse_url($this->url, PHP_URL_PATH)) && strpos($path, $rootPath) === 0) {
$path = substr($path, strlen($rootPath));
}
try {
-
$request = $this->initRequest($this->url . $path, $method, $request_config);
$request->setAuth($this->user, $this->password);
if ($body) {
$request->setBody($body);
$request->setHeader(['Content-Type' => 'application/xml; charset=utf-8']);
}
if (!empty($headers)) {
$request->setHeader($headers);
}
if ($debug) {
rcube::write_log('dav', "C: {$method}: " . (string) $request->getUrl()
. "\n" . $this->debugBody($body, $request->getHeaders()));
}
$response = $request->send();
$body = $response->getBody();
$code = $response->getStatus();
if ($debug) {
rcube::write_log('dav', "S: [{$code}]\n" . $this->debugBody($body, $response->getHeader()));
}
if ($code >= 300) {
throw new Exception("DAV Error ($code):\n{$body}");
}
$this->responseHeaders = $response->getHeader();
return $this->parseXML($body);
}
catch (Exception $e) {
rcube::raise_error($e, true, false);
return false;
}
}
/**
* Discover DAV folders of specified type on the server
*/
public function discover($component = 'VEVENT')
{
$roots = [
'VEVENT' => 'calendars',
'VTODO' => 'calendars',
'VCARD' => 'addressbooks',
];
$path = parse_url($this->url, PHP_URL_PATH);
$body = '<?xml version="1.0" encoding="utf-8"?>'
. '<d:propfind xmlns:d="DAV:">'
. '<d:prop>'
. '<d:current-user-principal />'
. '</d:prop>'
. '</d:propfind>';
- $response = $this->request('/' . $roots[$component], 'PROPFIND', $body);
+ // Note: Cyrus CardDAV service requires Depth:1 (CalDAV works without it)
+ $response = $this->request('/' . $roots[$component], 'PROPFIND', $body, ['Depth' => 1, 'Prefer' => 'return-minimal']);
$elements = $response->getElementsByTagName('response');
foreach ($elements as $element) {
foreach ($element->getElementsByTagName('prop') as $prop) {
$principal_href = $prop->nodeValue;
break;
}
}
if ($path && strpos($principal_href, $path) === 0) {
$principal_href = substr($principal_href, strlen($path));
}
+ $homes = [
+ 'VEVENT' => 'calendar-home-set',
+ 'VTODO' => 'calendar-home-set',
+ 'VCARD' => 'addressbook-home-set',
+ ];
+
+ $ns = [
+ 'VEVENT' => 'caldav',
+ 'VTODO' => 'caldav',
+ 'VCARD' => 'carddav',
+ ];
+
$body = '<?xml version="1.0" encoding="utf-8"?>'
- . '<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">'
+ . '<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:' . $ns[$component] . '">'
. '<d:prop>'
- . '<c:calendar-home-set />'
+ . '<c:' . $homes[$component] . ' />'
. '</d:prop>'
. '</d:propfind>';
$response = $this->request($principal_href, 'PROPFIND', $body);
$elements = $response->getElementsByTagName('response');
foreach ($elements as $element) {
foreach ($element->getElementsByTagName('prop') as $prop) {
$root_href = $prop->nodeValue;
break;
}
}
if (!empty($root_href)) {
if ($path && strpos($root_href, $path) === 0) {
$root_href = substr($root_href, strlen($path));
}
}
else {
// Kolab iRony's calendar root
$root_href = '/' . $roots[$component] . '/' . rawurlencode($this->user);
}
+ if ($component == 'VCARD') {
+ $add_ns = '';
+ $add_props = '';
+ }
+ else {
+ $add_ns = ' xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:a="http://apple.com/ns/ical/"';
+ $add_props = '<c:supported-calendar-component-set /><a:calendar-color />';
+ }
+
$body = '<?xml version="1.0" encoding="utf-8"?>'
- . '<d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:a="http://apple.com/ns/ical/">'
+ . '<d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/"' . $add_ns . '>'
. '<d:prop>'
. '<d:resourcetype />'
. '<d:displayname />'
+ // . '<d:sync-token />'
. '<cs:getctag />'
- . '<c:supported-calendar-component-set />'
- . '<a:calendar-color />'
+ . $add_props
. '</d:prop>'
. '</d:propfind>';
- $response = $this->request($root_href, 'PROPFIND', $body);
+ // Note: Cyrus CardDAV service requires Depth:1 (CalDAV works without it)
+ $response = $this->request($root_href, 'PROPFIND', $body, ['Depth' => 1, 'Prefer' => 'return-minimal']);
if (empty($response)) {
return false;
}
$folders = [];
-
foreach ($response->getElementsByTagName('response') as $element) {
$folder = $this->getFolderPropertiesFromResponse($element);
- if ($folder['type'] === $component) {
+
+ // Note: Addressbooks don't have 'type' specified
+ if (($component == 'VCARD' && in_array('addressbook', $folder['resource_type']))
+ || $folder['type'] === $component
+ ) {
$folders[] = $folder;
}
}
return $folders;
}
/**
* Create a DAV object in a folder
*/
- public function create($location, $content)
+ public function create($location, $content, $component = 'VEVENT')
{
- $response = $this->request($location, 'PUT', $content, ['Content-Type' => 'text/calendar; charset=utf-8']);
+ $ctype = [
+ 'VEVENT' => 'text/calendar',
+ 'VTODO' => 'text/calendar',
+ 'VCARD' => 'text/vcard',
+ ];
+
+ $headers = ['Content-Type' => $ctype[$component] . '; charset=utf-8'];
+
+ $response = $this->request($location, 'PUT', $content, $headers);
if ($response !== false) {
$etag = $this->responseHeaders['etag'];
if (preg_match('|^".*"$|', $etag)) {
$etag = substr($etag, 1, -1);
}
return $etag;
}
return false;
}
/**
* Update a DAV object in a folder
*/
- public function update($location, $content)
+ public function update($location, $content, $component = 'VEVENT')
{
- return $this->create($location, $content);
+ return $this->create($location, $content, $component);
}
/**
* Delete a DAV object from a folder
*/
public function delete($location)
{
$response = $this->request($location, 'DELETE', '', ['Depth' => 1, 'Prefer' => 'return-minimal']);
return $response !== false;
}
/**
* Fetch DAV objects metadata (ETag, href) a folder
*/
public function getIndex($location, $component = 'VEVENT')
{
$queries = [
'VEVENT' => 'calendar-query',
'VTODO' => 'calendar-query',
'VCARD' => 'addressbook-query',
];
$ns = [
'VEVENT' => 'caldav',
'VTODO' => 'caldav',
'VCARD' => 'carddav',
];
$filter = '';
if ($component != 'VCARD') {
$filter = '<c:comp-filter name="VCALENDAR">'
. '<c:comp-filter name="' . $component . '" />'
. '</c:comp-filter>';
}
$body = '<?xml version="1.0" encoding="utf-8"?>'
.' <c:' . $queries[$component] . ' xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:' . $ns[$component]. '">'
. '<d:prop>'
. '<d:getetag />'
. '</d:prop>'
. ($filter ? "<c:filter>$filter</c:filter>" : '')
. '</c:' . $queries[$component] . '>';
$response = $this->request($location, 'REPORT', $body, ['Depth' => 1, 'Prefer' => 'return-minimal']);
if (empty($response)) {
return false;
}
$objects = [];
foreach ($response->getElementsByTagName('response') as $element) {
$objects[] = $this->getObjectPropertiesFromResponse($element);
}
return $objects;
}
/**
* Fetch DAV objects data from a folder
*/
public function getData($location, $component = 'VEVENT', $hrefs = [])
{
if (empty($hrefs)) {
return [];
}
$body = '';
foreach ($hrefs as $href) {
$body .= '<d:href>' . $href . '</d:href>';
}
$queries = [
'VEVENT' => 'calendar-multiget',
'VTODO' => 'calendar-multiget',
'VCARD' => 'addressbook-multiget',
];
$ns = [
'VEVENT' => 'caldav',
'VTODO' => 'caldav',
'VCARD' => 'carddav',
];
$types = [
'VEVENT' => 'calendar-data',
'VTODO' => 'calendar-data',
'VCARD' => 'address-data',
];
$body = '<?xml version="1.0" encoding="utf-8"?>'
.' <c:' . $queries[$component] . ' xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:' . $ns[$component] . '">'
. '<d:prop>'
. '<d:getetag />'
. '<c:' . $types[$component]. ' />'
. '</d:prop>'
. $body
. '</c:' . $queries[$component] . '>';
$response = $this->request($location, 'REPORT', $body, ['Depth' => 1, 'Prefer' => 'return-minimal']);
if (empty($response)) {
return false;
}
$objects = [];
foreach ($response->getElementsByTagName('response') as $element) {
$objects[] = $this->getObjectPropertiesFromResponse($element);
}
return $objects;
}
/**
* Parse XML content
*/
protected function parseXML($xml)
{
$doc = new DOMDocument('1.0', 'UTF-8');
if (stripos($xml, '<?xml') === 0) {
if (!$doc->loadXML($xml)) {
throw new Exception("Failed to parse XML");
}
$doc->formatOutput = true;
}
return $doc;
}
/**
* Parse request/response body for debug purposes
*/
protected function debugBody($body, $headers)
{
$head = '';
foreach ($headers as $header_name => $header_value) {
$head .= "{$header_name}: {$header_value}\n";
}
if (stripos($body, '<?xml') === 0) {
$doc = new DOMDocument('1.0', 'UTF-8');
$doc->formatOutput = true;
$doc->preserveWhiteSpace = false;
if (!$doc->loadXML($body)) {
throw new Exception("Failed to parse XML");
}
$body = $doc->saveXML();
}
return $head . "\n" . rtrim($body);
}
/**
* Extract folder properties from a server 'response' element
*/
protected function getFolderPropertiesFromResponse(DOMNode $element)
{
if ($href = $element->getElementsByTagName('href')->item(0)) {
$href = $href->nodeValue;
/*
$path = parse_url($this->url, PHP_URL_PATH);
if ($path && strpos($href, $path) === 0) {
$href = substr($href, strlen($path));
}
*/
}
if ($color = $element->getElementsByTagName('calendar-color')->item(0)) {
if (preg_match('/^#[0-9A-F]{8}$/', $color->nodeValue)) {
$color = substr($color->nodeValue, 1, -2);
} else {
$color = null;
}
}
if ($name = $element->getElementsByTagName('displayname')->item(0)) {
$name = $name->nodeValue;
}
if ($ctag = $element->getElementsByTagName('getctag')->item(0)) {
$ctag = $ctag->nodeValue;
}
$component = null;
if ($set_element = $element->getElementsByTagName('supported-calendar-component-set')->item(0)) {
if ($comp_element = $set_element->getElementsByTagName('comp')->item(0)) {
$component = $comp_element->attributes->getNamedItem('name')->nodeValue;
}
}
$types = [];
if ($type_element = $element->getElementsByTagName('resourcetype')->item(0)) {
foreach ($type_element->childNodes as $node) {
$_type = explode(':', $node->nodeName);
$types[] = count($_type) > 1 ? $_type[1] : $_type[0];
}
}
return [
'href' => $href,
'name' => $name,
'ctag' => $ctag,
'color' => $color,
'type' => $component,
'resource_type' => $types,
];
}
/**
* Extract object properties from a server 'response' element
*/
protected function getObjectPropertiesFromResponse(DOMNode $element)
{
$uid = null;
if ($href = $element->getElementsByTagName('href')->item(0)) {
$href = $href->nodeValue;
/*
$path = parse_url($this->url, PHP_URL_PATH);
if ($path && strpos($href, $path) === 0) {
$href = substr($href, strlen($path));
}
*/
// Extract UID from the URL
$href_parts = explode('/', $href);
$uid = preg_replace('/\.[a-z]+$/', '', $href_parts[count($href_parts)-1]);
}
if ($data = $element->getElementsByTagName('calendar-data')->item(0)) {
$data = $data->nodeValue;
}
else if ($data = $element->getElementsByTagName('address-data')->item(0)) {
$data = $data->nodeValue;
}
if ($etag = $element->getElementsByTagName('getetag')->item(0)) {
$etag = $etag->nodeValue;
if (preg_match('|^".*"$|', $etag)) {
$etag = substr($etag, 1, -1);
}
}
return [
'href' => $href,
'data' => $data,
'etag' => $etag,
'uid' => $uid,
];
}
/**
* Initialize HTTP request object
*/
protected function initRequest($url = '', $method = 'GET', $config = array())
{
$rcube = rcube::get_instance();
$http_config = (array) $rcube->config->get('kolab_http_request');
// deprecated configuration options
if (empty($http_config)) {
foreach (array('ssl_verify_peer', 'ssl_verify_host') as $option) {
$value = $rcube->config->get('kolab_' . $option, true);
if (is_bool($value)) {
$http_config[$option] = $value;
}
}
}
if (!empty($config)) {
$http_config = array_merge($http_config, $config);
}
// load HTTP_Request2 (support both composer-installed and system-installed package)
if (!class_exists('HTTP_Request2')) {
require_once 'HTTP/Request2.php';
}
try {
$request = new HTTP_Request2();
$request->setConfig($http_config);
// proxy User-Agent string
$request->setHeader('user-agent', $_SERVER['HTTP_USER_AGENT']);
// cleanup
$request->setBody('');
$request->setUrl($url);
$request->setMethod($method);
return $request;
}
catch (Exception $e) {
rcube::raise_error($e, true, true);
}
}
}
diff --git a/plugins/libkolab/lib/kolab_storage_dav.php b/plugins/libkolab/lib/kolab_storage_dav.php
index ce064178..15295cfb 100644
--- a/plugins/libkolab/lib/kolab_storage_dav.php
+++ b/plugins/libkolab/lib/kolab_storage_dav.php
@@ -1,488 +1,492 @@
<?php
/**
* Kolab storage class providing access to groupware objects on a *DAV server.
*
* @author Aleksander Machniak <machniak@apheleia-it.ch>
*
* Copyright (C) 2022, Apheleia IT AG <contact@apheleia-it.ch>
*
* 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_dav
{
const ERROR_DAV_CONN = 1;
const ERROR_CACHE_DB = 2;
const ERROR_NO_PERMISSION = 3;
const ERROR_INVALID_FOLDER = 4;
protected $dav;
protected $url;
/**
* Object constructor
*/
public function __construct($url)
{
$this->url = $url;
$this->setup();
}
/**
* Setup the environment
*/
public function setup()
{
$rcmail = rcube::get_instance();
$this->config = $rcmail->config;
$this->dav = new kolab_dav_client($this->url);
}
/**
* Get a list of storage folders for the given data type
*
* @param string Data type to list folders for (contact,distribution-list,event,task,note)
*
* @return array List of kolab_storage_dav_folder objects
*/
public function get_folders($type)
{
$davTypes = [
'event' => 'VEVENT',
'task' => 'VTODO',
'contact' => 'VCARD',
];
// TODO: This should be cached
$folders = $this->dav->discover($davTypes[$type]);
if (is_array($folders)) {
foreach ($folders as $idx => $folder) {
// Exclude some special folders
if (in_array('schedule-inbox', $folder['resource_type']) || in_array('schedule-outbox', $folder['resource_type'])) {
unset($folders[$idx]);
continue;
}
$folders[$idx] = new kolab_storage_dav_folder($this->dav, $folder, $type);
}
}
return $folders ?: [];
}
/**
* Getter for the storage folder for the given type
*
* @param string Data type to list folders for (contact,distribution-list,event,task,note)
*
* @return object kolab_storage_dav_folder The folder object
*/
public function get_default_folder($type)
{
// TODO: Not used
}
/**
* Getter for a specific storage folder
*
- * @param string Folder to access
- * @param string Expected folder type
+ * @param string $id Folder to access
+ * @param string $type Expected folder type
*
- * @return object kolab_storage_folder The folder object
+ * @return ?object kolab_storage_folder The folder object
*/
- public function get_folder($folder, $type = null)
+ public function get_folder($id, $type = null)
{
- // TODO
+ foreach ($this->get_folders($type) as $folder) {
+ if ($folder->id == $id) {
+ return $folder;
+ }
+ }
}
/**
* Getter for a single Kolab object, identified by its UID.
* This will search all folders storing objects of the given type.
*
* @param string Object UID
* @param string Object type (contact,event,task,journal,file,note,configuration)
*
* @return array The Kolab object represented as hash array or false if not found
*/
public function get_object($uid, $type)
{
// TODO
return false;
}
/**
* Execute cross-folder searches with the given query.
*
* @param array Pseudo-SQL query as list of filter parameter triplets
* @param string Folder type (contact,event,task,journal,file,note,configuration)
* @param int Expected number of records or limit (for performance reasons)
*
* @return array List of Kolab data objects (each represented as hash array)
*/
public function select($query, $type, $limit = null)
{
$result = [];
foreach ($this->get_folders($type) as $folder) {
if ($limit) {
$folder->set_order_and_limit(null, $limit);
}
foreach ($folder->select($query) as $object) {
$result[] = $object;
}
}
return $result;
}
/**
* Compose an URL to query the free/busy status for the given user
*
* @param string Email address of the user to get free/busy data for
* @param object DateTime Start of the query range (optional)
* @param object DateTime End of the query range (optional)
*
* @return string Fully qualified URL to query free/busy data
*/
public static function get_freebusy_url($email, $start = null, $end = null)
{
return kolab_storage::get_freebusy_url($email, $start, $end);
}
/**
* Deletes a folder
*
* @param string $name Folder name
*
* @return bool True on success, false on failure
*/
public function folder_delete($name)
{
// TODO
}
/**
* Creates a folder
*
* @param string $name Folder name (UTF7-IMAP)
* @param string $type Folder type
* @param bool $subscribed Sets folder subscription
* @param bool $active Sets folder state (client-side subscription)
*
* @return bool True on success, false on failure
*/
public function folder_create($name, $type = null, $subscribed = false, $active = false)
{
// TODO
}
/**
* Renames DAV folder
*
* @param string $oldname Old folder name (UTF7-IMAP)
* @param string $newname New folder name (UTF7-IMAP)
*
* @return bool True on success, false on failure
*/
public function folder_rename($oldname, $newname)
{
// TODO
}
/**
* Rename or Create a new folder.
*
* Does additional checks for permissions and folder name restrictions
*
* @param array &$prop Hash array with folder properties and metadata
* - name: Folder name
* - oldname: Old folder name when changed
* - parent: Parent folder to create the new one in
* - type: Folder type to create
* - subscribed: Subscribed flag (IMAP subscription)
* - active: Activation flag (client-side subscription)
*
* @return string|false New folder name or False on failure
*/
public function folder_update(&$prop)
{
// TODO
}
/**
* Getter for human-readable name of a folder
*
* @param string $folder Folder name (UTF7-IMAP)
* @param string $folder_ns Will be set to namespace name of the folder
*
* @return string Name of the folder-object
*/
public static function object_name($folder, &$folder_ns = null)
{
// TODO: Shared folders
$folder_ns = 'personal';
return $folder;
}
/**
* Creates a SELECT field with folders list
*
* @param string $type Folder type
* @param array $attrs SELECT field attributes (e.g. name)
* @param string $current The name of current folder (to skip it)
*
* @return html_select SELECT object
*/
public function folder_selector($type, $attrs, $current = '')
{
// TODO
}
/**
* Returns a list of folder names
*
* @param string Optional root folder
* @param string Optional name pattern
* @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
* @param bool Enable to return subscribed folders only (null to use configured subscription mode)
* @param array Will be filled with folder-types data
*
* @return array List of folders
*/
public function list_folders($root = '', $mbox = '*', $filter = null, $subscribed = null, &$folderdata = array())
{
// TODO
}
/**
* Search for shared or otherwise not listed groupware folders the user has access
*
* @param string Folder type of folders to search for
* @param string Search string
* @param array Namespace(s) to exclude results from
*
* @return array List of matching kolab_storage_folder objects
*/
public function search_folders($type, $query, $exclude_ns = [])
{
// TODO
return [];
}
/**
* Sort the given list of folders by namespace/name
*
* @param array List of kolab_storage_dav_folder objects
*
* @return array Sorted list of folders
*/
public static function sort_folders($folders)
{
// TODO
return $folders;
}
/**
* Returns folder types indexed by folder name
*
* @param string $prefix Folder prefix (Default '*' for all folders)
*
* @return array|bool List of folders, False on failure
*/
public function folders_typedata($prefix = '*')
{
// TODO: Used by kolab_folders, kolab_activesync, kolab_delegation
return [];
}
/**
* Returns type of a DAV folder
*
* @param string $folder Folder name (UTF7-IMAP)
*
* @return string Folder type
*/
public function folder_type($folder)
{
// TODO: Used by kolab_folders, kolab_activesync, kolab_delegation
return 'event';
}
/**
* Sets folder content-type.
*
* @param string $folder Folder name
* @param string $type Content type
*
* @return bool True on success, False otherwise
*/
public function set_folder_type($folder, $type = 'mail')
{
// NOP: Used by kolab_folders, kolab_activesync, kolab_delegation
return false;
}
/**
* Check subscription status of this folder
*
* @param string $folder Folder name
* @param bool $temp Include temporary/session subscriptions
*
* @return bool True if subscribed, false if not
*/
public function folder_is_subscribed($folder, $temp = false)
{
// NOP
return true;
}
/**
* Change subscription status of this folder
*
* @param string $folder Folder name
* @param bool $temp Only subscribe temporarily for the current session
*
* @return True on success, false on error
*/
public function folder_subscribe($folder, $temp = false)
{
// NOP
return true;
}
/**
* Change subscription status of this folder
*
* @param string $folder Folder name
* @param bool $temp Only remove temporary subscription
*
* @return True on success, false on error
*/
public function folder_unsubscribe($folder, $temp = false)
{
// NOP
return false;
}
/**
* Check activation status of this folder
*
* @param string $folder Folder name
*
* @return bool True if active, false if not
*/
public function folder_is_active($folder)
{
// TODO
return true;
}
/**
* Change activation status of this folder
*
* @param string $folder Folder name
*
* @return True on success, false on error
*/
public function folder_activate($folder)
{
return true;
}
/**
* Change activation status of this folder
*
* @param string $folder Folder name
*
* @return True on success, false on error
*/
public function folder_deactivate($folder)
{
return false;
}
/**
* Creates default folder of specified type
* To be run when none of subscribed folders (of specified type) is found
*
* @param string $type Folder type
* @param string $props Folder properties (color, etc)
*
* @return string Folder name
*/
public function create_default_folder($type, $props = [])
{
// TODO: For kolab_addressbook??
return '';
}
/**
* Returns a list of IMAP folders shared by the given user
*
* @param array User entry from LDAP
* @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
* @param int 1 - subscribed folders only, 0 - all folders, 2 - all non-active
* @param array Will be filled with folder-types data
*
* @return array List of folders
*/
public function list_user_folders($user, $type, $subscribed = 0, &$folderdata = [])
{
// TODO
return [];
}
/**
* Get a list of (virtual) top-level folders from the other users namespace
*
* @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
* @param bool Enable to return subscribed folders only (null to use configured subscription mode)
*
* @return array List of kolab_storage_folder_user objects
*/
public function get_user_folders($type, $subscribed)
{
// TODO
return [];
}
/**
* Handler for user_delete plugin hooks
*
* Remove all cache data from the local database related to the given user.
*/
public static function delete_user_folders($args)
{
$db = rcmail::get_instance()->get_dbh();
$table = $db->table_name('kolab_folders', true);
$prefix = 'dav://' . urlencode($args['username']) . '@' . $args['host'] . '/%';
$db->query("DELETE FROM $table WHERE `resource` LIKE ?", $prefix);
}
/**
* Get folder METADATA for all supported keys
* Do this in one go for better caching performance
*/
public function folder_metadata($folder)
{
// TODO ?
return [];
}
}
diff --git a/plugins/libkolab/lib/kolab_storage_dav_cache.php b/plugins/libkolab/lib/kolab_storage_dav_cache.php
index 837783b5..b089b170 100644
--- a/plugins/libkolab/lib/kolab_storage_dav_cache.php
+++ b/plugins/libkolab/lib/kolab_storage_dav_cache.php
@@ -1,625 +1,632 @@
<?php
/**
* Kolab storage cache class providing a local caching layer for Kolab groupware objects.
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@apheleia-it.ch>
*
* Copyright (C) 2012-2013, Kolab Systems AG <contact@kolabsys.com>
* Copyright (C) 2022, Apheleia IT AG <contact@apheleia-it.ch>
*
* 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_dav_cache extends kolab_storage_cache
{
/**
* Factory constructor
*/
public static function factory(kolab_storage_folder $storage_folder)
{
$subclass = 'kolab_storage_dav_cache_' . $storage_folder->type;
if (class_exists($subclass)) {
return new $subclass($storage_folder);
}
rcube::raise_error(
['code' => 900, 'message' => "No {$subclass} class found for folder '{$storage_folder->name}'"],
true
);
return new kolab_storage_dav_cache($storage_folder);
}
/**
* Connect cache with a storage folder
*
* @param kolab_storage_folder The storage folder instance to connect with
*/
public function set_folder(kolab_storage_folder $storage_folder)
{
$this->folder = $storage_folder;
if (!$this->folder->valid) {
$this->ready = false;
return;
}
// compose fully qualified ressource uri for this instance
$this->resource_uri = $this->folder->get_resource_uri();
$this->cache_table = $this->db->table_name('kolab_cache_dav_' . $this->folder->type);
$this->ready = true;
}
/**
* Synchronize local cache data with remote
*/
public function synchronize()
{
// only sync once per request cycle
if ($this->synched) {
return;
}
$this->sync_start = time();
// read cached folder metadata
$this->_read_folder_data();
$ctag = $this->folder->get_ctag();
// check cache status ($this->metadata is set in _read_folder_data())
if (
empty($this->metadata['ctag'])
|| empty($this->metadata['changed'])
|| $this->metadata['ctag'] !== $ctag
) {
// lock synchronization for this folder and wait if already locked
$this->_sync_lock();
$result = $this->synchronize_worker();
// update ctag value (will be written to database in _sync_unlock())
if ($result) {
$this->metadata['ctag'] = $ctag;
$this->metadata['changed'] = date(self::DB_DATE_FORMAT, time());
}
// remove lock
$this->_sync_unlock();
}
$this->synched = time();
}
/**
* Perform cache synchronization
*/
protected function synchronize_worker()
{
// get effective time limit we have for synchronization (~70% of the execution time)
$time_limit = $this->_max_sync_lock_time() * 0.7;
if (time() - $this->sync_start > $time_limit) {
return false;
}
// TODO: Implement synchronization with use of WebDAV-Sync (RFC 6578)
// Get the objects from the DAV server
$dav_index = $this->folder->dav->getIndex($this->folder->href, $this->folder->get_dav_type());
if (!is_array($dav_index)) {
rcube::raise_error([
'code' => 900,
'message' => "Failed to sync the kolab cache for {$this->folder->href}"
], true);
return false;
}
// WARNING: For now we assume object's href is <calendar-href>/<uid>.ics,
// which would mean there are no duplicates (objects with the same uid).
// With DAV protocol we can't get UID without fetching the whole object.
// Also the folder_id + uid is a unique index in the database.
// In the future we maybe should store the href in database.
// Determine objects to fetch or delete
$new_index = [];
$update_index = [];
$old_index = $this->folder_index(); // uid -> etag
$chunk_size = 20; // max numer of objects per DAV request
foreach ($dav_index as $object) {
$uid = $object['uid'];
if (isset($old_index[$uid])) {
$old_etag = $old_index[$uid];
$old_index[$uid] = null;
if ($old_etag === $object['etag']) {
// the object didn't change
continue;
}
$update_index[$uid] = $object['href'];
}
else {
$new_index[$uid] = $object['href'];
}
}
// Fetch new objects and store in DB
if (!empty($new_index)) {
foreach (array_chunk($new_index, $chunk_size, true) as $chunk) {
$objects = $this->folder->dav->getData($this->folder->href, $this->folder->get_dav_type(), $chunk);
if (!is_array($objects)) {
rcube::raise_error([
'code' => 900,
'message' => "Failed to sync the kolab cache for {$this->folder->href}"
], true);
return false;
}
foreach ($objects as $object) {
if ($object = $this->folder->from_dav($object)) {
$this->_extended_insert(false, $object);
}
}
$this->_extended_insert(true, null);
// check time limit and abort sync if running too long
if (++$i % 25 == 0 && time() - $this->sync_start > $time_limit) {
return false;
}
}
}
// Fetch updated objects and store in DB
if (!empty($update_index)) {
foreach (array_chunk($update_index, $chunk_size, true) as $chunk) {
$objects = $this->folder->dav->getData($this->folder->href, $this->folder->get_dav_type(), $chunk);
if (!is_array($objects)) {
rcube::raise_error([
'code' => 900,
'message' => "Failed to sync the kolab cache for {$this->folder->href}"
], true);
return false;
}
foreach ($objects as $object) {
if ($object = $this->folder->from_dav($object)) {
$this->save($object, $object['uid']);
}
}
// check time limit and abort sync if running too long
if (++$i % 25 == 0 && time() - $this->sync_start > $time_limit) {
return false;
}
}
}
// Remove deleted objects
$old_index = array_filter($old_index);
if (!empty($old_index)) {
$quoted_uids = join(',', array_map(array($this->db, 'quote'), $old_index));
$this->db->query(
"DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `uid` IN ($quoted_uids)",
$this->folder_id
);
}
return true;
}
/**
* Return current folder index (uid -> etag)
*/
protected function folder_index()
{
// read cache index
$sql_result = $this->db->query(
"SELECT `uid`, `etag` FROM `{$this->cache_table}` WHERE `folder_id` = ?",
$this->folder_id
);
$index = [];
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$index[$sql_arr['uid']] = $sql_arr['etag'];
}
return $index;
}
/**
* Read a single entry from cache or from server directly
*
* @param string Object UID
* @param string Object type to read
* @param string Unused (kept for compat. with the parent class)
*/
public function get($uid, $type = null, $unused = null)
{
if ($this->ready) {
$this->_read_folder_data();
$sql_result = $this->db->query(
"SELECT * FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `uid` = ?",
$this->folder_id,
$uid
);
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$object = $this->_unserialize($sql_arr);
}
}
// fetch from DAV if not present in cache
if (empty($object)) {
if ($object = $this->folder->read_object($uid, $type ?: '*')) {
$this->save($object);
}
}
return $object ?: null;
}
/**
* Insert/Update a cache entry
*
* @param string Object UID
* @param array|false Hash array with object properties to save or false to delete the cache entry
* @param string Unused (kept for compat. with the parent class)
*/
public function set($uid, $object, $unused = null)
{
// remove old entry
if ($this->ready) {
$this->_read_folder_data();
$this->db->query(
"DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `uid` = ?",
$this->folder_id,
$uid
);
}
if ($object) {
$this->save($object);
}
}
/**
* Insert (or update) a cache entry
*
* @param mixed Hash array with object properties to save or false to delete the cache entry
* @param string Optional old message UID (for update)
* @param string Unused (kept for compat. with the parent class)
*/
public function save($object, $olduid = null, $unused = 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['uid'] = $object['uid'];
- $sql_data['etag'] = $object['etag'];
+ $sql_data['uid'] = rcube_charset::clean($object['uid']);
+ $sql_data['etag'] = rcube_charset::clean($object['etag']);
$args = [];
$cols = ['folder_id', 'uid', 'etag', 'changed', 'data', 'tags', 'words'];
$cols = array_merge($cols, $this->extra_cols);
foreach ($cols as $idx => $col) {
$cols[$idx] = $this->db->quote_identifier($col);
$args[] = $sql_data[$col];
}
if ($olduid) {
foreach ($cols as $idx => $col) {
$cols[$idx] = "$col = ?";
}
$query = "UPDATE `{$this->cache_table}` SET " . implode(', ', $cols)
. " WHERE `folder_id` = ? AND `uid` = ?";
$args[] = $this->folder_id;
$args[] = $olduid;
}
else {
$query = "INSERT INTO `{$this->cache_table}` (`created`, " . implode(', ', $cols)
. ") VALUES (" . $this->db->now() . str_repeat(', ?', count($cols)) . ")";
}
$result = $this->db->query($query, $args);
if (!$this->db->affected_rows($result)) {
rcube::raise_error([
'code' => 900,
'message' => "Failed to write to kolab cache"
], true);
}
}
}
/**
* Move an existing cache entry to a new resource
*
* @param string Entry's UID
* @param kolab_storage_folder Target storage folder instance
* @param string Unused (kept for compat. with the parent class)
* @param string Unused (kept for compat. with the parent class)
*/
public function move($uid, $target, $unused1 = null, $unused2 = null)
{
// TODO
}
/**
* Update resource URI for existing folder
*
* @param string Target DAV folder to move it to
*/
public function rename($new_folder)
{
// TODO
}
/**
* Select Kolab objects filtered by the given query
*
* @param array Pseudo-SQL query as list of filter parameter triplets
* triplet: ['<colname>', '<comparator>', '<value>']
* @param bool Set true to only return UIDs instead of complete objects
* @param bool Use fast mode to fetch only minimal set of information
* (no xml fetching and parsing, etc.)
*
* @return array|null|kolab_storage_dataset List of Kolab data objects (each represented as hash array) or UIDs
*/
public function select($query = [], $uids = false, $fast = false)
{
$result = $uids ? [] : new kolab_storage_dataset($this);
$this->_read_folder_data();
// fetch full object data on one query if a small result set is expected
$fetchall = !$uids && ($this->limit ? $this->limit[0] : ($count = $this->count($query))) < self::MAX_RECORDS;
// skip SELECT if we know it will return nothing
if ($count === 0) {
return $result;
}
$sql_query = "SELECT " . ($fetchall ? '*' : "`uid`")
. " FROM `{$this->cache_table}` WHERE `folder_id` = ?"
. $this->_sql_where($query)
. (!empty($this->order_by) ? " ORDER BY " . $this->order_by : '');
$sql_result = $this->limit ?
$this->db->limitquery($sql_query, $this->limit[1], $this->limit[0], $this->folder_id) :
$this->db->query($sql_query, $this->folder_id);
if ($this->db->is_error($sql_result)) {
if ($uids) {
return null;
}
$result->set_error(true);
return $result;
}
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
if ($fast) {
$sql_arr['fast-mode'] = true;
}
if ($uids) {
$result[] = $sql_arr['uid'];
}
else if ($fetchall && ($object = $this->_unserialize($sql_arr))) {
$result[] = $object;
}
else if (!$fetchall) {
$result[] = $sql_arr;
}
}
return $result;
}
/**
* Get number of objects mathing the given query
*
* @param array $query Pseudo-SQL query as list of filter parameter triplets
*
* @return int The number of objects of the given type
*/
public function count($query = [])
{
// read from local cache DB (assume it to be synchronized)
$this->_read_folder_data();
$sql_result = $this->db->query(
"SELECT COUNT(*) AS `numrows` FROM `{$this->cache_table}` ".
"WHERE `folder_id` = ?" . $this->_sql_where($query),
$this->folder_id
);
if ($this->db->is_error($sql_result)) {
return null;
}
$sql_arr = $this->db->fetch_assoc($sql_result);
$count = intval($sql_arr['numrows']);
return $count;
}
/**
* Getter for a single Kolab object identified by its UID
*
* @param string $uid Object UID
*
* @return array|null The Kolab object represented as hash array
*/
public function get_by_uid($uid)
{
$old_limit = $this->limit;
// set limit to skip count query
$this->limit = [1, 0];
$list = $this->select([['uid', '=', $uid]]);
// set the limit back to defined value
$this->limit = $old_limit;
if (!empty($list) && !empty($list[0])) {
return $list[0];
}
}
/**
* Check DAV connection error state
*/
protected function check_error()
{
// TODO ?
}
/**
* Write records into cache using extended inserts to reduce the number of queries to be executed
*
* @param bool Set to false to commit buffered insert, true to force an insert
* @param array Kolab object to cache
*/
protected function _extended_insert($force, $object)
{
static $buffer = '';
$line = '';
$cols = ['folder_id', 'uid', 'etag', 'created', 'changed', 'data', 'tags', 'words'];
if ($this->extra_cols) {
$cols = array_merge($cols, $this->extra_cols);
}
if ($object) {
$sql_data = $this->_serialize($object);
// Skip multi-folder insert for all databases but MySQL
// In Oracle we can't put long data inline, others we don't support yet
if (strpos($this->db->db_provider, 'mysql') !== 0) {
$extra_args = [];
- $params = [$this->folder_id, $object['uid'], $object['etag'], $sql_data['changed'],
- $sql_data['data'], $sql_data['tags'], $sql_data['words']];
+ $params = [
+ $this->folder_id,
+ rcube_charset::clean($object['uid']),
+ rcube_charset::clean($object['etag']),
+ $sql_data['changed'],
+ $sql_data['data'],
+ $sql_data['tags'],
+ $sql_data['words']
+ ];
foreach ($this->extra_cols as $col) {
$params[] = $sql_data[$col];
$extra_args[] = '?';
}
$cols = implode(', ', array_map(function($n) { return "`{$n}`"; }, $cols));
$extra_args = count($extra_args) ? ', ' . implode(', ', $extra_args) : '';
$result = $this->db->query(
"INSERT INTO `{$this->cache_table}` ($cols)"
. " VALUES (?, ?, " . $this->db->now() . ", ?, ?, ?, ?$extra_args)",
$params
);
if (!$this->db->affected_rows($result)) {
rcube::raise_error(array(
'code' => 900, 'message' => "Failed to write to kolab cache"
), true);
}
return;
}
$values = array(
$this->db->quote($this->folder_id),
- $this->db->quote($object['uid']),
- $this->db->quote($object['etag']),
+ $this->db->quote(rcube_charset::clean($object['uid'])),
+ $this->db->quote(rcube_charset::clean($object['etag'])),
$this->db->now(),
$this->db->quote($sql_data['changed']),
$this->db->quote($sql_data['data']),
$this->db->quote($sql_data['tags']),
$this->db->quote($sql_data['words']),
);
foreach ($this->extra_cols as $col) {
$values[] = $this->db->quote($sql_data[$col]);
}
$line = '(' . join(',', $values) . ')';
}
if ($buffer && ($force || (strlen($buffer) + strlen($line) > $this->max_sql_packet()))) {
$columns = implode(', ', array_map(function($n) { return "`{$n}`"; }, $cols));
$update = implode(', ', array_map(function($i) { return "`{$i}` = VALUES(`{$i}`)"; }, array_slice($cols, 2)));
$result = $this->db->query(
"INSERT INTO `{$this->cache_table}` ($columns) VALUES $buffer"
. " ON DUPLICATE KEY UPDATE $update"
);
if (!$this->db->affected_rows($result)) {
rcube::raise_error(array(
'code' => 900, 'message' => "Failed to write to kolab cache"
), true);
}
$buffer = '';
}
$buffer .= ($buffer ? ',' : '') . $line;
}
/**
* Helper method to turn stored cache data into a valid storage object
*/
protected function _unserialize($sql_arr)
{
if ($sql_arr['fast-mode'] && !empty($sql_arr['data']) && ($object = json_decode($sql_arr['data'], true))) {
foreach ($this->data_props as $prop) {
if (isset($object[$prop]) && is_array($object[$prop]) && $object[$prop]['cl'] == 'DateTime') {
$object[$prop] = new DateTime($object[$prop]['dt'], new DateTimeZone($object[$prop]['tz']));
}
else if (!isset($object[$prop]) && isset($sql_arr[$prop])) {
$object[$prop] = $sql_arr[$prop];
}
}
if ($sql_arr['created'] && empty($object['created'])) {
$object['created'] = new DateTime($sql_arr['created']);
}
if ($sql_arr['changed'] && empty($object['changed'])) {
$object['changed'] = new DateTime($sql_arr['changed']);
}
$object['_type'] = $sql_arr['type'] ?: $this->folder->type;
$object['uid'] = $sql_arr['uid'];
$object['etag'] = $sql_arr['etag'];
}
// Fetch a complete object from the server
else {
// TODO: Fetching objects one-by-one from DAV server is slow
$object = $this->folder->read_object($sql_arr['uid'], '*');
}
return $object;
}
}
diff --git a/plugins/libkolab/lib/kolab_storage_dav_cache_contact.php b/plugins/libkolab/lib/kolab_storage_dav_cache_contact.php
index 66d2b830..0ca118dd 100644
--- a/plugins/libkolab/lib/kolab_storage_dav_cache_contact.php
+++ b/plugins/libkolab/lib/kolab_storage_dav_cache_contact.php
@@ -1,116 +1,113 @@
<?php
/**
* Kolab storage cache class for contact objects
*
* @author Aleksander Machniak <machniak@apcheleia-it.ch>
*
* Copyright (C) 2013-2022, Apheleia IT AG <contact@apcheleia-it.ch>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-class kolab_storage_cache_contact extends kolab_storage_cache
+class kolab_storage_dav_cache_contact extends kolab_storage_dav_cache
{
protected $extra_cols_max = 255;
protected $extra_cols = ['type', 'name', 'firstname', 'surname', 'email'];
protected $data_props = ['type', 'name', 'firstname', 'middlename', 'prefix', 'suffix', 'surname', 'email', 'organization', 'member'];
- protected $fulltext_cols = ['name', 'firstname', 'surname', 'middlename', 'email:address'];
+ protected $fulltext_cols = ['name', 'firstname', 'surname', 'middlename', 'email'];
/**
* Helper method to convert the given Kolab object into a dataset to be written to cache
*
* @override
*/
protected function _serialize($object)
{
$sql_data = parent::_serialize($object);
- $sql_data['type'] = $object['_type'];
+ $sql_data['type'] = $object['_type'] ?: 'contact';
// 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']);
+ $sql_data['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'] = '';
+ foreach ($object as $colname => $value) {
+ list($col, $field) = explode(':', $colname);
+ if ($col == 'email' && !empty($value)) {
+ $sql_data['email'] = is_array($value) ? $value[0] : $value;
+ break;
+ }
}
// use organization if name is empty
if (empty($sql_data['name']) && !empty($object['organization'])) {
$sql_data['name'] = rcube_charset::clean($object['organization']);
}
// make sure some data is not longer that database limit (#5291)
foreach ($this->extra_cols as $col) {
if (strlen($sql_data[$col]) > $this->extra_cols_max) {
$sql_data[$col] = rcube_charset::clean(substr($sql_data[$col], 0, $this->extra_cols_max));
}
}
$sql_data['tags'] = ' ' . join(' ', $this->get_tags($object)) . ' '; // pad with spaces for strict/prefix search
$sql_data['words'] = ' ' . join(' ', $this->get_words($object)) . ' ';
return $sql_data;
}
/**
* Callback to get words to index for fulltext search
*
* @return array List of words to save in cache
*/
public function get_words($object)
{
$data = '';
- foreach ($this->fulltext_cols as $colname) {
+
+ foreach ($object as $colname => $value) {
list($col, $field) = explode(':', $colname);
- if ($field) {
- $a = [];
- foreach ((array)$object[$col] as $attr)
- $a[] = $attr[$field];
- $val = join(' ', $a);
- }
- else {
- $val = is_array($object[$col]) ? join(' ', $object[$col]) : $object[$col];
+ $val = '';
+ if (in_array($col, $this->fulltext_cols)) {
+ $val = is_array($value) ? join(' ', $value) : $value;
}
- if (strlen($val))
+ if (strlen($val)) {
$data .= $val . ' ';
+ }
}
return array_unique(rcube_utils::normalize_string($data, true));
}
/**
* Callback to get object specific tags to cache
*
* @return array List of tags to save in cache
*/
public function get_tags($object)
{
$tags = [];
if (!empty($object['birthday'])) {
$tags[] = 'x-has-birthday';
}
return $tags;
}
}
diff --git a/plugins/libkolab/lib/kolab_storage_dav_folder.php b/plugins/libkolab/lib/kolab_storage_dav_folder.php
index 32ecdd5d..c7a46f47 100644
--- a/plugins/libkolab/lib/kolab_storage_dav_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_dav_folder.php
@@ -1,546 +1,587 @@
<?php
/**
* A class representing a DAV folder object.
*
* @author Aleksander Machniak <machniak@apheleia-it.ch>
*
* Copyright (C) 2014-2022, Apheleia IT AG <contact@apheleia-it.ch>
*
* 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_dav_folder extends kolab_storage_folder
{
public $dav;
public $href;
public $attributes;
/**
* Object constructor
*/
public function __construct($dav, $attributes, $type_annotation = '')
{
$this->attributes = $attributes;
- $this->href = $this->attributes['href'];
- // Here we assume the last element of the folder path is the folder ID
- // if that's not the case, we should consider generating an ID
- $href = explode('/', unslashify($this->href));
- $this->id = $href[count($href) - 1];
+ $this->href = $this->attributes['href'];
+ $this->id = md5($this->href);
$this->dav = $dav;
$this->valid = true;
list($this->type, $suffix) = explode('.', $type_annotation);
$this->default = $suffix == 'default';
$this->subtype = $this->default ? '' : $suffix;
// Init cache
$this->cache = kolab_storage_dav_cache::factory($this);
}
/**
* Returns the owner of the folder.
*
* @param bool Return a fully qualified owner name (i.e. including domain for shared folders)
*
* @return string The owner of this folder.
*/
public function get_owner($fully_qualified = false)
{
// return cached value
if (isset($this->owner)) {
return $this->owner;
}
$rcube = rcube::get_instance();
$this->owner = $rcube->get_user_name();
$this->valid = true;
// TODO: Support shared folders
return $this->owner;
}
/**
* Get a folder Etag identifier
*/
public function get_ctag()
{
return $this->attributes['ctag'];
}
/**
* Getter for the name of the namespace to which the folder belongs
*
* @return string Name of the namespace (personal, other, shared)
*/
public function get_namespace()
{
// TODO: Support shared folders
return 'personal';
}
/**
* Get the display name value of this folder
*
* @return string Folder name
*/
public function get_name()
{
return kolab_storage_dav::object_name($this->attributes['name']);
}
/**
* Getter for the top-end folder name (not the entire path)
*
* @return string Name of this folder
*/
public function get_foldername()
{
return $this->attributes['name'];
}
public function get_folder_info()
{
return []; // todo ?
}
/**
* Getter for parent folder path
*
* @return string Full path to parent folder
*/
public function get_parent()
{
// TODO
return '';
}
/**
* Compose a unique resource URI for this folder
*/
public function get_resource_uri()
{
if (!empty($this->resource_uri)) {
return $this->resource_uri;
}
// compose fully qualified ressource uri for this instance
$host = preg_replace('|^https?://|', 'dav://' . urlencode($this->get_owner(true)) . '@', $this->dav->url);
$path = $this->href[0] == '/' ? $this->href : "/{$this->href}";
$host_path = parse_url($host, PHP_URL_PATH);
if ($host_path && strpos($path, $host_path) === 0) {
$path = substr($path, strlen($host_path));
}
$this->resource_uri = unslashify($host) . $path;
return $this->resource_uri;
}
/**
* Getter for the Cyrus mailbox identifier corresponding to this folder
* (e.g. user/john.doe/Calendar/Personal@example.org)
*
* @return string Mailbox ID
*/
public function get_mailbox_id()
{
// TODO: This is used with Bonnie related features
return '';
}
/**
* Get the color value stored in metadata
*
* @param string Default color value to return if not set
*
* @return mixed Color value from the folder metadata or $default if not set
*/
public function get_color($default = null)
{
return !empty($this->attributes['color']) ? $this->attributes['color'] : $default;
}
/**
* Get ACL information for this folder
*
* @return string Permissions as string
*/
public function get_myrights()
{
// TODO
return '';
}
/**
* Helper method to extract folder UID
*
* @return string Folder's UID
*/
public function get_uid()
{
// TODO ???
return '';
}
/**
* Check activation status of this folder
*
* @return bool True if enabled, false if not
*/
public function is_active()
{
// TODO
return true;
}
/**
* Change activation status of this folder
*
* @param bool The desired subscription status: true = active, false = not active
*
* @return bool True on success, false on error
*/
public function activate($active)
{
// TODO
return true;
}
/**
* Check subscription status of this folder
*
* @return bool True if subscribed, false if not
*/
public function is_subscribed()
{
// TODO
return true;
}
/**
* Change subscription status of this folder
*
* @param bool The desired subscription status: true = subscribed, false = not subscribed
*
* @return True on success, false on error
*/
public function subscribe($subscribed)
{
// TODO
return true;
}
/**
* Delete the specified object from this folder.
*
* @param array|string $object The Kolab object to delete or object UID
* @param bool $expunge Should the folder be expunged?
*
* @return bool True if successful, false on error
*/
public function delete($object, $expunge = true)
{
if (!$this->valid) {
return false;
}
$uid = is_array($object) ? $object['uid'] : $object;
$success = $this->dav->delete($this->object_location($uid), $content);
if ($success) {
$this->cache->set($uid, false);
}
return $success;
}
/**
*
*/
public function delete_all()
{
if (!$this->valid) {
return false;
}
// TODO: This method is used by kolab_addressbook plugin only
$this->cache->purge();
return false;
}
/**
* Restore a previously deleted object
*
* @param string $uid Object UID
*
* @return mixed Message UID on success, false on error
*/
public function undelete($uid)
{
if (!$this->valid) {
return false;
}
// TODO
return false;
}
/**
* Move a Kolab object message to another IMAP folder
*
* @param string Object UID
* @param string IMAP folder to move object to
*
* @return bool True on success, false on failure
*/
public function move($uid, $target_folder)
{
if (!$this->valid) {
return false;
}
// TODO
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 object 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'];
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]);
}
}
}
// process attachments
if (is_array($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]);
}
}
}
*/
$rcmail = rcube::get_instance();
$result = false;
// generate and save object message
if ($content = $this->to_dav($object)) {
- $method = $uid ? 'update' : 'create';
- $result = $this->dav->{$method}($this->object_location($object['uid']), $content);
+ $method = $uid ? 'update' : 'create';
+ $dav_type = $this->get_dav_type();
+ $result = $this->dav->{$method}($this->object_location($object['uid']), $content, $dav_type);
// Note: $result can be NULL if the request was successful, but ETag wasn't returned
if ($result !== false) {
// insert/update object in the cache
$object['etag'] = $result;
$this->cache->save($object, $uid);
$result = true;
}
}
return $result;
}
/**
* Fetch the object the DAV server and convert to internal format
*
* @param string The object UID to fetch
* @param string The object type expected (use wildcard '*' to accept all types)
* @param string Unused (kept for compat. with the parent class)
*
* @return mixed Hash array representing the Kolab object, a kolab_format instance or false if not found
*/
public function read_object($uid, $type = null, $folder = null)
{
if (!$this->valid) {
return false;
}
$href = $this->object_location($uid);
$objects = $this->dav->getData($this->href, $this->get_dav_type(), [$href]);
if (!is_array($objects) || count($objects) != 1) {
rcube::raise_error([
'code' => 900,
'message' => "Failed to fetch {$href}"
], true);
return false;
}
return $this->from_dav($objects[0]);
}
/**
* Convert DAV object into PHP array
*
* @param array Object data in kolab_dav_client::fetchData() format
*
* @return array Object properties
*/
public function from_dav($object)
{
if ($this->type == 'event') {
$ical = libcalendaring::get_ical();
$events = $ical->import($object['data']);
if (!count($events) || empty($events[0]['uid'])) {
return false;
}
$result = $events[0];
}
+ else if ($this->type == 'contact') {
+ if (stripos($object['data'], 'BEGIN:VCARD') !== 0) {
+ return false;
+ }
+
+ $vcard = new rcube_vcard($object['data'], RCUBE_CHARSET, false);
+
+ if (!empty($vcard->displayname) || !empty($vcard->surname) || !empty($vcard->firstname) || !empty($vcard->email)) {
+ $result = $vcard->get_assoc();
+ }
+ else {
+ return false;
+ }
+ }
$result['etag'] = $object['etag'];
$result['href'] = $object['href'];
- $result['uid'] = $object['uid'] ?: $result['uid'];
+ $result['uid'] = $object['uid'] ?: $result['uid'];
return $result;
}
/**
* Convert Kolab object into DAV format (iCalendar)
*/
public function to_dav($object)
{
$result = '';
if ($this->type == 'event') {
$ical = libcalendaring::get_ical();
if (!empty($object['exceptions'])) {
$object['recurrence']['EXCEPTIONS'] = $object['exceptions'];
}
$result = $ical->export([$object]);
}
+ else if ($this->type == 'contact') {
+ // copy values into vcard object
+ $vcard = new rcube_vcard('', RCUBE_CHARSET, false, ['uid' => 'UID']);
+
+ $vcard->set('groups', null);
+
+ foreach ($object as $key => $values) {
+ list($field, $section) = rcube_utils::explode(':', $key);
+
+ // avoid casting DateTime objects to array
+ if (is_object($values) && is_a($values, 'DateTime')) {
+ $values = [$values];
+ }
+
+ foreach ((array) $values as $value) {
+ if (isset($value)) {
+ $vcard->set($field, $value, $section);
+ }
+ }
+ }
+
+ $result = $vcard->export(false);
+ }
+
+ if ($result) {
+ // The content must be UTF-8, otherwise if we try to fetch the object
+ // from server XML parsing would fail.
+ $result = rcube_charset::clean($result);
+ }
return $result;
}
protected function object_location($uid)
{
return unslashify($this->href) . '/' . urlencode($uid) . '.' . $this->get_dav_ext();
}
/**
* Get a folder DAV content type
*/
public function get_dav_type()
{
$types = [
'event' => 'VEVENT',
'task' => 'VTODO',
'contact' => 'VCARD',
];
return $types[$this->type];
}
/**
* Get a DAV file extension for specified Kolab type
*/
public function get_dav_ext()
{
$types = [
'event' => 'ics',
'task' => 'ics',
'contact' => 'vcf',
];
return $types[$this->type];
}
/**
* Return folder name as string representation of this object
*
* @return string Full IMAP folder name
*/
public function __toString()
{
return $this->attributes['name'];
}
}

File Metadata

Mime Type
text/x-diff
Expires
Tue, Jun 10, 6:09 AM (1 d, 10 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
196973
Default Alt Text
(187 KB)

Event Timeline