Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F256920
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
187 KB
Referenced Files
None
Subscribers
None
View Options
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_] .= ' ' . 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, ' » ', 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
Details
Attached
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)
Attached To
Mode
R14 roundcubemail-plugins-kolab
Attached
Detach File
Event Timeline
Log In to Comment