Page MenuHomePhorge

No OneTemporary

Authored By
Unknown
Size
80 KB
Referenced Files
None
Subscribers
None
diff --git a/plugins/kolab_addressbook/kolab_addressbook.js b/plugins/kolab_addressbook/kolab_addressbook.js
index 79628e87..82b6d9dd 100644
--- a/plugins/kolab_addressbook/kolab_addressbook.js
+++ b/plugins/kolab_addressbook/kolab_addressbook.js
@@ -1,349 +1,265 @@
/**
* Client script for the Kolab address book plugin
*
* @author Aleksander Machniak <machniak@kolabsys.com>
+ * @author Thomas Bruederli <bruederli@kolabsys.com>
*
* @licstart The following is the entire license notice for the
* JavaScript code in this file.
*
- * Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com>
+ * Copyright (C) 2011-2014, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @licend The above is the entire license notice
* for the JavaScript code in this file.
*/
if (window.rcmail) {
rcmail.addEventListener('init', function() {
rcmail.set_book_actions();
if (rcmail.gui_objects.editform && rcmail.env.action.match(/^plugin\.book/)) {
rcmail.enable_command('book-save', true);
}
// add contextmenu items
if (window.rcm_contextmenu_register_command) {
var menu = $('#rcmGroupMenu');
rcm_contextmenu_register_command(
'book-edit',
function(cmd,el){ rcmail.book_edit() },
'kolab_addressbook.bookedit',
null,
true,
false,
false,
menu
);
rcm_contextmenu_register_command(
'book-delete',
function(cmd,el){ rcmail.book_delete() },
'kolab_addressbook.bookdelete',
null,
false,
false,
false,
menu
);
if (rcmail.env.kolab_addressbook_carddav_url) {
rcm_contextmenu_register_command(
'book-showurl',
function(cmd,el){ rcmail.book_showurl() },
'kolab_addressbook.bookshowurl',
null,
false,
false,
false,
menu
);
}
// adjust menu items when shown
rcmail.addEventListener('contextmenu_show', function(p){
if (p.menu.attr('id') != 'rcmGroupMenu')
return;
var m = String(p.src.attr('id')).match(/rcmli([a-z0-9\-_=]+)/i),
source = m && m.length ? rcmail.html_identifier_decode(m[1]) : null,
sources = rcmail.env.address_sources,
editable = source && sources[source] && sources[source].kolab && sources[source].editable,
showurl = source && sources[source] && sources[source].carddavurl;
if (p.menu) {
p.menu[editable ? 'enableContextMenuItems' : 'disableContextMenuItems']('#book-edit,#book-delete');
p.menu[showurl ? 'enableContextMenuItems' : 'disableContextMenuItems']('#book-showurl');
}
});
}
});
rcmail.addEventListener('listupdate', function() {
rcmail.set_book_actions();
});
}
// (De-)activates address book management commands
rcube_webmail.prototype.set_book_actions = function()
{
var source = this.env.source,
sources = this.env.address_sources;
this.enable_command('book-create', true);
this.enable_command('book-edit', 'book-delete', source && sources[source] && sources[source].kolab && sources[source].editable);
this.enable_command('book-showurl', source && sources[source] && sources[source].carddavurl);
};
rcube_webmail.prototype.book_create = function()
{
this.book_show_contentframe('create');
};
rcube_webmail.prototype.book_edit = function()
{
this.book_show_contentframe('edit');
};
rcube_webmail.prototype.book_delete = function()
{
if (this.env.source != '' && confirm(this.get_label('kolab_addressbook.bookdeleteconfirm'))) {
var lock = this.set_busy(true, 'kolab_addressbook.bookdeleting');
this.http_request('plugin.book', '_act=delete&_source='+urlencode(this.book_realname()), lock);
}
};
rcube_webmail.prototype.book_showurl = function()
{
var source = this.env.source ? this.env.address_sources[this.env.source] : null;
if (source && source.carddavurl) {
$('div.showurldialog:ui-dialog').dialog('close');
var $dialog = $('<div>').addClass('showurldialog').append('<p>'+rcmail.gettext('carddavurldescription', 'kolab_addressbook')+'</p>'),
textbox = $('<textarea>').addClass('urlbox').css('width', '100%').attr('rows', 2).appendTo($dialog);
$dialog.dialog({
resizable: true,
closeOnEscape: true,
title: rcmail.gettext('bookshowurl', 'kolab_addressbook'),
close: function() {
$dialog.dialog("destroy").remove();
},
width: 520
}).show();
textbox.val(source.carddavurl).select();
}
};
// displays page with book edit/create form
rcube_webmail.prototype.book_show_contentframe = function(action, framed)
{
var add_url = '', target = window;
// unselect contact
this.contact_list.clear_selection();
this.enable_command('edit', 'delete', 'compose', false);
if (this.env.contentframe && window.frames && window.frames[this.env.contentframe]) {
add_url = '&_framed=1';
target = window.frames[this.env.contentframe];
this.show_contentframe(true);
}
else if (framed)
return false;
if (action) {
this.lock_frame();
this.location_href(this.env.comm_path+'&_action=plugin.book&_act='+action
+'&_source='+urlencode(this.book_realname())
+add_url, target);
}
return true;
};
// submits book create/update form
rcube_webmail.prototype.book_save = function()
{
var form = this.gui_objects.editform,
input = $("input[name='_name']", form)
if (input.length && input.val() == '') {
alert(this.get_label('kolab_addressbook.nobooknamewarning'));
input.focus();
return;
}
input = this.display_message(this.get_label('kolab_addressbook.booksaving'), 'loading');
$('<input type="hidden" name="_unlock" />').val(input).appendTo(form);
form.submit();
};
// action executed after book delete
rcube_webmail.prototype.book_delete_done = function(id, recur)
{
var n, groups = this.env.contactgroups,
sources = this.env.address_sources,
olddata = sources[id];
this.treelist.remove(id);
for (n in groups)
if (groups[n].source == id) {
delete this.env.contactgroups[n];
delete this.env.contactfolders[n];
}
delete this.env.address_sources[id];
delete this.env.contactfolders[id];
if (recur)
return;
this.enable_command('group-create', 'book-edit', 'book-delete', false);
// remove subfolders
olddata.realname += this.env.delimiter;
for (n in sources)
if (sources[n].realname && sources[n].realname.indexOf(olddata.realname) == 0)
this.book_delete_done(n, true);
};
// action executed after book create/update
-rcube_webmail.prototype.book_update = function(data, old, recur)
+rcube_webmail.prototype.book_update = function(data, old)
{
- var n, i, id, len, link, row, prop, olddata, oldid, name, sources, level,
- folders = [], classes = ['addressbook'],
- groups = this.env.contactgroups;
+ var link, classes = [(data.group || ''), 'addressbook'];
- this.env.contactfolders[data.id] = this.env.address_sources[data.id] = data;
this.show_contentframe(false);
- // update (remove old row)
- if (old && old != data.id) {
- olddata = this.env.address_sources[old];
- delete this.env.address_sources[old];
- delete this.env.contactfolders[old];
- this.treelist.remove(old);
- }
-
- sources = this.env.address_sources;
-
// set row attributes
if (data.readonly)
classes.push('readonly');
- if (data.class_name)
- classes.push(data.class_name);
- // updated currently selected book
- if (this.env.source != '' && this.env.source == old) {
- classes.push('selected');
- this.env.source = data.id;
- }
+ if (data.group)
+ classes.push(data.group);
link = $('<a>').html(data.name)
.attr({
- href: '#', rel: data.id,
+ href: this.url('', { _source: data.id }),
+ rel: data.id,
onclick: "return rcmail.command('list', '" + data.id + "', this)"
});
- // add row at the end of the list
- // treelist widget is not very smart, we need
- // to do sorting and add groups list by ourselves
- this.treelist.insert({id: data.id, html:link, classes: classes, childlistclass: 'groups'}, '', false);
- row = $(this.treelist.get_item(data.id));
- row.append($('<ul class="groups">').hide());
-
- // we need to sort rows because treelist can't sort by property
- $.each(sources, function(i, v) {
- if (v.kolab && v.realname)
- folders.push(v.realname);
- });
- folders.sort();
-
- for (n=0, len=folders.length; n<len; n++)
- if (folders[n] == data.realname)
- break;
-
- // find the row before and re-insert after it
- if (n && n < len - 1) {
- name = folders[n-1];
- for (n in sources)
- if (sources[n].realname && sources[n].realname == name) {
- row.detach().insertAfter(this.treelist.get_item(n));
- break;
- }
+ // update (remove old row)
+ if (old) {
+ this.treelist.update(old, { id: data.id, html:link, classes: classes, parent:(old != data.id ? data.parent : null) }, data.group || true);
+ }
+ else {
+ this.treelist.insert({ id: data.id, html:link, classes: classes, childlistclass: 'groups' }, data.parent, data.group || true);
}
- if (olddata) {
- // update groups
- for (n in groups) {
- if (groups[n].source == old) {
- prop = groups[n];
- prop.type = 'group';
- prop.source = data.id;
- id = 'G' + prop.source + prop.id;
-
- link = $('<a>').text(prop.name)
- .attr({
- href: '#', rel: prop.source + ':' + prop.id,
- onclick: "return rcmail.command('listgroup', {source: '"+prop.source+"', id: '"+prop.id+"'}, this)"
- });
-
- this.treelist.insert({id:id, html:link, classes:['contactgroup']}, prop.source, true);
-
- this.env.contactfolders[id] = this.env.contactgroups[id] = prop;
- delete this.env.contactgroups[n];
- delete this.env.contactfolders[n];
- }
- }
-
- if (recur)
- return;
-
- // update subfolders
- old += '_';
- level = olddata.realname.split(this.env.delimiter).length - data.realname.split(this.env.delimiter).length;
- olddata.realname += this.env.delimiter;
-
- for (n in sources) {
- if (sources[n].realname && sources[n].realname.indexOf(olddata.realname) == 0) {
- prop = sources[n];
- oldid = sources[n].id;
- // new ID
- prop.id = data.id + '_' + n.substr(old.length);
- prop.realname = data.realname + prop.realname.substr(olddata.realname.length - 1);
- name = prop.name;
-
- // update display name
- if (level > 0) {
- for (i=level; i>0; i--)
- name = name.replace(/^&nbsp;&nbsp;/, '');
- }
- else if (level < 0) {
- for (i=level; i<0; i++)
- name = '&nbsp;&nbsp;' + name;
- }
+ this.env.contactfolders[data.id] = this.env.address_sources[data.id] = data;
- prop.name = name;
- this.book_update(prop, oldid, true)
- }
- }
+ // updated currently selected book
+ if (this.env.source != '' && this.env.source == old) {
+ this.treelist.select(data.id);
+ this.env.source = data.id;
}
};
// returns real IMAP folder name
rcube_webmail.prototype.book_realname = function()
{
var source = this.env.source, sources = this.env.address_sources;
return source != '' && sources[source] && sources[source].realname ? sources[source].realname : '';
};
diff --git a/plugins/kolab_addressbook/kolab_addressbook.php b/plugins/kolab_addressbook/kolab_addressbook.php
index f28d4966..530580bb 100644
--- a/plugins/kolab_addressbook/kolab_addressbook.php
+++ b/plugins/kolab_addressbook/kolab_addressbook.php
@@ -1,538 +1,650 @@
<?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, 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 = '?(?!login|logout).*';
private $sources;
+ private $folders;
private $rc;
private $ui;
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()
{
require_once(dirname(__FILE__) . '/lib/rcube_kolab_contacts.php');
$this->rc = rcube::get_instance();
// load required plugin
$this->require_plugin('libkolab');
// 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('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'));
// 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);
}
}
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'));
}
}
/**
* 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();
$undelete = $this->rc->config->get('undo_timeout');
// 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();
- $names = array();
-
foreach ($this->_list_sources() as $abook_id => $abook) {
- $name = kolab_storage::folder_displayname($abook->get_name(), $names);
-
// register this address source
- $sources[$abook_id] = array(
- 'id' => $abook_id,
- 'name' => $name,
- 'readonly' => $abook->readonly,
- 'editable' => $abook->editable,
- 'groups' => $abook->groups,
- 'undelete' => $abook->undelete && $undelete,
- 'realname' => rcube_charset::convert($abook->get_realname(), 'UTF7-IMAP'), // IMAP folder name
- 'class_name' => $abook->get_namespace(),
- 'carddavurl' => $abook->get_carddav_url(),
- 'kolab' => true,
- );
+ $sources[$abook_id] = $this->abook_prop($abook_id, $abook);
}
// 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)
+ {
+ return array(
+ 'id' => $id,
+ 'name' => $abook->get_name(),
+ 'listname' => $abook->get_foldername(),
+ 'readonly' => $abook->readonly,
+ 'editable' => $abook->editable,
+ '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(),
+ 'carddavurl' => $abook->get_carddav_url(),
+ 'kolab' => true,
+ );
+ }
+
+ /**
+ *
+ */
+ public function directorylist_html($args)
+ {
+ $out = '';
+ $jsdata = array();
+ $sources = (array)$this->rc->get_address_sources();
+
+ // list all non-kolab sources first
+ foreach (array_filter($sources, function($source){ return empty($source['kolab']); }) as $j => $source) {
+ $id = strval(strlen($source['id']) ? $source['id'] : $j);
+ $out .= $this->addressbook_list_item($id, $source, $jsdata) . '</li>';
+ }
+
+ // render a hierarchical list of kolab contact folders
+ kolab_storage::folder_hierarchy($this->folders, $tree);
+ $out .= $this->folder_tree_html($tree, $sources, $jsdata);
+
+ $this->rc->output->set_env('contactgroups', $jsdata);
+
+ $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 = array(
+ 'id' => $folder->id,
+ 'name' => $folder->get_name(),
+ 'listname' => $folder->get_foldername(),
+ 'group' => $folder->get_namespace(),
+ 'readonly' => true,
+ 'editable' => false,
+ 'kolab' => true,
+ 'virtual' => true,
+ );
+ }
+
+ $content = $this->addressbook_list_item($id, $source, $jsdata);
+
+ if (!empty($folder->children)) {
+ $child_html = $this->folder_tree_html($folder, $data, $jsdata);
+
+ if (!empty($child_html) && preg_match('!</ul>\n*$!', $content)) {
+ $content = preg_replace('!</ul>\n*$!', $child_html . '</ul>', $content);
+ }
+ else if (!empty($child_html)) {
+ $content .= html::tag('ul', array('style' => ($is_collapsed ? "display:none;" : null)), $child_html);
+ }
+ }
+
+ $out .= $content . '</li>';
+ }
+
+ return $out;
+ }
+
+ /**
+ *
+ */
+ protected function addressbook_list_item($id, $source, &$jsdata, $checkbox = false)
+ {
+ $folder = $this->folders[$id];
+ $current = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC);
+
+ // set class name(s)
+ $classes = array($source['group'] ?: '000', 'addressbook');
+ 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);
+ $out .= html::tag('li', array(
+ 'id' => 'rcmli' . rcube_utils::html_identifier($id, true),
+ 'class' => join(' ', $classes),
+ 'noclose' => true,
+ ),
+ ($source['virtual'] ?
+ html::a(array('tabindex' => '0'), $name) :
+ html::a(array(
+ 'href' => $this->rc->url(array('_source' => $id)),
+ 'rel' => $source['id'],
+ 'onclick' => "return " . rcmail_output::JS_OBJECT_NAME.".command('list','" . rcube::JQ($id) . "',this)",
+ ), $name)
+ )
+ );
+
+ $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') {
return $args;
}
$abook_prio = $this->addressbook_prio();
// here we cannot use rc->config->get()
$sources = $GLOBALS['CONFIG']['autocomplete_addressbooks'];
// 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']) {
$this->_list_sources();
if ($this->sources[$p['id']]) {
$p['instance'] = $this->sources[$p['id']];
}
+ else {
+ $folder = kolab_storage::get_folder(kolab_storage::id_decode($p['id']));
+ if ($folder->type) { // try with unencoded (old-style) identifier
+ $folder = kolab_storage::get_folder(kolab_storage::id_decode($p['id'], false));
+ }
+ if ($folder->type) {
+ $this->sources[$p['id']] = new rcube_kolab_contacts($folder->name);
+ $p['instance'] = $this->sources[$p['id']];
+ }
+ }
}
return $p;
}
private function _list_sources()
{
// already read sources
if (isset($this->sources))
return $this->sources;
+ kolab_storage::$encode_ids = true;
$this->sources = array();
+ $this->folders = array();
$abook_prio = $this->addressbook_prio();
// Personal address source(s) disabled?
if ($abook_prio == self::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
$names = array();
foreach ($folders as $folder) {
// create instance of rcube_contacts
- $abook_id = kolab_storage::folder_id($folder->name, false);
+ $abook_id = $folder->id;
$abook = new rcube_kolab_contacts($folder->name);
$this->sources[$abook_id] = $abook;
+ $this->folders[$abook_id] = $folder;
}
}
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']);
$p['form']['personal']['content'] = $this->_sort_form_fields($p['form']['personal']['content']);
/* 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),
)
);
*/
}
return $p;
}
private function _sort_form_fields($contents)
{
$block = array();
$contacts = reset($this->sources);
foreach (array_keys($contacts->coltypes) as $col) {
if (isset($contents[$col]))
$block[$col] = $contents[$col];
}
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');
$abook_type = $this->rc->config->get('address_book_type');
// Hide option if there's no global addressbook
if (empty($ldap_public) || $abook_type != 'ldap') {
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, 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) get_input_value('_'.$key, RCUBE_INPUT_POST);
}
return $args;
}
/**
* Handler for plugin actions
*/
public function book_actions()
{
$action = trim(get_input_value('_act', RCUBE_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(get_input_value('_name', RCUBE_INPUT_POST)),
'oldname' => trim(get_input_value('_oldname', RCUBE_INPUT_POST, true)), // UTF7-IMAP
'parent' => trim(get_input_value('_parent', RCUBE_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) {
$storage = $this->rc->get_storage();
$delimiter = $storage->get_hierarchy_delimiter();
- $kolab_folder = new rcube_kolab_contacts($folder);
-
- // create display name for the folder (see self::address_sources())
- if (strpos($folder, $delimiter)) {
- $names = array();
- foreach ($this->_list_sources() as $abook) {
- $realname = $abook->get_realname();
- // The list can be not updated yet, handle old folder name
- if ($type == 'update' && $realname == $prop['oldname']) {
- $abook = $kolab_folder;
- $realname = $folder;
- }
-
- $name = kolab_storage::folder_displayname($abook->get_name(), $names);
-
- if ($realname == $folder) {
- break;
- }
- }
- }
- else {
- $name = $kolab_folder->get_name();
- }
+ $kolab_folder = kolab_storage::get_folder($folder);
$this->rc->output->show_message('kolab_addressbook.book'.$type.'d', 'confirmation');
$this->rc->output->command('set_env', 'delimiter', $delimiter);
$this->rc->output->command('book_update', array(
'id' => kolab_storage::folder_id($folder),
- 'name' => $name,
+ 'name' => $kolab_folder->get_foldername(),
'readonly' => false,
'editable' => true,
'groups' => true,
'realname' => rcube_charset::convert($folder, 'UTF7-IMAP'), // IMAP folder name
- 'class_name' => $kolab_folder->get_namespace(),
+ 'group' => $kolab_folder->get_namespace(),
+ 'parent' => kolab_storage::folder_id($kolab_folder->get_parent()),
'kolab' => true,
), kolab_storage::folder_id($prop['oldname']));
$this->rc->output->send('iframe');
}
if (!$error)
$error = $plugin['message'] ? $plugin['message'] : 'kolab_addressbook.book'.$type.'error';
$this->rc->output->show_message($error, 'error');
// display the form again
$this->ui->book_edit();
}
/**
* Handler for address book delete action (AJAX)
*/
private function book_delete()
{
$folder = trim(get_input_value('_source', RCUBE_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));
}
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');
$abook_type = $this->rc->config->get('address_book_type');
if (empty($ldap_public) || $abook_type != 'ldap') {
$abook_prio = 1;
}
}
return $abook_prio;
}
}
diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
index ffce9b5e..2e93d462 100644
--- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
+++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php
@@ -1,1249 +1,1267 @@
<?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 $readonly = true;
public $editable = false;
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;
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);
// set localized labels for proprietary cols
foreach ($this->coltypes as $col => $prop) {
if (is_string($prop['label']))
$this->coltypes[$col]['label'] = rcube_label($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 editable flags according to folder permissions
if ($this->ready) {
if ($this->storagefolder->get_owner() == $_SESSION['username']) {
$this->editable = true;
$this->readonly = false;
}
else {
$rights = $this->storagefolder->get_myrights();
if (!PEAR::isError($rights)) {
if (strpos($rights, 'i') !== false)
$this->readonly = false;
if (strpos($rights, 'a') !== false || strpos($rights, 'x') !== false)
$this->editable = true;
}
}
}
$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()
{
$folder = kolab_storage::object_name($this->imap_folder, $this->namespace);
return $folder;
}
+ /**
+ * 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();
+ }
+
/**
* Compose an URL for CardDAV access to this address book (if configured)
*/
public function get_carddav_url()
{
$url = null;
$rcmail = rcmail::get_instance();
if ($template = $rcmail->config->get('kolab_addressbook_carddav_url', null)) {
return strtr($template, array(
'%h' => $_SERVER['HTTP_HOST'],
'%u' => urlencode($rcmail->get_user_name()),
'%i' => urlencode($this->storagefolder->get_uid()),
'%n' => urlencode($this->imap_folder),
));
}
return false;
}
/**
* Setter for the current group
*/
public function set_group($gid)
{
$this->gid = $gid;
}
/**
* Save a search string for future listings
*
* @param mixed Search params to use in listing method, obtained by get_search_set()
*/
public function set_search_set($filter)
{
$this->filter = $filter;
}
/**
* Getter for saved search properties
*
* @return mixed Search properties used by this class
*/
public function get_search_set()
{
return $this->filter;
}
/**
* Reset saved results and search parameters
*/
public function reset()
{
$this->result = null;
$this->filter = null;
}
/**
* List all active contact groups of this source
*
* @param string Optional search string to match group name
* @return array Indexed list of contact groups, each a hash array
*/
function list_groups($search = null)
{
$this->_fetch_groups();
$groups = array();
foreach ((array)$this->distlists as $group) {
if (!$search || strstr(strtolower($group['name']), strtolower($search)))
$groups[$group['name']] = array('ID' => $group['ID'], 'name' => $group['name']);
}
// sort groups
ksort($groups, SORT_LOCALE_STRING);
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 boolean 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;
// 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);
$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)), true);
}
}
else {
$this->_fetch_contacts($query = array(), true);
}
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 (isset($this->dataset)) {
$this->result->count = isset($query) ? $this->storagefolder->count($query) : 0;
foreach ($this->dataset as $idx => $record) {
$this->result->add($this->_to_rcube_contact($record));
}
}
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*)
* @param boolean $select True if results are requested, False if count only
* @param boolean $nocount True to skip the count query (select only)
* @param array $required List of fields that cannot be empty
*
* @return object 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 = array_keys($this->coltypes);
}
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');
}
// 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 $i => $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();
foreach (preg_grep($regexp, array_keys($contact)) as $col) {
$pos = strpos($col, ':');
$colname = $pos ? substr($col, 0, $pos) : $col;
$search = $advanced ? $value[array_search($colname, $fields)] : $value;
foreach ((array)$contact[$col] as $val) {
if ($this->compare_search_value($colname, $val, $search, $mode)) {
if (!$advanced) {
$this->filter['ids'][] = $id;
break 2;
}
else {
$found[$colname] = true;
}
}
}
}
if (count($found) >= $scount) // && $advanced
$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 boolean 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);
if (strpos($uid, 'mailto:') === 0) {
$this->_fetch_groups(true);
$rec = $this->contacts[$id];
$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();
foreach ((array)$this->groupmembers[$id] as $gid) {
if ($group = $this->distlists[$gid])
$out[$gid] = $group['name'];
}
return $out;
}
/**
* Create a new contact record
*
* @param array Assoziative 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 boolean 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 Assoziative 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 boolean 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, 'type' => 'php',
'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 boolean 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, 'type' => 'php',
'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, 'type' => 'php',
'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
*/
public function delete_all()
{
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, 'type' => 'php',
'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 boolean 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, 'type' => 'php',
'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
* @return boolean New name on success, false if no data was changed
*/
function rename_group($gid, $newname)
{
$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, 'type' => 'php',
'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, 'type' => 'php',
'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, 'type' => 'php',
'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
*
* @return boolean True if input is valid, False if not.
*/
public function validate(&$save_data)
{
// validate e-mail addresses
$valid = parent::validate($save_data);
// require at least one e-mail address (syntax check is already done)
if ($valid) {
if (!strlen($save_data['name'])
&& !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)
{
if (!isset($this->dataset) || !empty($query)) {
if ($limit) {
$this->storagefolder->set_order_and_limit($this->_sort_columns(), $this->page_size, ($this->list_page-1) * $this->page_size);
}
$this->sortindex = array();
$this->dataset = $this->storagefolder->select($query);
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->get_objects('distribution-list') 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)) {
switch ($mode) {
case 1: $prefix = '^'; $suffix = '$'; break; // strict
case 2: $prefix = '^'; $suffix = ''; break; // prefix
default: $prefix = ''; $suffix = ''; break; // substring
}
$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 && ($att = $record['_attachments'][$record['photo']])) {
// only fetch photo content if requested
if ($this->action == 'photo')
$record['photo'] = $att['content'] ? $att['content'] : $this->storagefolder->get_attachment($record['uid'], $att['id']);
}
// truncate publickey value for display
if ($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']);
}
}

File Metadata

Mime Type
text/x-diff
Expires
Sat, Apr 4, 1:07 AM (3 w, 2 d ago)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
18821253
Default Alt Text
(80 KB)

Event Timeline