summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorAleksander Machniak <alec@alec.pl>2013-06-19 19:52:33 +0200
committerAleksander Machniak <alec@alec.pl>2013-06-19 19:52:33 +0200
commit3bbe4e7424f12b4f870fb775b8230714a2862380 (patch)
tree925ee90ecc269ed5fad0426efed4e741ca3f3cd7
parentcd9e4154060ff38226e77eea82123519322b2327 (diff)
parentb200258d5af08ff24065d58c96d8ccf834d1ffad (diff)
Merge branch 'master' of github.com:roundcube/roundcubemail
-rw-r--r--CHANGELOG8
-rw-r--r--config/main.inc.php.dist57
-rw-r--r--installer/check.php6
-rw-r--r--installer/rcube_install.php7
-rw-r--r--plugins/acl/acl.php1
-rw-r--r--plugins/acl/config.inc.php.dist6
-rwxr-xr-xplugins/attachment_reminder/attachment_reminder.php8
-rw-r--r--program/include/rcmail.php2
-rw-r--r--program/js/app.js114
-rw-r--r--program/lib/Roundcube/rcube_config.php2
-rw-r--r--program/lib/Roundcube/rcube_ldap.php1117
-rw-r--r--program/lib/Roundcube/rcube_ldap_generic.php1049
-rw-r--r--program/lib/Roundcube/rcube_ldap_result.php130
-rw-r--r--program/lib/Roundcube/rcube_vcard.php10
-rw-r--r--program/localization/en_US/labels.inc1
-rw-r--r--program/steps/addressbook/func.inc66
-rw-r--r--program/steps/addressbook/list.inc5
-rw-r--r--program/steps/addressbook/save.inc4
-rw-r--r--program/steps/addressbook/show.inc5
-rw-r--r--program/steps/mail/list_contacts.inc29
-rw-r--r--skins/classic/addressbook.css31
-rw-r--r--skins/classic/functions.js1
-rw-r--r--skins/classic/images/contactgroup.pngbin0 -> 1038 bytes
-rw-r--r--skins/classic/mail.css11
-rw-r--r--skins/classic/templates/addressbook.html2
-rw-r--r--skins/classic/templates/contact.html2
-rw-r--r--skins/classic/templates/mail.html5
-rw-r--r--skins/larry/addressbook.css49
-rw-r--r--skins/larry/ie7hacks.css1
-rw-r--r--skins/larry/images/contactgroup.pngbin0 -> 2104 bytes
-rw-r--r--skins/larry/images/listicons.pngbin25486 -> 26384 bytes
-rw-r--r--skins/larry/mail.css13
-rw-r--r--skins/larry/styles.css4
-rw-r--r--skins/larry/templates/addressbook.html2
-rw-r--r--skins/larry/templates/contact.html2
-rw-r--r--skins/larry/ui.js33
36 files changed, 1938 insertions, 845 deletions
diff --git a/CHANGELOG b/CHANGELOG
index eaffa7099..3d0d60cc1 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,8 +1,16 @@
CHANGELOG Roundcube Webmail
===========================
+- Cache LDAP's user_specific search and use vlv for better performance (#1489186)
+- LDAP: auto-detect and use VLV indices for all search operations
+- LDAP: additional group configuration options for address books
+- LDAP: separated address book implementation from a generic LDAP wrapper class
+- Allow address books to browse a multi-level group hierarchy in the contacts list
+- Fix so install do not fail when one of DB driver checks fails but other drivers exist (#1489178)
+- Fix so exported vCard specifies encoding in v3-compatible format (#1489183)
- Fix session issues when local and database time differs (#1486132)
- Fix thread cache syncronization/validation (#1489028)
+- Added feature to import messages to the currently selected folder
- Add option show_real_foldernames to disable localization of special folders
- Fix database cache expunge issues (#1489149)
- Fix date format issues on MS SQL Server (#1488918)
diff --git a/config/main.inc.php.dist b/config/main.inc.php.dist
index 1c47c8170..1812105c1 100644
--- a/config/main.inc.php.dist
+++ b/config/main.inc.php.dist
@@ -592,7 +592,7 @@ $rcmail_config['ldap_public']['Verisign'] = array(
// For example %n = mail.domain.tld, %t = domain.tld
'hosts' => array('directory.verisign.com'),
'port' => 389,
- 'use_tls' => false,
+ 'use_tls' => false,
'ldap_version' => 3, // using LDAPv3
'network_timeout' => 10, // The timeout (in seconds) for connect + bind arrempts. This is only supported in PHP >= 5.3.0 with OpenLDAP 2.x
'user_specific' => false, // If true the base_dn, bind_dn and bind_pass default to the user's IMAP login.
@@ -653,6 +653,7 @@ $rcmail_config['ldap_public']['Verisign'] = array(
'phone:work' => 'telephoneNumber',
'phone:mobile' => 'mobile',
'phone:pager' => 'pager',
+ 'phone:workfax' => 'facsimileTelephoneNumber',
'street' => 'street',
'zipcode' => 'postalCode',
'region' => 'st',
@@ -663,9 +664,8 @@ $rcmail_config['ldap_public']['Verisign'] = array(
'department' => 'ou',
'jobtitle' => 'title',
'notes' => 'description',
+ 'photo' => 'jpegPhoto',
// these currently don't work:
- // 'phone:workfax' => 'facsimileTelephoneNumber',
- // 'photo' => 'jpegPhoto',
// 'manager' => 'manager',
// 'assistant' => 'secretary',
),
@@ -676,27 +676,48 @@ $rcmail_config['ldap_public']['Verisign'] = array(
// 'uid' => 'md5(microtime())', // You may specify PHP code snippets which are then eval'ed
// 'mail' => '{givenname}.{sn}@mydomain.com', // or composite strings with placeholders for existing attributes
),
- 'sort' => 'cn', // The field to sort the listing by.
- 'scope' => 'sub', // search mode: sub|base|list
- 'filter' => '(objectClass=inetOrgPerson)', // used for basic listing (if not empty) and will be &'d with search queries. example: status=act
- 'fuzzy_search' => true, // server allows wildcard search
- 'vlv' => false, // Enable Virtual List View to more efficiently fetch paginated data (if server supports it)
- 'numsub_filter' => '(objectClass=organizationalUnit)', // with VLV, we also use numSubOrdinates to query the total number of records. Set this filter to get all numSubOrdinates attributes for counting
- 'sizelimit' => '0', // Enables you to limit the count of entries fetched. Setting this to 0 means no limit.
- 'timelimit' => '0', // Sets the number of seconds how long is spend on the search. Setting this to 0 means no limit.
- 'referrals' => true|false, // Sets the LDAP_OPT_REFERRALS option. Mostly used in multi-domain Active Directory setups
+ 'sort' => 'cn', // The field to sort the listing by.
+ 'scope' => 'sub', // search mode: sub|base|list
+ 'filter' => '(objectClass=inetOrgPerson)', // used for basic listing (if not empty) and will be &'d with search queries. example: status=act
+ 'fuzzy_search' => true, // server allows wildcard search
+ 'vlv' => false, // Enable Virtual List View to more efficiently fetch paginated data (if server supports it)
+ 'vlv_search' => false, // Use Virtual List View functions for autocompletion searches (if server supports it)
+ 'numsub_filter' => '(objectClass=organizationalUnit)', // with VLV, we also use numSubOrdinates to query the total number of records. Set this filter to get all numSubOrdinates attributes for counting
+ 'config_root_dn' => 'cn=config', // Root DN to search config entries (e.g. vlv indexes)
+ 'sizelimit' => '0', // Enables you to limit the count of entries fetched. Setting this to 0 means no limit.
+ 'timelimit' => '0', // Sets the number of seconds how long is spend on the search. Setting this to 0 means no limit.
+ 'referrals' => false, // Sets the LDAP_OPT_REFERRALS option. Mostly used in multi-domain Active Directory setups
// definition for contact groups (uncomment if no groups are supported)
// for the groups base_dn, the user replacements %fu, %u, $d and %dc work as for base_dn (see above)
// if the groups base_dn is empty, the contact base_dn is used for the groups as well
// -> in this case, assure that groups and contacts are separated due to the concernig filters!
- 'groups' => array(
- 'base_dn' => '',
- 'scope' => 'sub', // search mode: sub|base|list
- 'filter' => '(objectClass=groupOfNames)',
+ 'groups' => array(
+ 'base_dn' => '',
+ 'scope' => 'sub', // Search mode: sub|base|list
+ 'filter' => '(objectClass=groupOfNames)',
'object_classes' => array("top", "groupOfNames"),
- 'member_attr' => 'member', // name of the member attribute, e.g. uniqueMember
- 'name_attr' => 'cn', // attribute to be used as group name
+ 'member_attr' => 'member', // Name of the member attribute, e.g. uniqueMember
+ 'name_attr' => 'cn', // Attribute to be used as group name
+ 'member_filter' => '(objectclass=*)', // Optional filter to use when querying for group members
+ 'vlv' => false, // Use VLV controls to list groups
+ ),
+ // this configuration replaces the regular groups listing in the directory tree with
+ // a hard-coded list of groups, each listing entries with the configured base DN and filter.
+ // if the 'groups' option from above is set, it'll be shown as the first entry with the name 'Groups'
+ 'group_filters' => array(
+ 'departments' => array(
+ 'name' => 'Company Departments',
+ 'scope' => 'list',
+ 'base_dn' => 'ou=Groups,dc=mydomain,dc=com',
+ 'filter' => '(|(objectclass=groupofuniquenames)(objectclass=groupofurls))',
+ ),
+ 'customers' => array(
+ 'name' => 'Customers',
+ 'scope' => 'sub',
+ 'base_dn' => 'ou=Customers,dc=mydomain,dc=com',
+ 'filter' => '(objectClass=inetOrgPerson)',
+ ),
),
);
*/
diff --git a/installer/check.php b/installer/check.php
index fcf35025e..bea8c42e0 100644
--- a/installer/check.php
+++ b/installer/check.php
@@ -139,10 +139,11 @@ foreach ($RCI->supported_dbs as $database => $ext) {
if (extension_loaded($ext)) {
// MySQL driver requires PHP >= 5.3 (#1488875)
if ($ext == 'pdo_mysql' && version_compare(PHP_VERSION, '5.3.0', '<')) {
- $RCI->fail($database, 'PHP >= 5.3 required');
+ $RCI->fail($database, 'PHP >= 5.3 required', null, true);
}
else {
$RCI->pass($database);
+ $found_db_driver = true;
}
}
else {
@@ -152,6 +153,9 @@ foreach ($RCI->supported_dbs as $database => $ext) {
}
echo '<br />';
}
+if (empty($found_db_driver)) {
+ $RCI->failures++;
+}
?>
diff --git a/installer/rcube_install.php b/installer/rcube_install.php
index c95d936d2..473fd2612 100644
--- a/installer/rcube_install.php
+++ b/installer/rcube_install.php
@@ -496,10 +496,13 @@ class rcube_install
* @param string Test name
* @param string Error message
* @param string URL for details
+ * @param bool Do not count this failure
*/
- function fail($name, $message = '', $url = '')
+ function fail($name, $message = '', $url = '', $optional=false)
{
- $this->failures++;
+ if (!$optional) {
+ $this->failures++;
+ }
echo Q($name) . ':&nbsp; <span class="fail">NOT OK</span>';
$this->_showhint($message, $url);
diff --git a/plugins/acl/acl.php b/plugins/acl/acl.php
index 28139e92c..59406262e 100644
--- a/plugins/acl/acl.php
+++ b/plugins/acl/acl.php
@@ -148,6 +148,7 @@ class acl extends rcube_plugin
// Load localization and include scripts
$this->load_config();
+ $this->specials = $this->rc->config->get('acl_specials', $this->specials);
$this->add_texts('localization/', array('deleteconfirm', 'norights',
'nouser', 'deleting', 'saving'));
$this->include_script('acl.js');
diff --git a/plugins/acl/config.inc.php.dist b/plugins/acl/config.inc.php.dist
index f957a233a..d0e1a0932 100644
--- a/plugins/acl/config.inc.php.dist
+++ b/plugins/acl/config.inc.php.dist
@@ -16,4 +16,10 @@ $rcmail_config['acl_users_field'] = 'mail';
// The LDAP search filter will be &'d with search queries
$rcmail_config['acl_users_filter'] = '';
+// Include the following 'special' access control subjects in the ACL dialog;
+// Defaults to array('anyone', 'anonymous') (not when set to an empty array)
+// Example: array('anyone') to exclude 'anonymous'.
+// Set to an empty array to exclude all special aci subjects.
+$rcmail_config['acl_specials'] = array('anyone', 'anonymous');
+
?>
diff --git a/plugins/attachment_reminder/attachment_reminder.php b/plugins/attachment_reminder/attachment_reminder.php
index a215ff57c..82ad7a60e 100755
--- a/plugins/attachment_reminder/attachment_reminder.php
+++ b/plugins/attachment_reminder/attachment_reminder.php
@@ -36,9 +36,11 @@ class attachment_reminder extends rcube_plugin
$rcmail = rcube::get_instance();
if ($rcmail->task == 'mail' && $rcmail->action == 'compose') {
- $this->include_script('attachment_reminder.js');
- $this->add_texts('localization/', array('keywords', 'forgotattachment'));
- $rcmail->output->add_label('addattachment', 'send');
+ if ($rcmail->config->get('attachment_reminder')) {
+ $this->include_script('attachment_reminder.js');
+ $this->add_texts('localization/', array('keywords', 'forgotattachment'));
+ $rcmail->output->add_label('addattachment', 'send');
+ }
}
if ($rcmail->task == 'settings') {
diff --git a/program/include/rcmail.php b/program/include/rcmail.php
index 39d804d1f..eff0425c8 100644
--- a/program/include/rcmail.php
+++ b/program/include/rcmail.php
@@ -294,7 +294,7 @@ class rcmail extends rcube
$list[$id] = array(
'id' => $id,
'name' => html::quote($prop['name']),
- 'groups' => is_array($prop['groups']),
+ 'groups' => !empty($prop['groups']) || !empty($prop['group_filters']),
'readonly' => !$prop['writable'],
'hidden' => $prop['hidden'],
'autocomplete' => in_array($id, $autocomplete)
diff --git a/program/js/app.js b/program/js/app.js
index cb08ce29d..41be99c0f 100644
--- a/program/js/app.js
+++ b/program/js/app.js
@@ -255,7 +255,8 @@ function rcube_webmail()
}
}
else if (this.env.action == 'compose') {
- this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel', 'toggle-editor', 'list-adresses', 'search', 'reset-search', 'extwin'];
+ this.env.address_group_stack = [];
+ this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel', 'toggle-editor', 'list-adresses', 'pushgroup', 'search', 'reset-search', 'extwin'];
if (this.env.drafts_mailbox)
this.env.compose_commands.push('savedraft')
@@ -323,11 +324,13 @@ function rcube_webmail()
break;
case 'addressbook':
+ this.env.address_group_stack = [];
+
if (this.gui_objects.folderlist)
this.env.contactfolders = $.extend($.extend({}, this.env.address_sources), this.env.contactgroups);
this.enable_command('add', 'import', this.env.writable_source);
- this.enable_command('list', 'listgroup', 'listsearch', 'advanced-search', true);
+ this.enable_command('list', 'listgroup', 'pushgroup', 'popgroup', 'listsearch', 'advanced-search', true);
if (this.gui_objects.contactslist) {
this.contact_list = new rcube_list_widget(this.gui_objects.contactslist,
@@ -1097,11 +1100,25 @@ function rcube_webmail()
}
break;
+ case 'pushgroup':
+ // add group ID to stack
+ this.env.address_group_stack.push(props.id);
+ if (obj && event)
+ rcube_event.cancel(event);
+
case 'listgroup':
this.reset_qsearch();
this.list_contacts(props.source, props.id);
break;
+ case 'popgroup':
+ if (this.env.address_group_stack.length > 1) {
+ this.env.address_group_stack.pop();
+ this.reset_qsearch();
+ this.list_contacts(props.source, this.env.address_group_stack[this.env.address_group_stack.length-1]);
+ }
+ break;
+
case 'import-messages':
var form = props || this.gui_objects.importform;
$('input[name="_unlock"]', form).val(this.set_busy(true, 'importwait'));
@@ -3091,7 +3108,13 @@ function rcube_webmail()
this.compose_recipient_select = function(list)
{
- this.enable_command('add-recipient', list.selection.length > 0);
+ var id, n, recipients = 0;
+ for (n=0; n < list.selection.length; n++) {
+ id = list.selection[n];
+ if (this.env.contactdata[id])
+ recipients++;
+ }
+ this.enable_command('add-recipient', recipients);
};
this.compose_add_recipient = function(field)
@@ -4109,7 +4132,7 @@ function rcube_webmail()
if (this.preview_timer)
clearTimeout(this.preview_timer);
- var n, id, sid, ref = this, writable = false,
+ var n, id, sid, contact, ref = this, writable = false,
source = this.env.source ? this.env.address_sources[this.env.source] : null;
// we don't have dblclick handler here, so use 200 instead of this.dblclick_time
@@ -4124,29 +4147,35 @@ function rcube_webmail()
// we'll also need to know sources used in selection for copy
// and group-addmember operations (drag&drop)
this.env.selection_sources = [];
- if (!source) {
- for (n in list.selection) {
+
+ if (source) {
+ this.env.selection_sources.push(this.env.source);
+ }
+
+ for (n in list.selection) {
+ contact = list.data[list.selection[n]];
+ if (!source) {
sid = String(list.selection[n]).replace(/^[^-]+-/, '');
if (sid && this.env.address_sources[sid]) {
- writable = writable || !this.env.address_sources[sid].readonly;
+ writable = writable || (!this.env.address_sources[sid].readonly && !contact.readonly);
this.env.selection_sources.push(sid);
}
}
- this.env.selection_sources = $.unique(this.env.selection_sources);
- }
- else {
- this.env.selection_sources.push(this.env.source);
- writable = !source.readonly;
+ else {
+ writable = writable || (!source.readonly && !contact.readonly);
+ }
}
+
+ this.env.selection_sources = $.unique(this.env.selection_sources);
}
// if a group is currently selected, and there is at least one contact selected
// thend we can enable the group-remove-selected command
- this.enable_command('group-remove-selected', this.env.group && list.selection.length > 0);
+ this.enable_command('group-remove-selected', this.env.group && list.selection.length > 0 && writable);
this.enable_command('compose', this.env.group || list.selection.length > 0);
this.enable_command('export-selected', list.selection.length > 0);
this.enable_command('edit', id && writable);
- this.enable_command('delete', list.selection.length && writable);
+ this.enable_command('delete', list.selection.length > 0 && writable);
return false;
};
@@ -4174,11 +4203,29 @@ function rcube_webmail()
else if (!this.env.search_request)
folder = group ? 'G'+src+group : src;
- this.select_folder(folder, '', true);
-
this.env.source = src;
this.env.group = group;
+ // truncate groups listing stack
+ var index = $.inArray(this.env.group, this.env.address_group_stack);
+ if (index < 0)
+ this.env.address_group_stack = [];
+ else
+ this.env.address_group_stack = this.env.address_group_stack.slice(0,index);
+
+ // make sure the current group is on top of the stack
+ if (this.env.group) {
+ this.env.address_group_stack.push(this.env.group);
+
+ // mark the first group on the stack as selected in the directory list
+ folder = 'G'+src+this.env.address_group_stack[0];
+ }
+ else if (this.gui_objects.addresslist_title) {
+ $(this.gui_objects.addresslist_title).html(this.get_label('contacts'));
+ }
+
+ this.select_folder(folder, '', true);
+
// load contacts remotely
if (this.gui_objects.contactslist) {
this.list_contacts_remote(src, group, page);
@@ -4233,16 +4280,38 @@ function rcube_webmail()
this.list_contacts_clear = function()
{
+ this.contact_list.data = {};
this.contact_list.clear(true);
this.show_contentframe(false);
this.enable_command('delete', false);
this.enable_command('compose', this.env.group ? true : false);
};
+ this.set_group_prop = function(prop)
+ {
+ if (this.gui_objects.addresslist_title) {
+ var boxtitle = $(this.gui_objects.addresslist_title).html(''); // clear contents
+
+ // add link to pop back to parent group
+ if (this.env.address_group_stack.length > 1) {
+ $('<a href="#list">...</a>')
+ .addClass('poplink')
+ .appendTo(boxtitle)
+ .click(function(e){ return ref.command('popgroup','',this); });
+ boxtitle.append('&nbsp;&raquo;&nbsp;');
+ }
+
+ boxtitle.append($('<span>'+prop.name+'</span>'));
+ }
+
+ this.triggerEvent('groupupdate', prop);
+ };
+
// load contact record
this.load_contact = function(cid, action, framed)
{
- var win, url = {}, target = window;
+ var win, url = {}, target = window,
+ rec = this.contact_list ? this.contact_list.data[cid] : null;
if (win = this.get_frame_window(this.env.contentframe)) {
url._framed = 1;
@@ -4252,7 +4321,9 @@ function rcube_webmail()
// load dummy content, unselect selected row(s)
if (!cid)
this.contact_list.clear_selection();
- this.enable_command('delete', 'compose', 'export-selected', cid);
+
+ this.enable_command('compose', rec && rec.email);
+ this.enable_command('export-selected', rec && rec._type != 'group');
}
else if (framed)
return false;
@@ -4362,7 +4433,7 @@ function rcube_webmail()
};
// update a contact record in the list
- this.update_contact_row = function(cid, cols_arr, newcid, source)
+ this.update_contact_row = function(cid, cols_arr, newcid, source, data)
{
var c, row, list = this.contact_list;
@@ -4376,10 +4447,11 @@ function rcube_webmail()
}
list.update_row(cid, cols_arr, newcid, true);
+ list.data[cid] = data;
};
// add row to contacts list
- this.add_contact_row = function(cid, cols, classes)
+ this.add_contact_row = function(cid, cols, classes, data)
{
if (!this.gui_objects.contactslist)
return false;
@@ -4401,6 +4473,8 @@ function rcube_webmail()
row.cols.push(col);
}
+ // store data in list member
+ list.data[cid] = data;
list.insert_row(row);
this.enable_command('export', list.rowcount > 0);
diff --git a/program/lib/Roundcube/rcube_config.php b/program/lib/Roundcube/rcube_config.php
index 18055f77d..913eacb05 100644
--- a/program/lib/Roundcube/rcube_config.php
+++ b/program/lib/Roundcube/rcube_config.php
@@ -194,7 +194,7 @@ class rcube_config
*/
public function get($name, $def = null)
{
- if (isset($this->prop[$name])) {
+ if (array_key_exists($name, $this->prop)) {
$result = $this->prop[$name];
}
else {
diff --git a/program/lib/Roundcube/rcube_ldap.php b/program/lib/Roundcube/rcube_ldap.php
index 39a48b456..54077c692 100644
--- a/program/lib/Roundcube/rcube_ldap.php
+++ b/program/lib/Roundcube/rcube_ldap.php
@@ -3,8 +3,8 @@
/*
+-----------------------------------------------------------------------+
| This file is part of the Roundcube Webmail client |
- | Copyright (C) 2006-2012, The Roundcube Dev Team |
- | Copyright (C) 2011-2012, Kolab Systems AG |
+ | Copyright (C) 2006-2013, The Roundcube Dev Team |
+ | Copyright (C) 2011-2013, Kolab Systems AG |
| |
| Licensed under the GNU General Public License version 3 or |
| any later version with exceptions for skins & plugins. |
@@ -36,7 +36,7 @@ class rcube_ldap extends rcube_addressbook
public $coltypes = array();
/** private properties */
- protected $conn;
+ protected $ldap;
protected $prop = array();
protected $fieldmap = array();
protected $sub_filter;
@@ -51,14 +51,11 @@ class rcube_ldap extends rcube_addressbook
private $group_url = null;
private $cache;
- private $vlv_active = false;
- private $vlv_count = 0;
-
/**
* Object constructor
*
- * @param array $p LDAP connection properties
+ * @param array $p LDAP connection properties
* @param boolean $debug Enables debug mode
* @param string $mail_domain Current user mail domain name
*/
@@ -66,6 +63,8 @@ class rcube_ldap extends rcube_addressbook
{
$this->prop = $p;
+ $fetch_attributes = array('objectClass');
+
if (isset($p['searchonly']))
$this->searchonly = $p['searchonly'];
@@ -82,6 +81,17 @@ class rcube_ldap extends rcube_addressbook
$this->prop['groups']['name_attr'] = 'cn';
if (empty($this->prop['groups']['scope']))
$this->prop['groups']['scope'] = 'sub';
+
+ // add group name attrib to the list of attributes to be fetched
+ $fetch_attributes[] = $this->prop['groups']['name_attr'];
+ }
+ if (is_array($p['group_filters']) && count($p['group_filters'])) {
+ $this->groups = true;
+
+ foreach ($p['group_filters'] as $group_filter) {
+ if ($group_filter['name_attr'])
+ $fetch_attributes[] = $group_filter['name_attr'];
+ }
}
// fieldmap property is given
@@ -185,12 +195,25 @@ class rcube_ldap extends rcube_addressbook
$this->mail_domain = $mail_domain;
// initialize cache
- $rcube = rcube::get_instance();
- $cache_type = $rcube->config->get('ldap_cache', 'db');
- $cache_ttl = $rcube->config->get('ldap_cache_ttl', '10m');
- $cache_name = 'LDAP.' . asciiwords($this->prop['name']);
+ $rcube = rcube::get_instance();
+ if ($cache_type = $rcube->config->get('ldap_cache', 'db')) {
+ $cache_ttl = $rcube->config->get('ldap_cache_ttl', '10m');
+ $cache_name = 'LDAP.' . asciiwords($this->prop['name']);
+
+ $this->cache = $rcube->get_cache($cache_name, $cache_type, $cache_ttl);
+ }
+
+ // determine which attributes to fetch
+ $this->prop['list_attributes'] = array_unique($fetch_attributes);
+ $this->prop['attributes'] = array_merge(array_values($this->fieldmap), $fetch_attributes);
+ foreach ($rcube->config->get('contactlist_fields') as $col) {
+ $this->prop['list_attributes'] = array_merge($this->prop['list_attributes'], $this->_map_field($col));
+ }
- $this->cache = $rcube->get_cache($cache_name, $cache_type, $cache_ttl);
+ // initialize ldap wrapper object
+ $this->ldap = new rcube_ldap_generic($this->prop);
+ $this->ldap->set_cache($this->cache);
+ $this->ldap->set_debug($this->debug);
$this->_connect();
}
@@ -203,49 +226,18 @@ class rcube_ldap extends rcube_addressbook
{
$rcube = rcube::get_instance();
- if (!function_exists('ldap_connect'))
- rcube::raise_error(array('code' => 100, 'type' => 'ldap',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => "No ldap support in this installation of PHP"),
- true, true);
-
- if (is_resource($this->conn))
+ if ($this->ready)
return true;
if (!is_array($this->prop['hosts']))
$this->prop['hosts'] = array($this->prop['hosts']);
- if (empty($this->prop['ldap_version']))
- $this->prop['ldap_version'] = 3;
-
// try to connect + bind for every host configured
// with OpenLDAP 2.x ldap_connect() always succeeds but ldap_bind will fail if host isn't reachable
// see http://www.php.net/manual/en/function.ldap-connect.php
foreach ($this->prop['hosts'] as $host) {
- $host = rcube_utils::idn_to_ascii(rcube_utils::parse_host($host));
- $hostname = $host.($this->prop['port'] ? ':'.$this->prop['port'] : '');
-
- $this->_debug("C: Connect [$hostname] [{$this->prop['name']}]");
-
- if ($lc = @ldap_connect($host, $this->prop['port'])) {
- if ($this->prop['use_tls'] === true)
- if (!ldap_start_tls($lc))
- continue;
-
- $this->_debug("S: OK");
-
- ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->prop['ldap_version']);
- $this->prop['host'] = $host;
- $this->conn = $lc;
-
- if (!empty($this->prop['network_timeout']))
- ldap_set_option($lc, LDAP_OPT_NETWORK_TIMEOUT, $this->prop['network_timeout']);
-
- if (isset($this->prop['referrals']))
- ldap_set_option($lc, LDAP_OPT_REFERRALS, $this->prop['referrals']);
- }
- else {
- $this->_debug("S: NOT OK");
+ // skip host if connection failed
+ if (!$this->ldap->connect($host)) {
continue;
}
@@ -260,7 +252,7 @@ class rcube_ldap extends rcube_addressbook
$this->base_dn = $this->prop['base_dn'];
$this->groups_base_dn = ($this->prop['groups']['base_dn']) ?
- $this->prop['groups']['base_dn'] : $this->base_dn;
+ $this->prop['groups']['base_dn'] : $this->base_dn;
// User specific access, generate the proper values to use.
if ($this->prop['user_specific']) {
@@ -279,29 +271,46 @@ class rcube_ldap extends rcube_addressbook
$replaces = array('%dn' => '', '%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u);
+ // Search for the dn to use to authenticate
if ($this->prop['search_base_dn'] && $this->prop['search_filter']) {
- if (!empty($this->prop['search_bind_dn']) && !empty($this->prop['search_bind_pw'])) {
- $this->bind($this->prop['search_bind_dn'], $this->prop['search_bind_pw']);
- }
-
- // Search for the dn to use to authenticate
- $this->prop['search_base_dn'] = strtr($this->prop['search_base_dn'], $replaces);
- $this->prop['search_filter'] = strtr($this->prop['search_filter'], $replaces);
+ $search_bind_dn = strtr($this->prop['search_bind_dn'], $replaces);
+ $search_base_dn = strtr($this->prop['search_base_dn'], $replaces);
+ $search_filter = strtr($this->prop['search_filter'], $replaces);
- $this->_debug("S: searching with base {$this->prop['search_base_dn']} for {$this->prop['search_filter']}");
+ $cache_key = 'DN.' . md5("$host:$search_bind_dn:$search_base_dn:$search_filter:"
+ .$this->prop['search_bind_pw']);
- $res = @ldap_search($this->conn, $this->prop['search_base_dn'], $this->prop['search_filter'], array('uid'));
- if ($res) {
- if (($entry = ldap_first_entry($this->conn, $res))
- && ($bind_dn = ldap_get_dn($this->conn, $entry))
- ) {
- $this->_debug("S: search returned dn: $bind_dn");
- $dn = ldap_explode_dn($bind_dn, 1);
- $replaces['%dn'] = $dn[0];
- }
+ if ($this->cache && ($dn = $this->cache->get($cache_key))) {
+ $replaces['%dn'] = $dn;
}
else {
- $this->_debug("S: ".ldap_error($this->conn));
+ $ldap = $this->ldap;
+ if (!empty($search_bind_dn) && !empty($this->prop['search_bind_pw'])) {
+ // To protect from "Critical extension is unavailable" error
+ // we need to use a separate LDAP connection
+ if (!empty($this->prop['vlv'])) {
+ $ldap = new rcube_ldap_generic($this->prop);
+ $ldap->set_debug($this->debug);
+ $ldap->set_cache($this->cache);
+ if (!$ldap->connect($host)) {
+ continue;
+ }
+ }
+
+ if (!$ldap->bind($search_bind_dn, $this->prop['search_bind_pw'])) {
+ continue; // bind failed, try next host
+ }
+ }
+
+ $res = $ldap->search($search_base_dn, $search_filter, 'sub', array('uid'));
+ if ($res) {
+ $res->rewind();
+ $replaces['%dn'] = $res->get_dn();
+ }
+
+ if ($ldap != $this->ldap) {
+ $ldap->close();
+ }
}
// DN not found
@@ -313,9 +322,13 @@ class rcube_ldap extends rcube_addressbook
'code' => 100, 'type' => 'ldap',
'file' => __FILE__, 'line' => __LINE__,
'message' => "DN not found using LDAP search."), true);
- return false;
+ continue;
}
}
+
+ if ($this->cache && !empty($replaces['%dn'])) {
+ $this->cache->set($cache_key, $replaces['%dn']);
+ }
}
// Replace the bind_dn and base_dn variables.
@@ -333,13 +346,13 @@ class rcube_ldap extends rcube_addressbook
}
else {
if (!empty($bind_dn)) {
- $this->ready = $this->bind($bind_dn, $bind_pass);
+ $this->ready = $this->ldap->bind($bind_dn, $bind_pass);
}
else if (!empty($this->prop['auth_cid'])) {
- $this->ready = $this->sasl_bind($this->prop['auth_cid'], $bind_pass, $bind_user);
+ $this->ready = $this->ldap->sasl_bind($this->prop['auth_cid'], $bind_pass, $bind_user);
}
else {
- $this->ready = $this->sasl_bind($bind_user, $bind_pass);
+ $this->ready = $this->ldap->sasl_bind($bind_user, $bind_pass);
}
}
@@ -350,10 +363,10 @@ class rcube_ldap extends rcube_addressbook
} // end foreach hosts
- if (!is_resource($this->conn)) {
+ if (!is_resource($this->ldap->conn)) {
rcube::raise_error(array('code' => 100, 'type' => 'ldap',
'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Could not connect to any LDAP server, last tried $hostname"), true);
+ 'message' => "Could not connect to any LDAP server, last tried $host"), true);
return false;
}
@@ -363,112 +376,47 @@ class rcube_ldap extends rcube_addressbook
/**
- * Bind connection with (SASL-) user and password
- *
- * @param string $authc Authentication user
- * @param string $pass Bind password
- * @param string $authz Autorization user
- *
- * @return boolean True on success, False on error
+ * Close connection to LDAP server
*/
- public function sasl_bind($authc, $pass, $authz=null)
+ function close()
{
- if (!$this->conn) {
- return false;
- }
-
- if (!function_exists('ldap_sasl_bind')) {
- rcube::raise_error(array('code' => 100, 'type' => 'ldap',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Unable to bind: ldap_sasl_bind() not exists"),
- true, true);
- }
-
- if (!empty($authz)) {
- $authz = 'u:' . $authz;
- }
-
- if (!empty($this->prop['auth_method'])) {
- $method = $this->prop['auth_method'];
- }
- else {
- $method = 'DIGEST-MD5';
- }
-
- $this->_debug("C: Bind [mech: $method, authc: $authc, authz: $authz] [pass: $pass]");
-
- if (ldap_sasl_bind($this->conn, NULL, $pass, $method, NULL, $authc, $authz)) {
- $this->_debug("S: OK");
- return true;
+ if ($this->ldap) {
+ $this->ldap->close();
}
-
- $this->_debug("S: ".ldap_error($this->conn));
-
- rcube::raise_error(array(
- 'code' => ldap_errno($this->conn), 'type' => 'ldap',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Bind failed for authcid=$authc ".ldap_error($this->conn)),
- true);
-
- return false;
}
/**
- * Bind connection with DN and password
- *
- * @param string Bind DN
- * @param string Bind password
+ * Returns address book name
*
- * @return boolean True on success, False on error
+ * @return string Address book name
*/
- public function bind($dn, $pass)
+ function get_name()
{
- if (!$this->conn) {
- return false;
- }
-
- $this->_debug("C: Bind [dn: $dn] [pass: $pass]");
-
- if (@ldap_bind($this->conn, $dn, $pass)) {
- $this->_debug("S: OK");
- return true;
- }
-
- $this->_debug("S: ".ldap_error($this->conn));
-
- rcube::raise_error(array(
- 'code' => ldap_errno($this->conn), 'type' => 'ldap',
- 'file' => __FILE__, 'line' => __LINE__,
- 'message' => "Bind failed for dn=$dn: ".ldap_error($this->conn)),
- true);
-
- return false;
+ return $this->prop['name'];
}
/**
- * Close connection to LDAP server
+ * Set internal list page
+ *
+ * @param number Page number to list
*/
- function close()
+ function set_page($page)
{
- if ($this->conn)
- {
- $this->_debug("C: Close");
- ldap_unbind($this->conn);
- $this->conn = null;
- }
+ $this->list_page = (int)$page;
+ $this->ldap->set_vlv_page($this->list_page, $this->page_size);
}
-
/**
- * Returns address book name
+ * Set internal page size
*
- * @return string Address book name
+ * @param number Number of records to display on one page
*/
- function get_name()
+ function set_pagesize($size)
{
- return $this->prop['name'];
+ $this->page_size = (int)$size;
+ $this->ldap->set_vlv_page($this->list_page, $this->page_size);
}
@@ -528,16 +476,14 @@ class rcube_ldap extends rcube_addressbook
*/
function list_records($cols=null, $subset=0)
{
- if ($this->prop['searchonly'] && empty($this->filter) && !$this->group_id)
- {
+ if ($this->prop['searchonly'] && empty($this->filter) && !$this->group_id) {
$this->result = new rcube_result_set(0);
$this->result->searchonly = true;
return $this->result;
}
// fetch group members recursively
- if ($this->group_id && $this->group_data['dn'])
- {
+ if ($this->group_id && $this->group_data['dn']) {
$entries = $this->list_group_members($this->group_data['dn']);
// make list of entries unique and sort it
@@ -551,34 +497,34 @@ class rcube_ldap extends rcube_addressbook
$entries['count'] = count($entries);
$this->result = new rcube_result_set($entries['count'], ($this->list_page-1) * $this->page_size);
}
- else
- {
- // add general filter to query
- if (!empty($this->prop['filter']) && empty($this->filter))
- $this->set_search_set($this->prop['filter']);
+ else {
+ $prop = $this->group_id ? $this->group_data : $this->prop;
+
+ // use global search filter
+ if (!empty($this->filter))
+ $prop['filter'] = $this->filter;
// exec LDAP search if no result resource is stored
- if ($this->conn && !$this->ldap_result)
- $this->_exec_search();
+ if ($this->ready && !$this->ldap_result)
+ $this->ldap_result = $this->ldap->search($prop['base_dn'], $prop['filter'], $prop['scope'], $this->prop['attributes'], $prop);
// count contacts for this user
$this->result = $this->count();
// we have a search result resource
- if ($this->ldap_result && $this->result->count > 0)
- {
+ if ($this->ldap_result && $this->result->count > 0) {
// sorting still on the ldap server
- if ($this->sort_col && $this->prop['scope'] !== 'base' && !$this->vlv_active)
- ldap_sort($this->conn, $this->ldap_result, $this->sort_col);
+ if ($this->sort_col && $prop['scope'] !== 'base' && !$this->ldap->vlv_active)
+ $this->ldap_result->sort($this->sort_col);
// get all entries from the ldap server
- $entries = ldap_get_entries($this->conn, $this->ldap_result);
+ $entries = $this->ldap_result->entries();
}
} // end else
// start and end of the page
- $start_row = $this->vlv_active ? 0 : $this->result->first;
+ $start_row = $this->ldap->vlv_active ? 0 : $this->result->first;
$start_row = $subset < 0 ? $start_row + $this->page_size + $subset : $start_row;
$last_row = $this->result->first + $this->page_size;
$last_row = $subset != 0 ? $start_row + abs($subset) : $last_row;
@@ -603,25 +549,19 @@ class rcube_ldap extends rcube_addressbook
// fetch group object
if (empty($entries)) {
- $result = @ldap_read($this->conn, $dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL'));
- if ($result === false)
- {
- $this->_debug("S: ".ldap_error($this->conn));
+ $entries = $this->ldap->read_entries($dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL'));
+ if ($entries === false) {
return $group_members;
}
-
- $entries = @ldap_get_entries($this->conn, $result);
}
- for ($i=0; $i < $entries['count']; $i++)
- {
+ for ($i=0; $i < $entries['count']; $i++) {
$entry = $entries[$i];
if (empty($entry['objectclass']))
continue;
- foreach ((array)$entry['objectclass'] as $objectclass)
- {
+ foreach ((array)$entry['objectclass'] as $objectclass) {
switch (strtolower($objectclass)) {
case "group":
case "groupofnames":
@@ -662,24 +602,19 @@ class rcube_ldap extends rcube_addressbook
return $group_members;
// read these attributes for all members
- $attrib = $count ? array('dn') : array_values($this->fieldmap);
- $attrib[] = 'objectClass';
+ $attrib = $count ? array('dn','objectClass') : $this->prop['list_attributes'];
$attrib[] = 'member';
$attrib[] = 'uniqueMember';
$attrib[] = 'memberURL';
- for ($i=0; $i < $entry[$attr]['count']; $i++)
- {
+ $filter = $this->prop['groups']['member_filter'] ? $this->prop['groups']['member_filter'] : '(objectclass=*)';
+
+ for ($i=0; $i < $entry[$attr]['count']; $i++) {
if (empty($entry[$attr][$i]))
continue;
- $result = @ldap_read($this->conn, $entry[$attr][$i], '(objectclass=*)',
- $attrib, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']);
-
- $members = @ldap_get_entries($this->conn, $result);
- if ($members == false)
- {
- $this->_debug("S: ".ldap_error($this->conn));
+ $members = $this->ldap->read_entries($entry[$attr][$i], $filter, $attrib);
+ if ($members == false) {
$members = array();
}
@@ -705,34 +640,22 @@ class rcube_ldap extends rcube_addressbook
{
$group_members = array();
- for ($i=0; $i < $entry['memberurl']['count']; $i++)
- {
+ for ($i=0; $i < $entry['memberurl']['count']; $i++) {
// extract components from url
if (!preg_match('!ldap:///([^\?]+)\?\?(\w+)\?(.*)$!', $entry['memberurl'][$i], $m))
continue;
// add search filter if any
$filter = $this->filter ? '(&(' . $m[3] . ')(' . $this->filter . '))' : $m[3];
- $func = $m[2] == 'sub' ? 'ldap_search' : ($m[2] == 'base' ? 'ldap_read' : 'ldap_list');
-
- $attrib = $count ? array('dn') : array_values($this->fieldmap);
- if ($result = @$func($this->conn, $m[1], $filter,
- $attrib, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit'])
- ) {
- $this->_debug("S: ".ldap_count_entries($this->conn, $result)." record(s) for ".$m[1]);
- }
- else {
- $this->_debug("S: ".ldap_error($this->conn));
- return $group_members;
- }
-
- $entries = @ldap_get_entries($this->conn, $result);
- for ($j = 0; $j < $entries['count']; $j++)
- {
- if ($nested_group_members = $this->list_group_members($entries[$j]['dn'], $count))
- $group_members = array_merge($group_members, $nested_group_members);
- else
- $group_members[] = $entries[$j];
+ $attrs = $count ? array('dn','objectClass') : $this->prop['list_attributes'];
+ if ($result = $this->ldap->search($m[1], $filter, $m[2], $attrs, $this->group_data)) {
+ $entries = $result->entries();
+ for ($j = 0; $j < $entries['count']; $j++) {
+ if (self::is_group_entry($entries[$j]) && ($nested_group_members = $this->list_group_members($entries[$j]['dn'], $count)))
+ $group_members = array_merge($group_members, $nested_group_members);
+ else
+ $group_members[] = $entries[$j];
+ }
}
}
@@ -768,14 +691,11 @@ class rcube_ldap extends rcube_addressbook
$mode = intval($mode);
// special treatment for ID-based search
- if ($fields == 'ID' || $fields == $this->primary_key)
- {
+ if ($fields == 'ID' || $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))
- {
+ foreach ($ids as $id) {
+ if ($rec = $this->get_record($id, true)) {
$result->add($rec);
$result->count++;
}
@@ -787,34 +707,20 @@ class rcube_ldap extends rcube_addressbook
$rcube = rcube::get_instance();
$list_fields = $rcube->config->get('contactlist_fields');
- if ($this->prop['vlv_search'] && $this->conn && join(',', (array)$fields) == join(',', $list_fields))
- {
- // add general filter to query
- if (!empty($this->prop['filter']) && empty($this->filter))
- $this->set_search_set($this->prop['filter']);
-
- // set VLV controls with encoded search string
- $this->_vlv_set_controls($this->prop, $this->list_page, $this->page_size, $value);
-
- $function = $this->_scope2func($this->prop['scope']);
- $this->ldap_result = @$function($this->conn, $this->base_dn, $this->filter ? $this->filter : '(objectclass=*)',
- array_values($this->fieldmap), 0, $this->page_size, (int)$this->prop['timelimit']);
-
+ if ($this->prop['vlv_search'] && $this->ready && join(',', (array)$fields) == join(',', $list_fields)) {
$this->result = new rcube_result_set(0);
- if (!$this->ldap_result) {
- $this->_debug("S: ".ldap_error($this->conn));
+ $search_suffix = $this->prop['fuzzy_search'] && $mode != 1 ? '*' : '';
+ $ldap_data = $this->ldap->search($this->base_dn, $this->prop['filter'], $this->prop['scope'], $this->prop['attributes'],
+ array('search' => $value . $search_suffix /*, 'sort' => $this->prop['sort'] */));
+ if ($ldap_data === false) {
return $this->result;
}
- $this->_debug("S: ".ldap_count_entries($this->conn, $this->ldap_result)." record(s)");
-
// get all entries of this page and post-filter those that really match the query
- $search = mb_strtolower($value);
- $entries = ldap_get_entries($this->conn, $this->ldap_result);
-
- for ($i = 0; $i < $entries['count']; $i++) {
- $rec = $this->_ldap2result($entries[$i]);
+ $search = mb_strtolower($value);
+ foreach ($ldap_data as $i => $entry) {
+ $rec = $this->_ldap2result($entry);
foreach ($fields as $f) {
foreach ((array)$rec[$f] as $val) {
if ($this->compare_search_value($f, $val, $search, $mode)) {
@@ -840,31 +746,27 @@ class rcube_ldap extends rcube_addressbook
}
}
- if ($fields == '*')
- {
+ if ($fields == '*') {
// search_fields are required for fulltext search
- if (empty($this->prop['search_fields']))
- {
+ if (empty($this->prop['search_fields'])) {
$this->set_error(self::ERROR_SEARCH, 'nofulltextsearch');
$this->result = new rcube_result_set();
return $this->result;
}
- if (is_array($this->prop['search_fields']))
- {
+ if (is_array($this->prop['search_fields'])) {
foreach ($this->prop['search_fields'] as $field) {
- $filter .= "($field=$wp" . $this->_quote_string($value) . "$ws)";
+ $filter .= "($field=$wp" . rcube_ldap_generic::quote_string($value) . "$ws)";
}
}
}
- else
- {
+ else {
foreach ((array)$fields as $idx => $field) {
$val = is_array($value) ? $value[$idx] : $value;
if ($attrs = $this->_map_field($field)) {
if (count($attrs) > 1)
$filter .= '(|';
foreach ($attrs as $f)
- $filter .= "($f=$wp" . $this->_quote_string($val) . "$ws)";
+ $filter .= "($f=$wp" . rcube_ldap_generic::quote_string($val) . "$ws)";
if (count($attrs) > 1)
$filter .= ')';
}
@@ -875,6 +777,8 @@ class rcube_ldap extends rcube_addressbook
// add required (non empty) fields filter
$req_filter = '';
foreach ((array)$required as $field) {
+ if (in_array($field, (array)$fields)) // required field is already in search filter
+ continue;
if ($attrs = $this->_map_field($field)) {
if (count($attrs) > 1)
$req_filter .= '(|';
@@ -897,7 +801,6 @@ class rcube_ldap extends rcube_addressbook
// set filter string and execute search
$this->set_search_set($filter);
- $this->_exec_search();
if ($select)
$this->list_records();
@@ -916,20 +819,21 @@ class rcube_ldap extends rcube_addressbook
function count()
{
$count = 0;
- if ($this->conn && $this->ldap_result) {
- $count = $this->vlv_active ? $this->vlv_count : ldap_count_entries($this->conn, $this->ldap_result);
+ if ($this->ldap_result) {
+ $count = $this->ldap_result->count();
}
else if ($this->group_id && $this->group_data['dn']) {
$count = count($this->list_group_members($this->group_data['dn'], true));
}
- else if ($this->conn) {
- // We have a connection but no result set, attempt to get one.
- if (empty($this->filter)) {
- // The filter is not set, set it.
- $this->filter = $this->prop['filter'];
+ // We have a connection but no result set, attempt to get one.
+ else if ($this->ready) {
+ $prop = $this->group_id ? $this->group_data : $this->prop;
+
+ if (!empty($this->filter)) { // Use global search filter
+ $prop['filter'] = $this->filter;
}
- $count = (int) $this->_exec_search(true);
+ $count = $this->ldap->search($prop['base_dn'], $prop['filter'], $prop['scope'], array('dn'), $prop, true);
}
return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
@@ -959,28 +863,16 @@ class rcube_ldap extends rcube_addressbook
{
$res = $this->result = null;
- if ($this->conn && $dn)
- {
+ if ($this->ready && $dn) {
$dn = self::dn_decode($dn);
- $this->_debug("C: Read [dn: $dn] [(objectclass=*)]");
-
- if ($ldap_result = @ldap_read($this->conn, $dn, '(objectclass=*)', array_values($this->fieldmap))) {
- $this->_debug("S: OK");
-
- $entry = ldap_first_entry($this->conn, $ldap_result);
-
- if ($entry && ($rec = ldap_get_attributes($this->conn, $entry))) {
- $rec = array_change_key_case($rec, CASE_LOWER);
- }
- }
- else {
- $this->_debug("S: ".ldap_error($this->conn));
+ if ($rec = $this->ldap->get_entry($dn)) {
+ $rec = array_change_key_case($rec, CASE_LOWER);
}
// Use ldap_list to get subentries like country (c) attribute (#1488123)
if (!empty($rec) && $this->sub_filter) {
- if ($entries = $this->ldap_list($dn, $this->sub_filter, array_keys($this->prop['sub_fields']))) {
+ if ($entries = $this->ldap->list_entries($dn, $this->sub_filter, array_keys($this->prop['sub_fields']))) {
foreach ($entries as $entry) {
$lrec = array_change_key_case($entry, CASE_LOWER);
$rec = array_merge($lrec, $rec);
@@ -992,7 +884,7 @@ class rcube_ldap extends rcube_addressbook
// Add in the dn for the entry.
$rec['dn'] = $dn;
$res = $this->_ldap2result($rec);
- $this->result = new rcube_result_set();
+ $this->result = new rcube_result_set(1);
$this->result->add($res);
}
}
@@ -1105,7 +997,7 @@ class rcube_ldap extends rcube_addressbook
}
// Build the new entries DN.
- $dn = $this->prop['LDAP_rdn'].'='.$this->_quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->base_dn;
+ $dn = $this->prop['LDAP_rdn'].'='.rcube_ldap_generic::quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->base_dn;
// Remove attributes that need to be added separately (child objects)
$xfields = array();
@@ -1118,19 +1010,19 @@ class rcube_ldap extends rcube_addressbook
}
}
- if (!$this->ldap_add($dn, $newentry)) {
+ if (!$this->ldap->add($dn, $newentry)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
foreach ($xfields as $xidx => $xf) {
- $xdn = $xidx.'='.$this->_quote_string($xf).','.$dn;
+ $xdn = $xidx.'='.rcube_ldap_generic::quote_string($xf).','.$dn;
$xf = array(
$xidx => $xf,
'objectClass' => (array) $this->prop['sub_fields'][$xidx],
);
- $this->ldap_add($xdn, $xf);
+ $this->ldap->add($xdn, $xf);
}
$dn = self::dn_encode($dn);
@@ -1236,7 +1128,7 @@ class rcube_ldap extends rcube_addressbook
// Update the entry as required.
if (!empty($deletedata)) {
// Delete the fields.
- if (!$this->ldap_mod_del($dn, $deletedata)) {
+ if (!$this->ldap->mod_del($dn, $deletedata)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
@@ -1246,17 +1138,17 @@ class rcube_ldap extends rcube_addressbook
// Handle RDN change
if ($replacedata[$this->prop['LDAP_rdn']]) {
$newdn = $this->prop['LDAP_rdn'].'='
- .$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true)
+ .rcube_ldap_generic::quote_string($replacedata[$this->prop['LDAP_rdn']], true)
.','.$this->base_dn;
if ($dn != $newdn) {
$newrdn = $this->prop['LDAP_rdn'].'='
- .$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true);
+ .rcube_ldap_generic::quote_string($replacedata[$this->prop['LDAP_rdn']], true);
unset($replacedata[$this->prop['LDAP_rdn']]);
}
}
// Replace the fields.
if (!empty($replacedata)) {
- if (!$this->ldap_mod_replace($dn, $replacedata)) {
+ if (!$this->ldap->mod_replace($dn, $replacedata)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
@@ -1272,8 +1164,8 @@ class rcube_ldap extends rcube_addressbook
// remove sub-entries
if (!empty($subdeldata)) {
foreach ($subdeldata as $fld => $val) {
- $subdn = $fld.'='.$this->_quote_string($val).','.$dn;
- if (!$this->ldap_delete($subdn)) {
+ $subdn = $fld.'='.rcube_ldap_generic::quote_string($val).','.$dn;
+ if (!$this->ldap->delete($subdn)) {
return false;
}
}
@@ -1281,7 +1173,7 @@ class rcube_ldap extends rcube_addressbook
if (!empty($newdata)) {
// Add the fields.
- if (!$this->ldap_mod_add($dn, $newdata)) {
+ if (!$this->ldap->mod_add($dn, $newdata)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
@@ -1289,7 +1181,7 @@ class rcube_ldap extends rcube_addressbook
// Handle RDN change
if (!empty($newrdn)) {
- if (!$this->ldap_rename($dn, $newrdn, null, true)) {
+ if (!$this->ldap->rename($dn, $newrdn, null, true)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
@@ -1300,7 +1192,7 @@ class rcube_ldap extends rcube_addressbook
// change the group membership of the contact
if ($this->groups) {
$group_ids = $this->get_record_groups($dn);
- foreach ($group_ids as $group_id)
+ foreach ($group_ids as $group_id => $group_prop)
{
$this->remove_from_group($group_id, $dn);
$this->add_to_group($group_id, $newdn);
@@ -1313,12 +1205,12 @@ class rcube_ldap extends rcube_addressbook
// add sub-entries
if (!empty($subnewdata)) {
foreach ($subnewdata as $fld => $val) {
- $subdn = $fld.'='.$this->_quote_string($val).','.$dn;
+ $subdn = $fld.'='.rcube_ldap_generic::quote_string($val).','.$dn;
$xf = array(
$fld => $val,
'objectClass' => (array) $this->prop['sub_fields'][$fld],
);
- $this->ldap_add($subdn, $xf);
+ $this->ldap->add($subdn, $xf);
}
}
@@ -1346,9 +1238,9 @@ class rcube_ldap extends rcube_addressbook
// Need to delete all sub-entries first
if ($this->sub_filter) {
- if ($entries = $this->ldap_list($dn, $this->sub_filter)) {
+ if ($entries = $this->ldap->list_entries($dn, $this->sub_filter)) {
foreach ($entries as $entry) {
- if (!$this->ldap_delete($entry['dn'])) {
+ if (!$this->ldap->delete($entry['dn'])) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
@@ -1357,7 +1249,7 @@ class rcube_ldap extends rcube_addressbook
}
// Delete the record.
- if (!$this->ldap_delete($dn)) {
+ if (!$this->ldap->delete($dn)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
@@ -1366,7 +1258,7 @@ class rcube_ldap extends rcube_addressbook
if ($this->groups) {
$dn = self::dn_encode($dn);
$group_ids = $this->get_record_groups($dn);
- foreach ($group_ids as $group_id) {
+ foreach ($group_ids as $group_id => $group_prop) {
$this->remove_from_group($group_id, $dn);
}
}
@@ -1381,8 +1273,8 @@ class rcube_ldap extends rcube_addressbook
*/
function delete_all()
{
- //searching for contact entries
- $dn_list = $this->ldap_list($this->base_dn, $this->prop['filter'] ? $this->prop['filter'] : '(objectclass=*)');
+ // searching for contact entries
+ $dn_list = $this->ldap->list_entries($this->base_dn, $this->prop['filter'] ? $this->prop['filter'] : '(objectclass=*)');
if (!empty($dn_list)) {
foreach ($dn_list as $idx => $entry) {
@@ -1432,120 +1324,26 @@ class rcube_ldap extends rcube_addressbook
}
}
- /**
- * Execute the LDAP search based on the stored credentials
- */
- private function _exec_search($count = false)
- {
- if ($this->ready)
- {
- $filter = $this->filter ? $this->filter : '(objectclass=*)';
- $function = $this->_scope2func($this->prop['scope'], $ns_function);
-
- $this->_debug("C: Search [$filter][dn: $this->base_dn]");
-
- // when using VLV, we get the total count by...
- if (!$count && $function != 'ldap_read' && $this->prop['vlv'] && !$this->group_id) {
- // ...either reading numSubOrdinates attribute
- if ($this->prop['numsub_filter'] && ($result_count = @$ns_function($this->conn, $this->base_dn, $this->prop['numsub_filter'], array('numSubOrdinates'), 0, 0, 0))) {
- $counts = ldap_get_entries($this->conn, $result_count);
- for ($this->vlv_count = $j = 0; $j < $counts['count']; $j++)
- $this->vlv_count += $counts[$j]['numsubordinates'][0];
- $this->_debug("D: total numsubordinates = " . $this->vlv_count);
- }
- else if (!function_exists('ldap_parse_virtuallist_control')) // ...or by fetching all records dn and count them
- $this->vlv_count = $this->_exec_search(true);
-
- $this->vlv_active = $this->_vlv_set_controls($this->prop, $this->list_page, $this->page_size);
- }
-
- // only fetch dn for count (should keep the payload low)
- $attrs = $count ? array('dn') : array_values($this->fieldmap);
- if ($this->ldap_result = @$function($this->conn, $this->base_dn, $filter,
- $attrs, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit'])
- ) {
- // when running on a patched PHP we can use the extended functions to retrieve the total count from the LDAP search result
- if ($this->vlv_active && function_exists('ldap_parse_virtuallist_control')) {
- if (ldap_parse_result($this->conn, $this->ldap_result,
- $errcode, $matcheddn, $errmsg, $referrals, $serverctrls)
- && $serverctrls // can be null e.g. in case of adm. limit error
- ) {
- ldap_parse_virtuallist_control($this->conn, $serverctrls,
- $last_offset, $this->vlv_count, $vresult);
- $this->_debug("S: VLV result: last_offset=$last_offset; content_count=$this->vlv_count");
- }
- else {
- $this->_debug("S: ".($errmsg ? $errmsg : ldap_error($this->conn)));
- }
- }
-
- $entries_count = ldap_count_entries($this->conn, $this->ldap_result);
- $this->_debug("S: $entries_count record(s)");
-
- return $count ? $entries_count : true;
- }
- else {
- $this->_debug("S: ".ldap_error($this->conn));
- }
- }
-
- return false;
- }
-
- /**
- * Choose the right PHP function according to scope property
- */
- private function _scope2func($scope, &$ns_function = null)
- {
- switch ($scope) {
- case 'sub':
- $function = $ns_function = 'ldap_search';
- break;
- case 'base':
- $function = $ns_function = 'ldap_read';
- break;
- default:
- $function = 'ldap_list';
- $ns_function = 'ldap_read';
- break;
- }
-
- return $function;
- }
-
- /**
- * Set server controls for Virtual List View (paginated listing)
- */
- private function _vlv_set_controls($prop, $list_page, $page_size, $search = null)
- {
- $sort_ctrl = array('oid' => "1.2.840.113556.1.4.473", 'value' => $this->_sort_ber_encode((array)$prop['sort']));
- $vlv_ctrl = array('oid' => "2.16.840.1.113730.3.4.9", 'value' => $this->_vlv_ber_encode(($offset = ($list_page-1) * $page_size + 1), $page_size, $search), 'iscritical' => true);
-
- $sort = (array)$prop['sort'];
- $this->_debug("C: set controls sort=" . join(' ', unpack('H'.(strlen($sort_ctrl['value'])*2), $sort_ctrl['value'])) . " ($sort[0]);"
- . " vlv=" . join(' ', (unpack('H'.(strlen($vlv_ctrl['value'])*2), $vlv_ctrl['value']))) . " ($offset/$page_size)");
-
- if (!ldap_set_option($this->conn, LDAP_OPT_SERVER_CONTROLS, array($sort_ctrl, $vlv_ctrl))) {
- $this->_debug("S: ".ldap_error($this->conn));
- $this->set_error(self::ERROR_SEARCH, 'vlvnotsupported');
- return false;
- }
-
- return true;
- }
-
/**
* Converts LDAP entry into an array
*/
private function _ldap2result($rec)
{
- $out = array();
+ $out = array('_type' => 'person');
+ $fieldmap = $this->fieldmap;
if ($rec['dn'])
$out[$this->primary_key] = self::dn_encode($rec['dn']);
- foreach ($this->fieldmap as $rf => $lf)
+ // determine record type
+ if (self::is_group_entry($rec)) {
+ $out['_type'] = 'group';
+ $out['readonly'] = true;
+ $fieldmap['name'] = $this->group_data['name_attr'] ? $this->group_data['name_attr'] : $this->prop['groups']['name_attr'];
+ }
+
+ foreach ($fieldmap as $rf => $lf)
{
for ($i=0; $i < $rec[$lf]['count']; $i++) {
if (!($value = $rec[$lf][$i]))
@@ -1607,8 +1405,10 @@ class rcube_ldap extends rcube_addressbook
if (is_array($colprop['serialized'])) {
foreach ($colprop['serialized'] as $subtype => $delim) {
$key = $col.':'.$subtype;
- foreach ((array)$save_cols[$key] as $i => $val)
- $save_cols[$key][$i] = join($delim, array($val['street'], $val['locality'], $val['zipcode'], $val['country']));
+ foreach ((array)$save_cols[$key] as $i => $val) {
+ $values = array($val['street'], $val['locality'], $val['zipcode'], $val['country']);
+ $save_cols[$key][$i] = count(array_filter($values)) ? join($delim, $values) : null;
+ }
}
}
}
@@ -1659,6 +1459,16 @@ class rcube_ldap extends rcube_addressbook
return (isset($aliases[$name]) ? $aliases[$name] : $name) . $suffix;
}
+ /**
+ * Determines whether the given LDAP entry is a group record
+ */
+ private static function is_group_entry($entry)
+ {
+ return array_intersect(
+ array('group', 'groupofnames', 'kolabgroupofnames', 'groupofuniquenames','kolabgroupofuniquenames','groupofurls'),
+ array_map('strtolower', (array)$entry['objectclass'])
+ );
+ }
/**
* Prints debug info to the log
@@ -1675,55 +1485,27 @@ class rcube_ldap extends rcube_addressbook
* Activate/deactivate debug mode
*
* @param boolean $dbg True if LDAP commands should be logged
- * @access public
*/
function set_debug($dbg = true)
{
$this->debug = $dbg;
- }
-
- /**
- * Quotes attribute value string
- *
- * @param string $str Attribute value
- * @param bool $dn True if the attribute is a DN
- *
- * @return string Quoted string
- */
- private static function _quote_string($str, $dn=false)
- {
- // take firt entry if array given
- if (is_array($str))
- $str = reset($str);
-
- if ($dn)
- $replace = array(','=>'\2c', '='=>'\3d', '+'=>'\2b', '<'=>'\3c',
- '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c', '"'=>'\22', '#'=>'\23');
- else
- $replace = array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c',
- '/'=>'\2f');
-
- return strtr($str, $replace);
+ if ($this->ldap) {
+ $this->ldap->set_debug($dbg);
+ }
}
/**
* Setter for the current group
- * (empty, has to be re-implemented by extending class)
*/
function set_group($group_id)
{
- if ($group_id)
- {
- if (($group_cache = $this->cache->get('groups')) === null)
- $group_cache = $this->_fetch_groups();
-
+ if ($group_id) {
$this->group_id = $group_id;
- $this->group_data = $group_cache[$group_id];
+ $this->group_data = $this->get_group_entry($group_id);
}
- else
- {
+ else {
$this->group_id = 0;
$this->group_data = null;
}
@@ -1746,9 +1528,9 @@ class rcube_ldap extends rcube_addressbook
return array();
// use cached list for searching
- $this->cache->expunge();
- if (!$search || ($group_cache = $this->cache->get('groups')) === null)
+ if (!$this->cache || ($group_cache = $this->cache->get('groups')) === null) {
$group_cache = $this->_fetch_groups();
+ }
$groups = array();
if ($search) {
@@ -1769,6 +1551,23 @@ class rcube_ldap extends rcube_addressbook
*/
private function _fetch_groups($vlv_page = 0)
{
+ // special case: list groups from 'group_filters' config
+ if (!empty($this->prop['group_filters'])) {
+ $groups = array();
+
+ // list regular groups configuration as special filter
+ if (!empty($this->prop['groups']['filter'])) {
+ $id = '__groups__';
+ $groups[$id] = array('ID' => $id, 'name' => rcube_label('groups'), 'virtual' => true) + $this->prop['groups'];
+ }
+
+ foreach ($this->prop['group_filters'] as $id => $prop) {
+ $groups[$id] = $prop + array('ID' => $id, 'name' => ucfirst($id), 'virtual' => true, 'base_dn' => $this->base_dn);
+ }
+
+ return $groups;
+ }
+
$base_dn = $this->groups_base_dn;
$filter = $this->prop['groups']['filter'];
$name_attr = $this->prop['groups']['name_attr'];
@@ -1776,46 +1575,46 @@ class rcube_ldap extends rcube_addressbook
$sort_attrs = $this->prop['groups']['sort'] ? (array)$this->prop['groups']['sort'] : array($name_attr);
$sort_attr = $sort_attrs[0];
- $this->_debug("C: Search [$filter][dn: $base_dn]");
+ $ldap = $this->ldap;
// use vlv to list groups
if ($this->prop['groups']['vlv']) {
$page_size = 200;
if (!$this->prop['groups']['sort'])
$this->prop['groups']['sort'] = $sort_attrs;
- $vlv_active = $this->_vlv_set_controls($this->prop['groups'], $vlv_page+1, $page_size);
+
+ $ldap = clone $this->ldap;
+ $ldap->set_config($this->prop['groups']);
+ $ldap->set_vlv_page($vlv_page+1, $page_size);
}
- $function = $this->_scope2func($this->prop['groups']['scope']);
- $res = @$function($this->conn, $base_dn, $filter, array_unique(array('dn', 'objectClass', $name_attr, $email_attr, $sort_attr)));
- if ($res === false)
- {
- $this->_debug("S: ".ldap_error($this->conn));
+ $attrs = array_unique(array('dn', 'objectClass', $name_attr, $email_attr, $sort_attr));
+ $ldap_data = $ldap->search($base_dn, $filter, $this->prop['groups']['scope'], $attrs, $this->prop['groups']);
+ if ($ldap_data === false) {
return array();
}
- $ldap_data = ldap_get_entries($this->conn, $res);
- $this->_debug("S: ".ldap_count_entries($this->conn, $res)." record(s)");
-
$groups = array();
$group_sortnames = array();
- $group_count = $ldap_data["count"];
- for ($i=0; $i < $group_count; $i++)
- {
- $group_name = is_array($ldap_data[$i][$name_attr]) ? $ldap_data[$i][$name_attr][0] : $ldap_data[$i][$name_attr];
- $group_id = self::dn_encode($group_name);
+ $group_count = $ldap_data->count();
+ foreach ($ldap_data as $entry) {
+ if (!$entry['dn']) // DN is mandatory
+ $entry['dn'] = $ldap_data->get_dn();
+
+ $group_name = is_array($entry[$name_attr]) ? $entry[$name_attr][0] : $entry[$name_attr];
+ $group_id = self::dn_encode($entry['dn']);
$groups[$group_id]['ID'] = $group_id;
- $groups[$group_id]['dn'] = $ldap_data[$i]['dn'];
+ $groups[$group_id]['dn'] = $entry['dn'];
$groups[$group_id]['name'] = $group_name;
- $groups[$group_id]['member_attr'] = $this->get_group_member_attr($ldap_data[$i]['objectclass']);
+ $groups[$group_id]['member_attr'] = $this->get_group_member_attr($entry['objectclass']);
// list email attributes of a group
- for ($j=0; $ldap_data[$i][$email_attr] && $j < $ldap_data[$i][$email_attr]['count']; $j++) {
- if (strpos($ldap_data[$i][$email_attr][$j], '@') > 0)
- $groups[$group_id]['email'][] = $ldap_data[$i][$email_attr][$j];
+ for ($j=0; $entry[$email_attr] && $j < $entry[$email_attr]['count']; $j++) {
+ if (strpos($entry[$email_attr][$j], '@') > 0)
+ $groups[$group_id]['email'][] = $entry[$email_attr][$j];
}
- $group_sortnames[] = mb_strtolower($ldap_data[$i][$sort_attr][0]);
+ $group_sortnames[] = mb_strtolower($entry[$sort_attr][0]);
}
// recursive call can exit here
@@ -1823,8 +1622,7 @@ class rcube_ldap extends rcube_addressbook
return $groups;
// call recursively until we have fetched all groups
- while ($vlv_active && $group_count == $page_size)
- {
+ while ($this->prop['groups']['vlv'] && $group_count == $page_size) {
$next_page = $this->_fetch_groups(++$vlv_page);
$groups = array_merge($groups, $next_page);
$group_count = count($next_page);
@@ -1835,12 +1633,48 @@ class rcube_ldap extends rcube_addressbook
array_multisort($group_sortnames, SORT_ASC, SORT_STRING, $groups);
// cache this
- $this->cache->set('groups', $groups);
+ if ($this->cache) {
+ $this->cache->set('groups', $groups);
+ }
return $groups;
}
/**
+ * Fetch a group entry from LDAP and save in local cache
+ */
+ private function get_group_entry($group_id)
+ {
+ if (!$this->cache || ($group_cache = $this->cache->get('groups')) === null) {
+ $group_cache = $this->_fetch_groups();
+ }
+
+ // add group record to cache if it isn't yet there
+ if (!isset($group_cache[$group_id])) {
+ $name_attr = $this->prop['groups']['name_attr'];
+ $dn = self::dn_decode($group_id);
+
+ if ($list = $this->ldap->read_entries($dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL',$name_attr,$this->fieldmap['email']))) {
+ $entry = $list[0];
+ $group_name = is_array($entry[$name_attr]) ? $entry[$name_attr][0] : $entry[$name_attr];
+ $group_cache[$group_id]['ID'] = $group_id;
+ $group_cache[$group_id]['dn'] = $dn;
+ $group_cache[$group_id]['name'] = $group_name;
+ $group_cache[$group_id]['member_attr'] = $this->get_group_member_attr($entry['objectclass']);
+ }
+ else {
+ $group_cache[$group_id] = false;
+ }
+
+ if ($this->cache) {
+ $this->cache->set('groups', $group_cache);
+ }
+ }
+
+ return $group_cache[$group_id];
+ }
+
+ /**
* Get group properties such as name and email address(es)
*
* @param string Group identifier
@@ -1848,10 +1682,7 @@ class rcube_ldap extends rcube_addressbook
*/
function get_group($group_id)
{
- if (($group_cache = $this->cache->get('groups')) === null)
- $group_cache = $this->_fetch_groups();
-
- $group_data = $group_cache[$group_id];
+ $group_data = $this->get_group_entry($group_id);
unset($group_data['dn'], $group_data['member_attr']);
return $group_data;
@@ -1865,9 +1696,8 @@ class rcube_ldap extends rcube_addressbook
*/
function create_group($group_name)
{
- $base_dn = $this->groups_base_dn;
- $new_dn = "cn=$group_name,$base_dn";
- $new_gid = self::dn_encode($group_name);
+ $new_dn = 'cn=' . rcube_ldap_generic::quote_string($group_name, true) . ',' . $this->groups_base_dn;
+ $new_gid = self::dn_encode($new_dn);
$member_attr = $this->get_group_member_attr();
$name_attr = $this->prop['groups']['name_attr'] ? $this->prop['groups']['name_attr'] : 'cn';
@@ -1877,12 +1707,14 @@ class rcube_ldap extends rcube_addressbook
$member_attr => '',
);
- if (!$this->ldap_add($new_dn, $new_entry)) {
+ if (!$this->ldap->add($new_dn, $new_entry)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
- $this->cache->remove('groups');
+ if ($this->cache) {
+ $this->cache->remove('groups');
+ }
return array('id' => $new_gid, 'name' => $group_name);
}
@@ -1895,19 +1727,21 @@ class rcube_ldap extends rcube_addressbook
*/
function delete_group($group_id)
{
- if (($group_cache = $this->cache->get('groups')) === null)
+ if (!$this->cache || ($group_cache = $this->cache->get('groups')) === null) {
$group_cache = $this->_fetch_groups();
+ }
- $base_dn = $this->groups_base_dn;
- $group_name = $group_cache[$group_id]['name'];
- $del_dn = "cn=$group_name,$base_dn";
+ $del_dn = $group_cache[$group_id]['dn'];
- if (!$this->ldap_delete($del_dn)) {
+ if (!$this->ldap->delete($del_dn)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
- $this->cache->remove('groups');
+ if ($this->cache) {
+ unset($group_cache[$group_id]);
+ $this->cache->set('groups', $group_cache);
+ }
return true;
}
@@ -1922,21 +1756,22 @@ class rcube_ldap extends rcube_addressbook
*/
function rename_group($group_id, $new_name, &$new_gid)
{
- if (($group_cache = $this->cache->get('groups')) === null)
+ if (!$this->cache || ($group_cache = $this->cache->get('groups')) === null) {
$group_cache = $this->_fetch_groups();
+ }
- $base_dn = $this->groups_base_dn;
- $group_name = $group_cache[$group_id]['name'];
- $old_dn = "cn=$group_name,$base_dn";
- $new_rdn = "cn=$new_name";
- $new_gid = self::dn_encode($new_name);
+ $old_dn = $group_cache[$group_id]['dn'];
+ $new_rdn = "cn=" . rcube_ldap_generic::quote_string($new_name, true);
+ $new_gid = self::dn_encode($new_rdn . ',' . $this->groups_base_dn);
- if (!$this->ldap_rename($old_dn, $new_rdn, null, true)) {
+ if (!$this->ldap->rename($old_dn, $new_rdn, null, true)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return false;
}
- $this->cache->remove('groups');
+ if ($this->cache) {
+ $this->cache->remove('groups');
+ }
return $new_name;
}
@@ -1951,27 +1786,28 @@ class rcube_ldap extends rcube_addressbook
*/
function add_to_group($group_id, $contact_ids)
{
- if (($group_cache = $this->cache->get('groups')) === null)
+ if (!$this->cache || ($group_cache = $this->cache->get('groups')) === null) {
$group_cache = $this->_fetch_groups();
+ }
if (!is_array($contact_ids))
$contact_ids = explode(',', $contact_ids);
- $base_dn = $this->groups_base_dn;
- $group_name = $group_cache[$group_id]['name'];
$member_attr = $group_cache[$group_id]['member_attr'];
- $group_dn = "cn=$group_name,$base_dn";
+ $group_dn = $group_cache[$group_id]['dn'];
$new_attrs = array();
foreach ($contact_ids as $id)
$new_attrs[$member_attr][] = self::dn_decode($id);
- if (!$this->ldap_mod_add($group_dn, $new_attrs)) {
+ if (!$this->ldap->mod_add($group_dn, $new_attrs)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return 0;
}
- $this->cache->remove('groups');
+ if ($this->cache) {
+ $this->cache->remove('groups');
+ }
return count($new_attrs[$member_attr]);
}
@@ -1986,27 +1822,28 @@ class rcube_ldap extends rcube_addressbook
*/
function remove_from_group($group_id, $contact_ids)
{
- if (($group_cache = $this->cache->get('groups')) === null)
+ if (!$this->cache || ($group_cache = $this->cache->get('groups')) === null) {
$group_cache = $this->_fetch_groups();
+ }
if (!is_array($contact_ids))
$contact_ids = explode(',', $contact_ids);
- $base_dn = $this->groups_base_dn;
- $group_name = $group_cache[$group_id]['name'];
$member_attr = $group_cache[$group_id]['member_attr'];
- $group_dn = "cn=$group_name,$base_dn";
- $del_attrs = array();
+ $group_dn = $group_cache[$group_id]['dn'];
+ $del_attrs = array();
foreach ($contact_ids as $id)
$del_attrs[$member_attr][] = self::dn_decode($id);
- if (!$this->ldap_mod_del($group_dn, $del_attrs)) {
+ if (!$this->ldap->mod_del($group_dn, $del_attrs)) {
$this->set_error(self::ERROR_SAVING, 'errorsaving');
return 0;
}
- $this->cache->remove('groups');
+ if ($this->cache) {
+ $this->cache->remove('groups');
+ }
return count($del_attrs[$member_attr]);
}
@@ -2033,23 +1870,18 @@ class rcube_ldap extends rcube_addressbook
$add_filter = "($member_attr=$contact_dn)";
$filter = strtr("(|(member=$contact_dn)(uniqueMember=$contact_dn)$add_filter)", array('\\' => '\\\\'));
- $this->_debug("C: Search [$filter][dn: $base_dn]");
-
- $res = @ldap_search($this->conn, $base_dn, $filter, array($name_attr));
- if ($res === false)
- {
- $this->_debug("S: ".ldap_error($this->conn));
+ $ldap_data = $this->ldap->search($base_dn, $filter, 'sub', array('dn', $name_attr));
+ if ($res === false) {
return array();
}
- $ldap_data = ldap_get_entries($this->conn, $res);
- $this->_debug("S: ".ldap_count_entries($this->conn, $res)." record(s)");
$groups = array();
- for ($i=0; $i<$ldap_data["count"]; $i++)
- {
- $group_name = $ldap_data[$i][$name_attr][0];
- $group_id = self::dn_encode($group_name);
- $groups[$group_id] = $group_id;
+ foreach ($ldap_data as $entry) {
+ if (!$entry['dn'])
+ $entry['dn'] = $ldap_data->get_dn();
+ $group_name = $entry[$name_attr][0];
+ $group_id = self::dn_encode($entry['dn']);
+ $groups[$group_id] = array('ID' => $group_id, 'name' => $group_name, 'dn' => $entry['dn']);
}
return $groups;
}
@@ -2092,135 +1924,6 @@ class rcube_ldap extends rcube_addressbook
/**
- * Generate BER encoded string for Virtual List View option
- *
- * @param integer List offset (first record)
- * @param integer Records per page
- * @return string BER encoded option value
- */
- private function _vlv_ber_encode($offset, $rpp, $search = '')
- {
- # this string is ber-encoded, php will prefix this value with:
- # 04 (octet string) and 10 (length of 16 bytes)
- # the code behind this string is broken down as follows:
- # 30 = ber sequence with a length of 0e (14) bytes following
- # 02 = type integer (in two's complement form) with 2 bytes following (beforeCount): 01 00 (ie 0)
- # 02 = type integer (in two's complement form) with 2 bytes following (afterCount): 01 18 (ie 25-1=24)
- # a0 = type context-specific/constructed with a length of 06 (6) bytes following
- # 02 = type integer with 2 bytes following (offset): 01 01 (ie 1)
- # 02 = type integer with 2 bytes following (contentCount): 01 00
-
- # whith a search string present:
- # 81 = type context-specific/constructed with a length of 04 (4) bytes following (the length will change here)
- # 81 indicates a user string is present where as a a0 indicates just a offset search
- # 81 = type context-specific/constructed with a length of 06 (6) bytes following
-
- # the following info was taken from the ISO/IEC 8825-1:2003 x.690 standard re: the
- # encoding of integer values (note: these values are in
- # two-complement form so since offset will never be negative bit 8 of the
- # leftmost octet should never by set to 1):
- # 8.3.2: If the contents octets of an integer value encoding consist
- # of more than one octet, then the bits of the first octet (rightmost) and bit 8
- # of the second (to the left of first octet) octet:
- # a) shall not all be ones; and
- # b) shall not all be zero
-
- if ($search)
- {
- $search = preg_replace('/[^-[:alpha:] ,.()0-9]+/', '', $search);
- $ber_val = self::_string2hex($search);
- $str = self::_ber_addseq($ber_val, '81');
- }
- else
- {
- # construct the string from right to left
- $str = "020100"; # contentCount
-
- $ber_val = self::_ber_encode_int($offset); // returns encoded integer value in hex format
-
- // calculate octet length of $ber_val
- $str = self::_ber_addseq($ber_val, '02') . $str;
-
- // now compute length over $str
- $str = self::_ber_addseq($str, 'a0');
- }
-
- // now tack on records per page
- $str = "020100" . self::_ber_addseq(self::_ber_encode_int($rpp-1), '02') . $str;
-
- // now tack on sequence identifier and length
- $str = self::_ber_addseq($str, '30');
-
- return pack('H'.strlen($str), $str);
- }
-
-
- /**
- * create ber encoding for sort control
- *
- * @param array List of cols to sort by
- * @return string BER encoded option value
- */
- private function _sort_ber_encode($sortcols)
- {
- $str = '';
- foreach (array_reverse((array)$sortcols) as $col) {
- $ber_val = self::_string2hex($col);
-
- # 30 = ber sequence with a length of octet value
- # 04 = octet string with a length of the ascii value
- $oct = self::_ber_addseq($ber_val, '04');
- $str = self::_ber_addseq($oct, '30') . $str;
- }
-
- // now tack on sequence identifier and length
- $str = self::_ber_addseq($str, '30');
-
- return pack('H'.strlen($str), $str);
- }
-
- /**
- * Add BER sequence with correct length and the given identifier
- */
- private static function _ber_addseq($str, $identifier)
- {
- $len = dechex(strlen($str)/2);
- if (strlen($len) % 2 != 0)
- $len = '0'.$len;
-
- return $identifier . $len . $str;
- }
-
- /**
- * Returns BER encoded integer value in hex format
- */
- private static function _ber_encode_int($offset)
- {
- $val = dechex($offset);
- $prefix = '';
-
- // check if bit 8 of high byte is 1
- if (preg_match('/^[89abcdef]/', $val))
- $prefix = '00';
-
- if (strlen($val)%2 != 0)
- $prefix .= '0';
-
- return $prefix . $val;
- }
-
- /**
- * Returns ascii string encoded in hex
- */
- private static function _string2hex($str)
- {
- $hex = '';
- for ($i=0; $i < strlen($str); $i++)
- $hex .= dechex(ord($str[$i]));
- return $hex;
- }
-
- /**
* HTML-safe DN string encoding
*
* @param string $str DN string
@@ -2247,130 +1950,4 @@ class rcube_ldap extends rcube_addressbook
return base64_decode($str);
}
- /**
- * Wrapper for ldap_add()
- */
- protected function ldap_add($dn, $entry)
- {
- $this->_debug("C: Add [dn: $dn]: ".print_r($entry, true));
-
- $res = ldap_add($this->conn, $dn, $entry);
- if ($res === false) {
- $this->_debug("S: ".ldap_error($this->conn));
- return false;
- }
-
- $this->_debug("S: OK");
- return true;
- }
-
- /**
- * Wrapper for ldap_delete()
- */
- protected function ldap_delete($dn)
- {
- $this->_debug("C: Delete [dn: $dn]");
-
- $res = ldap_delete($this->conn, $dn);
- if ($res === false) {
- $this->_debug("S: ".ldap_error($this->conn));
- return false;
- }
-
- $this->_debug("S: OK");
- return true;
- }
-
- /**
- * Wrapper for ldap_mod_replace()
- */
- protected function ldap_mod_replace($dn, $entry)
- {
- $this->_debug("C: Replace [dn: $dn]: ".print_r($entry, true));
-
- if (!ldap_mod_replace($this->conn, $dn, $entry)) {
- $this->_debug("S: ".ldap_error($this->conn));
- return false;
- }
-
- $this->_debug("S: OK");
- return true;
- }
-
- /**
- * Wrapper for ldap_mod_add()
- */
- protected function ldap_mod_add($dn, $entry)
- {
- $this->_debug("C: Add [dn: $dn]: ".print_r($entry, true));
-
- if (!ldap_mod_add($this->conn, $dn, $entry)) {
- $this->_debug("S: ".ldap_error($this->conn));
- return false;
- }
-
- $this->_debug("S: OK");
- return true;
- }
-
- /**
- * Wrapper for ldap_mod_del()
- */
- protected function ldap_mod_del($dn, $entry)
- {
- $this->_debug("C: Delete [dn: $dn]: ".print_r($entry, true));
-
- if (!ldap_mod_del($this->conn, $dn, $entry)) {
- $this->_debug("S: ".ldap_error($this->conn));
- return false;
- }
-
- $this->_debug("S: OK");
- return true;
- }
-
- /**
- * Wrapper for ldap_rename()
- */
- protected function ldap_rename($dn, $newrdn, $newparent = null, $deleteoldrdn = true)
- {
- $this->_debug("C: Rename [dn: $dn] [dn: $newrdn]");
-
- if (!ldap_rename($this->conn, $dn, $newrdn, $newparent, $deleteoldrdn)) {
- $this->_debug("S: ".ldap_error($this->conn));
- return false;
- }
-
- $this->_debug("S: OK");
- return true;
- }
-
- /**
- * Wrapper for ldap_list()
- */
- protected function ldap_list($dn, $filter, $attrs = array(''))
- {
- $list = array();
- $this->_debug("C: List [dn: $dn] [{$filter}]");
-
- if ($result = ldap_list($this->conn, $dn, $filter, $attrs)) {
- $list = ldap_get_entries($this->conn, $result);
-
- if ($list === false) {
- $this->_debug("S: ".ldap_error($this->conn));
- return array();
- }
-
- $count = $list['count'];
- unset($list['count']);
-
- $this->_debug("S: $count record(s)");
- }
- else {
- $this->_debug("S: ".ldap_error($this->conn));
- }
-
- return $list;
- }
-
}
diff --git a/program/lib/Roundcube/rcube_ldap_generic.php b/program/lib/Roundcube/rcube_ldap_generic.php
new file mode 100644
index 000000000..88378dc22
--- /dev/null
+++ b/program/lib/Roundcube/rcube_ldap_generic.php
@@ -0,0 +1,1049 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | Roundcube/rcube_ldap_generic.php |
+ | |
+ | This file is part of the Roundcube Webmail client |
+ | Copyright (C) 2006-2013, The Roundcube Dev Team |
+ | Copyright (C) 2012-2013, Kolab Systems AG |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
+ | |
+ | PURPOSE: |
+ | Provide basic functionality for accessing LDAP directories |
+ | |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com> |
+ | Aleksander Machniak <machniak@kolabsys.com> |
+ +-----------------------------------------------------------------------+
+*/
+
+
+/*
+ LDAP connection properties
+ --------------------------
+
+ $prop = array(
+ 'host' => '<ldap-server-address>',
+ // or
+ 'hosts' => array('directory.verisign.com'),
+ 'port' => 389,
+ 'use_tls' => true|false,
+ 'ldap_version' => 3, // using LDAPv3
+ 'auth_method' => '', // SASL authentication method (for proxy auth), e.g. DIGEST-MD5
+ 'attributes' => array('dn'), // List of attributes to read from the server
+ 'vlv' => false, // Enable Virtual List View to more efficiently fetch paginated data (if server supports it)
+ 'config_root_dn' => 'cn=config', // Root DN to read config (e.g. vlv indexes) from
+ 'numsub_filter' => '(objectClass=organizationalUnit)', // with VLV, we also use numSubOrdinates to query the total number of records. Set this filter to get all numSubOrdinates attributes for counting
+ 'sizelimit' => '0', // Enables you to limit the count of entries fetched. Setting this to 0 means no limit.
+ 'timelimit' => '0', // Sets the number of seconds how long is spend on the search. Setting this to 0 means no limit.
+ 'network_timeout' => 10, // The timeout (in seconds) for connect + bind arrempts. This is only supported in PHP >= 5.3.0 with OpenLDAP 2.x
+ 'referrals' => true|false, // Sets the LDAP_OPT_REFERRALS option. Mostly used in multi-domain Active Directory setups
+ );
+*/
+
+/**
+ * Model class to access an LDAP directories
+ *
+ * @package Framework
+ * @subpackage LDAP
+ */
+class rcube_ldap_generic
+{
+ const UPDATE_MOD_ADD = 1;
+ const UPDATE_MOD_DELETE = 2;
+ const UPDATE_MOD_REPLACE = 4;
+ const UPDATE_MOD_FULL = 7;
+
+ public $conn;
+ public $vlv_active = false;
+
+ /** private properties */
+ protected $cache = null;
+ protected $config = array();
+ protected $attributes = array('dn');
+ protected $entries = null;
+ protected $result = null;
+ protected $debug = false;
+ protected $list_page = 1;
+ protected $page_size = 10;
+ protected $vlv_config = null;
+
+
+ /**
+ * Object constructor
+ *
+ * @param array $p LDAP connection properties
+ */
+ function __construct($p)
+ {
+ $this->config = $p;
+
+ if (is_array($p['attributes']))
+ $this->attributes = $p['attributes'];
+
+ if (!is_array($p['hosts']) && !empty($p['host']))
+ $this->config['hosts'] = array($p['host']);
+ }
+
+ /**
+ * Activate/deactivate debug mode
+ *
+ * @param boolean $dbg True if LDAP commands should be logged
+ */
+ public function set_debug($dbg = true)
+ {
+ $this->debug = $dbg;
+ }
+
+ /**
+ * Set connection options
+ *
+ * @param mixed $opt Option name as string or hash array with multiple options
+ * @param mixed $val Option value
+ */
+ public function set_config($opt, $val = null)
+ {
+ if (is_array($opt))
+ $this->config = array_merge($this->config, $opt);
+ else
+ $this->config[$opt] = $value;
+ }
+
+ /**
+ * Enable caching by passing an instance of rcube_cache to be used by this object
+ *
+ * @param object rcube_cache Instance or False to disable caching
+ */
+ public function set_cache($cache_engine)
+ {
+ $this->cache = $cache_engine;
+ }
+
+ /**
+ * Set properties for VLV-based paging
+ *
+ * @param number $page Page number to list (starting at 1)
+ * @param number $size Number of entries to display on one page
+ */
+ public function set_vlv_page($page, $size = 10)
+ {
+ $this->list_page = $page;
+ $this->page_size = $size;
+ }
+
+ /**
+ * Establish a connection to the LDAP server
+ */
+ public function connect($host = null)
+ {
+ if (!function_exists('ldap_connect')) {
+ rcube::raise_error(array('code' => 100, 'type' => 'ldap',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "No ldap support in this installation of PHP"),
+ true);
+ return false;
+ }
+
+ if (is_resource($this->conn) && $this->config['host'] == $host)
+ return true;
+
+ if (empty($this->config['ldap_version']))
+ $this->config['ldap_version'] = 3;
+
+ // iterate over hosts if none specified
+ if (!$host) {
+ if (!is_array($this->config['hosts']))
+ $this->config['hosts'] = array($this->config['hosts']);
+
+ foreach ($this->config['hosts'] as $host) {
+ if ($this->connect($host)) {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ // open connection to the given $host
+ $host = rcube_utils::idn_to_ascii(rcube_utils::parse_host($host));
+ $hostname = $host . ($this->config['port'] ? ':'.$this->config['port'] : '');
+
+ $this->_debug("C: Connect to $hostname [{$this->config['name']}]");
+
+ if ($lc = @ldap_connect($host, $this->config['port'])) {
+ if ($this->config['use_tls'] === true)
+ if (!ldap_start_tls($lc))
+ continue;
+
+ $this->_debug("S: OK");
+
+ ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->config['ldap_version']);
+ $this->config['host'] = $host;
+ $this->conn = $lc;
+
+ if (!empty($this->config['network_timeout']))
+ ldap_set_option($lc, LDAP_OPT_NETWORK_TIMEOUT, $this->config['network_timeout']);
+
+ if (isset($this->config['referrals']))
+ ldap_set_option($lc, LDAP_OPT_REFERRALS, $this->config['referrals']);
+ }
+ else {
+ $this->_debug("S: NOT OK");
+ }
+
+ if (!is_resource($this->conn)) {
+ rcube::raise_error(array('code' => 100, 'type' => 'ldap',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Could not connect to any LDAP server, last tried $hostname"),
+ true);
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Bind connection with (SASL-) user and password
+ *
+ * @param string $authc Authentication user
+ * @param string $pass Bind password
+ * @param string $authz Autorization user
+ *
+ * @return boolean True on success, False on error
+ */
+ public function sasl_bind($authc, $pass, $authz=null)
+ {
+ if (!$this->conn) {
+ return false;
+ }
+
+ if (!function_exists('ldap_sasl_bind')) {
+ rcube::raise_error(array('code' => 100, 'type' => 'ldap',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Unable to bind: ldap_sasl_bind() not exists"),
+ true);
+ return false;
+ }
+
+ if (!empty($authz)) {
+ $authz = 'u:' . $authz;
+ }
+
+ if (!empty($this->config['auth_method'])) {
+ $method = $this->config['auth_method'];
+ }
+ else {
+ $method = 'DIGEST-MD5';
+ }
+
+ $this->_debug("C: SASL Bind [mech: $method, authc: $authc, authz: $authz, pass: $pass]");
+
+ if (ldap_sasl_bind($this->conn, NULL, $pass, $method, NULL, $authc, $authz)) {
+ $this->_debug("S: OK");
+ return true;
+ }
+
+ $this->_debug("S: ".ldap_error($this->conn));
+
+ rcube::raise_error(array(
+ 'code' => ldap_errno($this->conn), 'type' => 'ldap',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "SASL Bind failed for authcid=$authc ".ldap_error($this->conn)),
+ true);
+ return false;
+ }
+
+ /**
+ * Bind connection with DN and password
+ *
+ * @param string $dn Bind DN
+ * @param string $pass Bind password
+ *
+ * @return boolean True on success, False on error
+ */
+ public function bind($dn, $pass)
+ {
+ if (!$this->conn) {
+ return false;
+ }
+
+ $this->_debug("C: Bind $dn [pass: $pass]");
+
+ if (@ldap_bind($this->conn, $dn, $pass)) {
+ $this->_debug("S: OK");
+ return true;
+ }
+
+ $this->_debug("S: ".ldap_error($this->conn));
+
+ rcube::raise_error(array(
+ 'code' => ldap_errno($this->conn), 'type' => 'ldap',
+ 'file' => __FILE__, 'line' => __LINE__,
+ 'message' => "Bind failed for dn=$dn: ".ldap_error($this->conn)),
+ true);
+
+ return false;
+ }
+
+ /**
+ * Close connection to LDAP server
+ */
+ public function close()
+ {
+ if ($this->conn) {
+ $this->_debug("C: Close");
+ ldap_unbind($this->conn);
+ $this->conn = null;
+ }
+ }
+
+ /**
+ * Return the last result set
+ *
+ * @return object rcube_ldap_result Result object
+ */
+ function get_result()
+ {
+ return $this->result;
+ }
+
+ /**
+ * Get a specific LDAP entry, identified by its DN
+ *
+ * @param string $dn Record identifier
+ * @return array Hash array
+ */
+ function get_entry($dn)
+ {
+ $rec = null;
+
+ if ($this->conn && $dn) {
+ $this->_debug("C: Read $dn [(objectclass=*)]");
+
+ if ($ldap_result = @ldap_read($this->conn, $dn, '(objectclass=*)', $this->attributes)) {
+ $this->_debug("S: OK");
+
+ if ($entry = ldap_first_entry($this->conn, $ldap_result)) {
+ $rec = ldap_get_attributes($this->conn, $entry);
+ }
+ }
+ else {
+ $this->_debug("S: ".ldap_error($this->conn));
+ }
+
+ if (!empty($rec)) {
+ $rec['dn'] = $dn; // Add in the dn for the entry.
+ }
+ }
+
+ return $rec;
+ }
+
+ /**
+ * Execute the LDAP search based on the stored credentials
+ *
+ * @param string $base_dn The base DN to query
+ * @param string $filter The LDAP filter for search
+ * @param string $scope The LDAP scope (list|sub|base)
+ * @param array $attrs List of entry attributes to read
+ * @param array $prop Hash array with query configuration properties:
+ * - sort: array of sort attributes (has to be in sync with the VLV index)
+ * - search: search string used for VLV controls
+ * @param boolean $count_only Set to true if only entry count is requested
+ *
+ * @return mixed rcube_ldap_result object or number of entries (if count_only=true) or false on error
+ */
+ public function search($base_dn, $filter = '', $scope = 'sub', $attrs = array('dn'), $prop = array(), $count_only = false)
+ {
+ if (!$this->conn) {
+ return false;
+ }
+
+ if (empty($filter)) {
+ $filter = '(objectclass=*)';
+ }
+
+ $this->_debug("C: Search $base_dn for $filter");
+
+ $function = self::scope2func($scope, $ns_function);
+
+ // find available VLV index for this query
+ if (!$count_only && ($vlv_sort = $this->_find_vlv($base_dn, $filter, $scope, $prop['sort']))) {
+ // when using VLV, we get the total count by...
+ // ...either reading numSubOrdinates attribute
+ if (($sub_filter = $this->config['numsub_filter']) &&
+ ($result_count = @$ns_function($this->conn, $base_dn, $sub_filter, array('numSubOrdinates'), 0, 0, 0))
+ ) {
+ $counts = ldap_get_entries($this->conn, $result_count);
+ for ($vlv_count = $j = 0; $j < $counts['count']; $j++)
+ $vlv_count += $counts[$j]['numsubordinates'][0];
+ $this->_debug("D: total numsubordinates = " . $vlv_count);
+ }
+ // ...or by fetching all records dn and count them
+ else if (!function_exists('ldap_parse_virtuallist_control')) {
+ $vlv_count = $this->search($base_dn, $filter, $scope, array('dn'), $prop, true);
+ }
+
+ $this->vlv_active = $this->_vlv_set_controls($vlv_sort, $this->list_page, $this->page_size, $prop['search']);
+ }
+ else {
+ $this->vlv_active = false;
+ }
+
+ // only fetch dn for count (should keep the payload low)
+ if ($ldap_result = @$function($this->conn, $base_dn, $filter,
+ $attrs, 0, (int)$this->config['sizelimit'], (int)$this->config['timelimit'])
+ ) {
+ // when running on a patched PHP we can use the extended functions
+ // to retrieve the total count from the LDAP search result
+ if ($this->vlv_active && function_exists('ldap_parse_virtuallist_control')) {
+ if (ldap_parse_result($this->conn, $ldap_result, $errcode, $matcheddn, $errmsg, $referrals, $serverctrls)) {
+ ldap_parse_virtuallist_control($this->conn, $serverctrls, $last_offset, $vlv_count, $vresult);
+ $this->_debug("S: VLV result: last_offset=$last_offset; content_count=$vlv_count");
+ }
+ else {
+ $this->_debug("S: ".($errmsg ? $errmsg : ldap_error($this->conn)));
+ }
+ }
+ else if ($this->debug) {
+ $this->_debug("S: ".ldap_count_entries($this->conn, $ldap_result)." record(s) found");
+ }
+
+ $this->result = new rcube_ldap_result($this->conn, $ldap_result, $base_dn, $filter, $vlv_count);
+
+ return $count_only ? $this->result->count() : $this->result;
+ }
+ else {
+ $this->_debug("S: ".ldap_error($this->conn));
+ }
+
+ return false;
+ }
+
+ /**
+ * Modify an LDAP entry on the server
+ *
+ * @param string $dn Entry DN
+ * @param array $params Hash array of entry attributes
+ * @param int $mode Update mode (UPDATE_MOD_ADD | UPDATE_MOD_DELETE | UPDATE_MOD_REPLACE)
+ */
+ public function modify($dn, $parms, $mode = 255)
+ {
+ // TODO: implement this
+
+ return false;
+ }
+
+ /**
+ * Wrapper for ldap_add()
+ *
+ * @see ldap_add()
+ */
+ public function add($dn, $entry)
+ {
+ $this->_debug("C: Add $dn: ".print_r($entry, true));
+
+ $res = ldap_add($this->conn, $dn, $entry);
+ if ($res === false) {
+ $this->_debug("S: ".ldap_error($this->conn));
+ return false;
+ }
+
+ $this->_debug("S: OK");
+ return true;
+ }
+
+ /**
+ * Wrapper for ldap_delete()
+ *
+ * @see ldap_delete()
+ */
+ public function delete($dn)
+ {
+ $this->_debug("C: Delete $dn");
+
+ $res = ldap_delete($this->conn, $dn);
+ if ($res === false) {
+ $this->_debug("S: ".ldap_error($this->conn));
+ return false;
+ }
+
+ $this->_debug("S: OK");
+ return true;
+ }
+
+ /**
+ * Wrapper for ldap_mod_replace()
+ *
+ * @see ldap_mod_replace()
+ */
+ public function mod_replace($dn, $entry)
+ {
+ $this->_debug("C: Replace $dn: ".print_r($entry, true));
+
+ if (!ldap_mod_replace($this->conn, $dn, $entry)) {
+ $this->_debug("S: ".ldap_error($this->conn));
+ return false;
+ }
+
+ $this->_debug("S: OK");
+ return true;
+ }
+
+ /**
+ * Wrapper for ldap_mod_add()
+ *
+ * @see ldap_mod_add()
+ */
+ public function mod_add($dn, $entry)
+ {
+ $this->_debug("C: Add $dn: ".print_r($entry, true));
+
+ if (!ldap_mod_add($this->conn, $dn, $entry)) {
+ $this->_debug("S: ".ldap_error($this->conn));
+ return false;
+ }
+
+ $this->_debug("S: OK");
+ return true;
+ }
+
+ /**
+ * Wrapper for ldap_mod_del()
+ *
+ * @see ldap_mod_del()
+ */
+ public function mod_del($dn, $entry)
+ {
+ $this->_debug("C: Delete $dn: ".print_r($entry, true));
+
+ if (!ldap_mod_del($this->conn, $dn, $entry)) {
+ $this->_debug("S: ".ldap_error($this->conn));
+ return false;
+ }
+
+ $this->_debug("S: OK");
+ return true;
+ }
+
+ /**
+ * Wrapper for ldap_rename()
+ *
+ * @see ldap_rename()
+ */
+ public function rename($dn, $newrdn, $newparent = null, $deleteoldrdn = true)
+ {
+ $this->_debug("C: Rename $dn to $newrdn");
+
+ if (!ldap_rename($this->conn, $dn, $newrdn, $newparent, $deleteoldrdn)) {
+ $this->_debug("S: ".ldap_error($this->conn));
+ return false;
+ }
+
+ $this->_debug("S: OK");
+ return true;
+ }
+
+ /**
+ * Wrapper for ldap_list() + ldap_get_entries()
+ *
+ * @see ldap_list()
+ * @see ldap_get_entries()
+ */
+ public function list_entries($dn, $filter, $attributes = array('dn'))
+ {
+ $list = array();
+ $this->_debug("C: List $dn [{$filter}]");
+
+ if ($result = ldap_list($this->conn, $dn, $filter, $attributes)) {
+ $list = ldap_get_entries($this->conn, $result);
+
+ if ($list === false) {
+ $this->_debug("S: ".ldap_error($this->conn));
+ return array();
+ }
+
+ $count = $list['count'];
+ unset($list['count']);
+
+ $this->_debug("S: $count record(s)");
+ }
+ else {
+ $this->_debug("S: ".ldap_error($this->conn));
+ }
+
+ return $list;
+ }
+
+ /**
+ * Wrapper for ldap_read() + ldap_get_entries()
+ *
+ * @see ldap_read()
+ * @see ldap_get_entries()
+ */
+ public function read_entries($dn, $filter, $attributes = null)
+ {
+ $this->_debug("C: Read $dn [{$filter}]");
+
+ if ($this->conn && $dn) {
+ if (!$attributes)
+ $attributes = $this->attributes;
+
+ $result = @ldap_read($this->conn, $dn, $filter, $attributes, 0, (int)$this->config['sizelimit'], (int)$this->config['timelimit']);
+ if ($result === false) {
+ $this->_debug("S: ".ldap_error($this->conn));
+ return false;
+ }
+
+ $this->_debug("S: OK");
+ return ldap_get_entries($this->conn, $result);
+ }
+
+ return false;
+ }
+
+ /**
+ * Choose the right PHP function according to scope property
+ *
+ * @param string $scope The LDAP scope (sub|base|list)
+ * @param string $ns_function Function to be used for numSubOrdinates queries
+ * @return string PHP function to be used to query directory
+ */
+ public static function scope2func($scope, &$ns_function = null)
+ {
+ switch ($scope) {
+ case 'sub':
+ $function = $ns_function = 'ldap_search';
+ break;
+ case 'base':
+ $function = $ns_function = 'ldap_read';
+ break;
+ default:
+ $function = 'ldap_list';
+ $ns_function = 'ldap_read';
+ break;
+ }
+
+ return $function;
+ }
+
+ /**
+ * Convert the given scope integer value to a string representation
+ */
+ public static function scopeint2str($scope)
+ {
+ switch ($scope) {
+ case 2: return 'sub';
+ case 1: return 'one';
+ case 0: return 'base';
+ default: $this->_debug("Scope $scope is not a valid scope integer");
+ }
+
+ return '';
+ }
+
+ /**
+ * Escapes the given value according to RFC 2254 so that it can be safely used in LDAP filters.
+ *
+ * @param string $val Value to quote
+ * @return string The escaped value
+ */
+ public static function escape_value($val)
+ {
+ return strtr($str, array('*'=>'\2a', '('=>'\28', ')'=>'\29',
+ '\\'=>'\5c', '/'=>'\2f'));
+ }
+
+ /**
+ * Escapes a DN value according to RFC 2253
+ *
+ * @param string $dn DN value o quote
+ * @return string The escaped value
+ */
+ public static function escape_dn($dn)
+ {
+ return strtr($str, array(','=>'\2c', '='=>'\3d', '+'=>'\2b',
+ '<'=>'\3c', '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c',
+ '"'=>'\22', '#'=>'\23'));
+ }
+
+ /**
+ * Normalize a LDAP result by converting entry attributes arrays into single values
+ *
+ * @param array $result LDAP result set fetched with ldap_get_entries()
+ * @return array Hash array with normalized entries, indexed by their DNs
+ */
+ public static function normalize_result($result)
+ {
+ if (!is_array($result)) {
+ return array();
+ }
+
+ $entries = array();
+ for ($i = 0; $i < $result['count']; $i++) {
+ $key = $result[$i]['dn'] ? $result[$i]['dn'] : $i;
+ $entries[$key] = self::normalize_entry($result[$i]);
+ }
+
+ return $entries;
+ }
+
+ /**
+ * Turn an LDAP entry into a regular PHP array with attributes as keys.
+ *
+ * @param array $entry Attributes array as retrieved from ldap_get_attributes() or ldap_get_entries()
+ * @return array Hash array with attributes as keys
+ */
+ public static function normalize_entry($entry)
+ {
+ $rec = array();
+ for ($i=0; $i < $entry['count']; $i++) {
+ $attr = $entry[$i];
+ if ($entry[$attr]['count'] == 1) {
+ switch ($attr) {
+ case 'objectclass':
+ $rec[$attr] = array(strtolower($entry[$attr][0]));
+ break;
+ default:
+ $rec[$attr] = $entry[$attr][0];
+ break;
+ }
+ }
+ else {
+ for ($j=0; $j < $entry[$attr]['count']; $j++) {
+ $rec[$attr][$j] = $entry[$attr][$j];
+ }
+ }
+ }
+
+ return $rec;
+ }
+
+ /**
+ * Set server controls for Virtual List View (paginated listing)
+ */
+ private function _vlv_set_controls($sort, $list_page, $page_size, $search = null)
+ {
+ $sort_ctrl = array('oid' => "1.2.840.113556.1.4.473", 'value' => self::_sort_ber_encode((array)$sort));
+ $vlv_ctrl = array('oid' => "2.16.840.1.113730.3.4.9", 'value' => self::_vlv_ber_encode(($offset = ($list_page-1) * $page_size + 1), $page_size, $search), 'iscritical' => true);
+
+ $this->_debug("C: Set controls sort=" . join(' ', unpack('H'.(strlen($sort_ctrl['value'])*2), $sort_ctrl['value'])) . " ($sort[0]);"
+ . " vlv=" . join(' ', (unpack('H'.(strlen($vlv_ctrl['value'])*2), $vlv_ctrl['value']))) . " ($offset/$page_size; $search)");
+
+ if (!ldap_set_option($this->conn, LDAP_OPT_SERVER_CONTROLS, array($sort_ctrl, $vlv_ctrl))) {
+ $this->_debug("S: ".ldap_error($this->conn));
+ $this->set_error(self::ERROR_SEARCH, 'vlvnotsupported');
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Returns unified attribute name (resolving aliases)
+ */
+ private static function _attr_name($namev)
+ {
+ // list of known attribute aliases
+ static $aliases = array(
+ 'gn' => 'givenname',
+ 'rfc822mailbox' => 'email',
+ 'userid' => 'uid',
+ 'emailaddress' => 'email',
+ 'pkcs9email' => 'email',
+ );
+
+ list($name, $limit) = explode(':', $namev, 2);
+ $suffix = $limit ? ':'.$limit : '';
+
+ return (isset($aliases[$name]) ? $aliases[$name] : $name) . $suffix;
+ }
+
+ /**
+ * Quotes attribute value string
+ *
+ * @param string $str Attribute value
+ * @param bool $dn True if the attribute is a DN
+ *
+ * @return string Quoted string
+ */
+ public static function quote_string($str, $dn=false)
+ {
+ // take firt entry if array given
+ if (is_array($str))
+ $str = reset($str);
+
+ if ($dn)
+ $replace = array(','=>'\2c', '='=>'\3d', '+'=>'\2b', '<'=>'\3c',
+ '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c', '"'=>'\22', '#'=>'\23');
+ else
+ $replace = array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c',
+ '/'=>'\2f');
+
+ return strtr($str, $replace);
+ }
+
+ /**
+ * Prints debug info to the log
+ */
+ private function _debug($str)
+ {
+ if ($this->debug && class_exists('rcube')) {
+ rcube::write_log('ldap', $str);
+ }
+ }
+
+
+ /***************** Virtual List View (VLV) related utility functions **************** */
+
+ /**
+ * Return the search string value to be used in VLV controls
+ */
+ private function _vlv_search($sort, $search)
+ {
+ foreach ($search as $attr => $value) {
+ if (!in_array(strtolower($attr), $sort)) {
+ $this->_debug("d: Cannot use VLV search using attribute not indexed: $attr (not in " . var_export($sort, true) . ")");
+ return null;
+ } else {
+ return $value;
+ }
+ }
+ }
+
+ /**
+ * Find a VLV index matching the given query attributes
+ *
+ * @return string Sort attribute or False if no match
+ */
+ private function _find_vlv($base_dn, $filter, $scope, $sort_attrs = null)
+ {
+ if (!$this->config['vlv'] || $scope == 'base') {
+ return false;
+ }
+
+ // get vlv config
+ $vlv_config = $this->_read_vlv_config();
+
+ if ($vlv = $vlv_config[$base_dn]) {
+ $this->_debug("D: Found a VLV for $base_dn");
+
+ if ($vlv['filter'] == strtolower($filter) || stripos($filter, '(&'.$vlv['filter'].'(') === 0) {
+ $this->_debug("D: Filter matches");
+ if ($vlv['scope'] == $scope) {
+ // Not passing any sort attributes means you don't care
+ if (empty($sort_attrs) || in_array($sort_attrs, $vlv['sort'])) {
+ return $vlv['sort'][0];
+ }
+ }
+ else {
+ $this->_debug("D: Scope does not match");
+ }
+ }
+ else {
+ $this->_debug("D: Filter does not match");
+ }
+ }
+ else {
+ $this->_debug("D: No VLV for $base_dn");
+ }
+
+ return false;
+ }
+
+ /**
+ * Return VLV indexes and searches including necessary configuration
+ * details.
+ */
+ private function _read_vlv_config()
+ {
+ if (empty($this->config['vlv']) || empty($this->config['config_root_dn'])) {
+ return array();
+ }
+ // return hard-coded VLV config
+ else if (is_array($this->config['vlv'])) {
+ return $this->config['vlv'];
+ }
+
+ // return cached result
+ if (is_array($this->vlv_config)) {
+ return $this->vlv_config;
+ }
+
+ if ($this->cache && ($cached_config = $this->cache->get('vlvconfig'))) {
+ $this->vlv_config = $cached_config;
+ return $this->vlv_config;
+ }
+
+ $this->vlv_config = array();
+
+ $ldap_result = ldap_search($this->conn, $this->config['config_root_dn'], '(objectclass=vlvsearch)', array('*'), 0, 0, 0);
+ $vlv_searches = new rcube_ldap_result($this->conn, $ldap_result, $this->config['config_root_dn'], '(objectclass=vlvsearch)');
+
+ if ($vlv_searches->count() < 1) {
+ $this->_debug("D: Empty result from search for '(objectclass=vlvsearch)' on '$config_root_dn'");
+ return array();
+ }
+
+ foreach ($vlv_searches->entries(true) as $vlv_search_dn => $vlv_search_attrs) {
+ // Multiple indexes may exist
+ $ldap_result = ldap_search($this->conn, $vlv_search_dn, '(objectclass=vlvindex)', array('*'), 0, 0, 0);
+ $vlv_indexes = new rcube_ldap_result($this->conn, $ldap_result, $vlv_search_dn, '(objectclass=vlvindex)');
+
+ // Reset this one for each VLV search.
+ $_vlv_sort = array();
+ foreach ($vlv_indexes->entries(true) as $vlv_index_dn => $vlv_index_attrs) {
+ $_vlv_sort[] = explode(' ', $vlv_index_attrs['vlvsort']);
+ }
+
+ $this->vlv_config[$vlv_search_attrs['vlvbase']] = array(
+ 'scope' => self::scopeint2str($vlv_search_attrs['vlvscope']),
+ 'filter' => strtolower($vlv_search_attrs['vlvfilter']),
+ 'sort' => $_vlv_sort,
+ );
+ }
+
+ // cache this
+ if ($this->cache)
+ $this->cache->set('vlvconfig', $this->vlv_config);
+
+ $this->_debug("D: Refreshed VLV config: " . var_export($this->vlv_config, true));
+
+ return $this->vlv_config;
+ }
+
+ /**
+ * Generate BER encoded string for Virtual List View option
+ *
+ * @param integer List offset (first record)
+ * @param integer Records per page
+ *
+ * @return string BER encoded option value
+ */
+ private static function _vlv_ber_encode($offset, $rpp, $search = '')
+ {
+ /*
+ this string is ber-encoded, php will prefix this value with:
+ 04 (octet string) and 10 (length of 16 bytes)
+ the code behind this string is broken down as follows:
+ 30 = ber sequence with a length of 0e (14) bytes following
+ 02 = type integer (in two's complement form) with 2 bytes following (beforeCount): 01 00 (ie 0)
+ 02 = type integer (in two's complement form) with 2 bytes following (afterCount): 01 18 (ie 25-1=24)
+ a0 = type context-specific/constructed with a length of 06 (6) bytes following
+ 02 = type integer with 2 bytes following (offset): 01 01 (ie 1)
+ 02 = type integer with 2 bytes following (contentCount): 01 00
+
+ with a search string present:
+ 81 = type context-specific/constructed with a length of 04 (4) bytes following (the length will change here)
+ 81 indicates a user string is present where as a a0 indicates just a offset search
+ 81 = type context-specific/constructed with a length of 06 (6) bytes following
+
+ The following info was taken from the ISO/IEC 8825-1:2003 x.690 standard re: the
+ encoding of integer values (note: these values are in
+ two-complement form so since offset will never be negative bit 8 of the
+ leftmost octet should never by set to 1):
+ 8.3.2: If the contents octets of an integer value encoding consist
+ of more than one octet, then the bits of the first octet (rightmost)
+ and bit 8 of the second (to the left of first octet) octet:
+ a) shall not all be ones; and
+ b) shall not all be zero
+ */
+
+ if ($search) {
+ $search = preg_replace('/[^-[:alpha:] ,.()0-9]+/', '', $search);
+ $ber_val = self::_string2hex($search);
+ $str = self::_ber_addseq($ber_val, '81');
+ }
+ else {
+ // construct the string from right to left
+ $str = "020100"; # contentCount
+
+ $ber_val = self::_ber_encode_int($offset); // returns encoded integer value in hex format
+
+ // calculate octet length of $ber_val
+ $str = self::_ber_addseq($ber_val, '02') . $str;
+
+ // now compute length over $str
+ $str = self::_ber_addseq($str, 'a0');
+ }
+
+ // now tack on records per page
+ $str = "020100" . self::_ber_addseq(self::_ber_encode_int($rpp-1), '02') . $str;
+
+ // now tack on sequence identifier and length
+ $str = self::_ber_addseq($str, '30');
+
+ return pack('H'.strlen($str), $str);
+ }
+
+ /**
+ * create ber encoding for sort control
+ *
+ * @param array List of cols to sort by
+ * @return string BER encoded option value
+ */
+ private static function _sort_ber_encode($sortcols)
+ {
+ $str = '';
+ foreach (array_reverse((array)$sortcols) as $col) {
+ $ber_val = self::_string2hex($col);
+
+ // 30 = ber sequence with a length of octet value
+ // 04 = octet string with a length of the ascii value
+ $oct = self::_ber_addseq($ber_val, '04');
+ $str = self::_ber_addseq($oct, '30') . $str;
+ }
+
+ // now tack on sequence identifier and length
+ $str = self::_ber_addseq($str, '30');
+
+ return pack('H'.strlen($str), $str);
+ }
+
+ /**
+ * Add BER sequence with correct length and the given identifier
+ */
+ private static function _ber_addseq($str, $identifier)
+ {
+ $len = dechex(strlen($str)/2);
+ if (strlen($len) % 2 != 0)
+ $len = '0'.$len;
+
+ return $identifier . $len . $str;
+ }
+
+ /**
+ * Returns BER encoded integer value in hex format
+ */
+ private static function _ber_encode_int($offset)
+ {
+ $val = dechex($offset);
+ $prefix = '';
+
+ // check if bit 8 of high byte is 1
+ if (preg_match('/^[89abcdef]/', $val))
+ $prefix = '00';
+
+ if (strlen($val)%2 != 0)
+ $prefix .= '0';
+
+ return $prefix . $val;
+ }
+
+ /**
+ * Returns ascii string encoded in hex
+ */
+ private static function _string2hex($str)
+ {
+ $hex = '';
+ for ($i=0; $i < strlen($str); $i++) {
+ $hex .= dechex(ord($str[$i]));
+ }
+ return $hex;
+ }
+
+}
diff --git a/program/lib/Roundcube/rcube_ldap_result.php b/program/lib/Roundcube/rcube_ldap_result.php
new file mode 100644
index 000000000..efc3331bc
--- /dev/null
+++ b/program/lib/Roundcube/rcube_ldap_result.php
@@ -0,0 +1,130 @@
+<?php
+
+/*
+ +-----------------------------------------------------------------------+
+ | Roundcube/rcube_ldap_result.php |
+ | |
+ | This file is part of the Roundcube Webmail client |
+ | Copyright (C) 2006-2013, The Roundcube Dev Team |
+ | Copyright (C) 2013, Kolab Systems AG |
+ | |
+ | Licensed under the GNU General Public License version 3 or |
+ | any later version with exceptions for skins & plugins. |
+ | See the README file for a full license statement. |
+ | |
+ | PURPOSE: |
+ | Model class that represents an LDAP search result |
+ | |
+ +-----------------------------------------------------------------------+
+ | Author: Thomas Bruederli <roundcube@gmail.com> |
+ +-----------------------------------------------------------------------+
+*/
+
+
+/**
+ * Model class representing an LDAP search result
+ *
+ * @package Framework
+ * @subpackage LDAP
+ */
+class rcube_ldap_result implements Iterator
+{
+ public $conn;
+ public $ldap;
+ public $base_dn;
+ public $filter;
+
+ private $count = null;
+ private $current = null;
+ private $iteratorkey = 0;
+
+ /**
+ * Default constructor
+ *
+ * @param resource $conn LDAP link identifier
+ * @param resource $ldap LDAP result entry identifier
+ * @param string $base_dn Base DN used to get this result
+ * @param string $filter Filter query used to get this result
+ * @param integer $count Record count value (pre-calculated)
+ */
+ function __construct($conn, $ldap, $base_dn, $filter, $count = null)
+ {
+ $this->conn = $conn;
+ $this->ldap = $ldap;
+ $this->base_dn = $base_dn;
+ $this->filter = $filter;
+ $this->count = $count;
+ }
+
+ /**
+ * Wrapper for ldap_sort()
+ */
+ public function sort($attr)
+ {
+ return ldap_sort($this->conn, $this->ldap, $attr);
+ }
+
+ /**
+ * Get entries count
+ */
+ public function count()
+ {
+ if (!isset($this->count))
+ $this->count = ldap_count_entries($this->conn, $this->ldap);
+
+ return $this->count;
+ }
+
+ /**
+ * Wrapper for ldap_get_entries()
+ *
+ * @param boolean $normalize Optionally normalize the entries to a list of hash arrays
+ * @return array List of LDAP entries
+ */
+ public function entries($normalize = false)
+ {
+ $entries = ldap_get_entries($this->conn, $this->ldap);
+ return $normalize ? rcube_ldap_generic::normalize_result($entries) : $entries;
+ }
+
+ /**
+ * Wrapper for ldap_get_dn() using the current entry pointer
+ */
+ public function get_dn()
+ {
+ return $this->current ? ldap_get_dn($this->conn, $this->current) : null;
+ }
+
+
+ /*** Implements the PHP 5 Iterator interface to make foreach work ***/
+
+ function current()
+ {
+ $attrib = ldap_get_attributes($this->conn, $this->current);
+ $attrib['dn'] = ldap_get_dn($this->conn, $this->current);
+ return $attrib;
+ }
+
+ function key()
+ {
+ return $this->iteratorkey;
+ }
+
+ function rewind()
+ {
+ $this->iteratorkey = 0;
+ $this->current = ldap_first_entry($this->conn, $this->ldap);
+ }
+
+ function next()
+ {
+ $this->iteratorkey++;
+ $this->current = ldap_next_entry($this->conn, $this->current);
+ }
+
+ function valid()
+ {
+ return (bool)$this->current;
+ }
+
+}
diff --git a/program/lib/Roundcube/rcube_vcard.php b/program/lib/Roundcube/rcube_vcard.php
index 90acb2110..a71305c4b 100644
--- a/program/lib/Roundcube/rcube_vcard.php
+++ b/program/lib/Roundcube/rcube_vcard.php
@@ -714,9 +714,15 @@ class rcube_vcard
$value[] = $attrvalues;
}
else if (is_bool($attrvalues)) {
- // true means just tag, not tag=value, as in PHOTO;BASE64:...
+ // true means just a tag, not tag=value, as in PHOTO;BASE64:...
if ($attrvalues) {
- $attr .= strtoupper(";$attrname");
+ // vCard v3 uses ENCODING=B (#1489183)
+ if ($attrname == 'base64') {
+ $attr .= ";ENCODING=B";
+ }
+ else {
+ $attr .= strtoupper(";$attrname");
+ }
}
}
else {
diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc
index c5e6cae4c..046f2f488 100644
--- a/program/localization/en_US/labels.inc
+++ b/program/localization/en_US/labels.inc
@@ -356,6 +356,7 @@ $labels['lastpage'] = 'Show last page';
$labels['group'] = 'Group';
$labels['groups'] = 'Groups';
+$labels['listgroup'] = 'List group members';
$labels['personaladrbook'] = 'Personal Addresses';
$labels['searchsave'] = 'Save search';
diff --git a/program/steps/addressbook/func.inc b/program/steps/addressbook/func.inc
index 3db2409e8..c7f7fb479 100644
--- a/program/steps/addressbook/func.inc
+++ b/program/steps/addressbook/func.inc
@@ -307,7 +307,7 @@ function rcmail_contacts_list($attrib)
global $CONTACTS, $OUTPUT;
// define list of cols to be displayed
- $a_show_cols = array('name');
+ $a_show_cols = array('name','action');
// add id to message list table if not specified
if (!strlen($attrib['id']))
@@ -336,31 +336,73 @@ function rcmail_js_contacts_list($result, $prefix='')
return;
// define list of cols to be displayed
- $a_show_cols = array('name');
+ $a_show_cols = array('name','action');
while ($row = $result->next()) {
+ $row['CID'] = $row['ID'];
+ $row['email'] = reset(rcube_addressbook::get_col_values('email', $row, true));
+
+ $source_id = $OUTPUT->get_env('source');
$a_row_cols = array();
- $classes = array('person'); // org records will follow some day
+ $classes = array($row['_type'] ? $row['_type'] : 'person');
// build contact ID with source ID
if (isset($row['sourceid'])) {
$row['ID'] = $row['ID'].'-'.$row['sourceid'];
+ $source_id = $row['sourceid'];
}
// format each col
foreach ($a_show_cols as $col) {
- $val = $col == 'name' ? rcube_addressbook::compose_list_name($row) : $row[$col];
- $a_row_cols[$col] = Q($val);
+ $val = '';
+ switch ($col) {
+ case 'name':
+ $val = Q(rcube_addressbook::compose_list_name($row));
+ break;
+
+ case 'action':
+ if ($row['_type'] == 'group') {
+ $val = html::a(array(
+ 'href' => '#list',
+ 'rel' => $row['ID'],
+ 'title' => rcube_label('listgroup'),
+ 'onclick' => sprintf("return %s.command('pushgroup',{'source':'%s','id':'%s'},this,event)", JS_OBJECT_NAME, $source_id, $row['CID']),
+ ), '&raquo;');
+ }
+ else
+ $val = '&nbsp;';
+ break;
+
+ default:
+ $val = Q($row[$col]);
+ break;
+ }
+
+ $a_row_cols[$col] = $val;
}
if ($row['readonly'])
$classes[] = 'readonly';
- $OUTPUT->command($prefix.'add_contact_row', $row['ID'], $a_row_cols, join(' ', $classes));
+ $OUTPUT->command($prefix.'add_contact_row', $row['ID'], $a_row_cols, join(' ', $classes), array_intersect_key($row, array('ID'=>1,'readonly'=>1,'_type'=>1,'email'=>1,'name'=>1)));
}
}
+function rcmail_contacts_list_title($attrib)
+{
+ global $OUTPUT;
+
+ $attrib += array('label' => 'contacts', 'id' => 'rcmabooklisttitle', 'tag' => 'span');
+ unset($attrib['name']);
+
+ $OUTPUT->add_gui_object('addresslist_title', $attrib['id']);
+ $OUTPUT->add_label('contacts');
+
+ return html::tag($attrib['tag'], $attrib, rcube_label($attrib['label']), html::$common_attrib);
+}
+
+
// similar function as /steps/settings/identities.inc::rcmail_identity_frame()
function rcmail_contact_frame($attrib)
{
@@ -429,7 +471,7 @@ function rcmail_get_type_label($type)
function rcmail_contact_form($form, $record, $attrib = null)
{
- global $RCMAIL, $CONFIG;
+ global $RCMAIL;
// Allow plugins to modify contact form content
$plugin = $RCMAIL->plugins->exec_hook('contact_form', array(
@@ -438,7 +480,7 @@ function rcmail_contact_form($form, $record, $attrib = null)
$form = $plugin['form'];
$record = $plugin['record'];
$edit_mode = $RCMAIL->action != 'show';
- $del_button = $attrib['deleteicon'] ? html::img(array('src' => $CONFIG['skin_path'] . $attrib['deleteicon'], 'alt' => rcube_label('delete'))) : rcube_label('delete');
+ $del_button = $attrib['deleteicon'] ? html::img(array('src' => $RCMAIL->output->get_skin_file($attrib['deleteicon']), 'alt' => rcube_label('delete'))) : rcube_label('delete');
unset($attrib['deleteicon']);
$out = '';
@@ -695,12 +737,15 @@ function rcmail_contact_form($form, $record, $attrib = null)
function rcmail_contact_photo($attrib)
{
- global $SOURCE_ID, $CONTACTS, $CONTACT_COLTYPES, $RCMAIL, $CONFIG;
+ global $SOURCE_ID, $CONTACTS, $CONTACT_COLTYPES, $RCMAIL;
if ($result = $CONTACTS->get_result())
$record = $result->first();
- $photo_img = $attrib['placeholder'] ? $CONFIG['skin_path'] . $attrib['placeholder'] : 'program/resources/blank.gif';
+ $photo_img = $attrib['placeholder'] ? $RCMAIL->output->get_skin_file($attrib['placeholder']) : 'program/resources/blank.gif';
+ if ($record['_type'] == 'group' && $attrib['placeholdergroup'])
+ $photo_img = $RCMAIL->output->get_skin_file($attrib['placeholdergroup']);
+
$RCMAIL->output->set_env('photo_placeholder', $photo_img);
unset($attrib['placeholder']);
@@ -791,6 +836,7 @@ $OUTPUT->add_handlers(array(
'directorylist' => 'rcmail_directory_list',
// 'groupslist' => 'rcmail_contact_groups',
'addresslist' => 'rcmail_contacts_list',
+ 'addresslisttitle' => 'rcmail_contacts_list_title',
'addressframe' => 'rcmail_contact_frame',
'recordscountdisplay' => 'rcmail_rowcount_display',
'searchform' => array($OUTPUT, 'search_form')
diff --git a/program/steps/addressbook/list.inc b/program/steps/addressbook/list.inc
index 1bb28658b..6f3a3e0f3 100644
--- a/program/steps/addressbook/list.inc
+++ b/program/steps/addressbook/list.inc
@@ -81,6 +81,11 @@ else {
$OUTPUT->show_message('contactsearchonly', 'notice');
$OUTPUT->command('command', 'advanced-search');
}
+
+ if ($CONTACTS->group_id) {
+ $OUTPUT->command('set_group_prop', array('ID' => $CONTACTS->group_id)
+ + array_intersect_key((array)$CONTACTS->get_group($CONTACTS->group_id), array('name'=>1,'email'=>1)));
+ }
}
// update message count display
diff --git a/program/steps/addressbook/save.inc b/program/steps/addressbook/save.inc
index 25bfbd48b..e7e5efc63 100644
--- a/program/steps/addressbook/save.inc
+++ b/program/steps/addressbook/save.inc
@@ -134,11 +134,11 @@ if (!empty($cid))
$record['email'] = reset($CONTACTS->get_col_values('email', $record, true));
$record['name'] = rcube_addressbook::compose_list_name($record);
- foreach (array('name', 'email') as $col)
+ foreach (array('name') as $col)
$a_js_cols[] = Q((string)$record[$col]);
// update the changed col in list
- $OUTPUT->command('parent.update_contact_row', $cid, $a_js_cols, $newcid, $source);
+ $OUTPUT->command('parent.update_contact_row', $cid, $a_js_cols, $newcid, $source, $record);
// show confirmation
$OUTPUT->show_message('successfullysaved', 'confirmation', null, false);
diff --git a/program/steps/addressbook/show.inc b/program/steps/addressbook/show.inc
index 1a97c65b1..63abc8c4b 100644
--- a/program/steps/addressbook/show.inc
+++ b/program/steps/addressbook/show.inc
@@ -213,8 +213,11 @@ function rcmail_contact_record_groups($contact_id)
$checkbox = new html_checkbox(array('name' => '_gid[]',
'class' => 'groupmember', 'disabled' => $CONTACTS->readonly));
- foreach ($GROUPS as $group) {
+ foreach (array_merge($GROUPS, $members) as $group) {
$gid = $group['ID'];
+ if ($seen[$gid]++)
+ continue;
+
$table->add(null, $checkbox->show($members[$gid] ? $gid : null,
array('value' => $gid, 'id' => 'ff_gid' . $gid)));
$table->add(null, html::label('ff_gid' . $gid, Q($group['name'])));
diff --git a/program/steps/mail/list_contacts.inc b/program/steps/mail/list_contacts.inc
index 7e3b349cd..a48109fed 100644
--- a/program/steps/mail/list_contacts.inc
+++ b/program/steps/mail/list_contacts.inc
@@ -73,8 +73,11 @@ else {
$CONTACTS->set_pagesize($page_size);
$CONTACTS->set_page($page);
+ if ($group_id = get_input_value('_gid', RCUBE_INPUT_GPC)) {
+ $CONTACTS->set_group($group_id);
+ }
// list groups of this source (on page one)
- if ($CONTACTS->groups && $CONTACTS->list_page == 1) {
+ else if ($CONTACTS->groups && $CONTACTS->list_page == 1) {
foreach ($CONTACTS->list_groups() as $group) {
$CONTACTS->reset();
$CONTACTS->set_group($group['ID']);
@@ -89,6 +92,19 @@ else {
'contactgroup' => html::span(array('title' => $email), Q($group['name']))), 'group');
}
}
+ // make virtual groups clickable to list their members
+ else if ($group_prop['virtual']) {
+ $row_id = 'G'.$group['ID'];
+ $OUTPUT->command('add_contact_row', $row_id, array(
+ 'contactgroup' => html::a(array(
+ 'href' => '#list',
+ 'rel' => $row['ID'],
+ 'title' => rcube_label('listgroup'),
+ 'onclick' => sprintf("return %s.command('pushgroup',{'source':'%s','id':'%s'},this,event)", JS_OBJECT_NAME, $source, $group['ID']),
+ ), Q($group['name']) . '&nbsp;' . html::span('action', '&raquo;'))),
+ 'group',
+ array('ID' => $group['ID'], 'name' => $group['name'], 'virtual' => true));
+ }
// show group with count
else if (($result = $CONTACTS->count()) && $result->count) {
$row_id = 'E'.$group['ID'];
@@ -97,10 +113,12 @@ else {
'contactgroup' => Q($group['name'] . ' (' . intval($result->count) . ')')), 'group');
}
}
+
+ $CONTACTS->reset();
+ $CONTACTS->set_group(0);
}
// get contacts for this user
- $CONTACTS->set_group(0);
$result = $CONTACTS->list_records($afields);
}
}
@@ -118,10 +136,13 @@ else if (!empty($result) && $result->count > 0) {
foreach ($emails as $i => $email) {
$row_id = $row['ID'].$i;
$jsresult[$row_id] = format_email_recipient($email, $name);
+ $classname = $row['_type'] == 'group' ? 'group' : 'person';
+ $keyname = $row['_type'] == 'group' ? 'contactgroup' : 'contact';
+
$OUTPUT->command('add_contact_row', $row_id, array(
- 'contact' => html::span(array('title' => $email), Q($name ? $name : $email) .
+ $keyname => html::span(array('title' => $email), Q($name ? $name : $email) .
($name && count($emails) > 1 ? '&nbsp;' . html::span('email', Q($email)) : '')
- )), 'person');
+ )), $classname);
}
}
}
diff --git a/skins/classic/addressbook.css b/skins/classic/addressbook.css
index 5afa4592f..415142e0c 100644
--- a/skins/classic/addressbook.css
+++ b/skins/classic/addressbook.css
@@ -224,6 +224,37 @@
-o-text-overflow: ellipsis;
}
+#contacts-table .contact.readonly td
+{
+ font-style: italic;
+}
+
+#contacts-table td.name
+{
+ width: 95%;
+}
+
+#contacts-table td.action
+{
+ width: 12px;
+ padding: 0px 6px 0 4px;
+ text-align: right;
+}
+
+#contacts-table td.action a
+{
+ font-size: 16px;
+ font-weight: bold;
+ font-style: normal;
+ text-decoration: none;
+ color: #333;
+}
+
+#contacts-table .selected td.action a
+{
+ color: #fff;
+}
+
#contacts-box
{
position: absolute;
diff --git a/skins/classic/functions.js b/skins/classic/functions.js
index 5dd332ab7..d10812c57 100644
--- a/skins/classic/functions.js
+++ b/skins/classic/functions.js
@@ -967,6 +967,7 @@ function rcube_init_mail_ui()
rcmail.addEventListener('responseaftergetunread', rcube_render_mailboxlist);
rcmail.addEventListener('responseaftercheck-recent', rcube_render_mailboxlist);
rcmail.addEventListener('aftercollapse-folder', rcube_render_mailboxlist);
+ rcmail.addEventListener('afterimport-messages', function(){ rcmail_ui.show_popup('uploadform', false); });
new rcmail_scroller('#mailboxlist-content', '#mailboxlist-title', '#mailboxlist-footer');
}
diff --git a/skins/classic/images/contactgroup.png b/skins/classic/images/contactgroup.png
new file mode 100644
index 000000000..c46383255
--- /dev/null
+++ b/skins/classic/images/contactgroup.png
Binary files differ
diff --git a/skins/classic/mail.css b/skins/classic/mail.css
index 7c350ca3d..0193e87ff 100644
--- a/skins/classic/mail.css
+++ b/skins/classic/mail.css
@@ -1600,16 +1600,19 @@ input.from_address
vertical-align: middle;
}
+#upload-form,
#attachment-form
{
padding: 6px;
}
+#upload-form div,
#attachment-form div
{
padding: 2px;
}
+#upload-form div.buttons,
#attachment-form div.buttons
{
margin-top: 4px;
@@ -1687,6 +1690,14 @@ input.from_address
-o-text-overflow: ellipsis;
}
+#contacts-table td span.email
+{
+ display: inline;
+ color: #ccc;
+ font-style: italic;
+ margin-left: 0.5em;
+}
+
#abookcountbar
{
margin-top: 4px;
diff --git a/skins/classic/templates/addressbook.html b/skins/classic/templates/addressbook.html
index ba119891c..fdcd1847f 100644
--- a/skins/classic/templates/addressbook.html
+++ b/skins/classic/templates/addressbook.html
@@ -75,7 +75,7 @@
<div id="addressscreen">
<div id="addresslist">
-<div class="boxtitle"><roundcube:label name="contacts" /></div>
+<roundcube:object name="addresslisttitle" label="contacts" tag="div" class="boxtitle" />
<div class="boxlistcontent">
<roundcube:object name="addresslist" id="contacts-table" class="records-table" cellspacing="0" summary="Contacts list" noheader="true" />
</div>
diff --git a/skins/classic/templates/contact.html b/skins/classic/templates/contact.html
index d74a78b27..8be112b49 100644
--- a/skins/classic/templates/contact.html
+++ b/skins/classic/templates/contact.html
@@ -13,7 +13,7 @@
<div id="sourcename"><roundcube:label name="addressbook" />: <roundcube:var name="env:sourcename" /></div>
<roundcube:endif />
- <div id="contactphoto"><roundcube:object name="contactphoto" id="contactpic" placeholder="/images/contactpic.png" /></div>
+ <div id="contactphoto"><roundcube:object name="contactphoto" id="contactpic" placeholder="/images/contactpic.png" placeholderGroup="/images/contactgroup.png" /></div>
<roundcube:object name="contacthead" id="contacthead" />
<div style="clear:both"></div>
<div id="contacttabs">
diff --git a/skins/classic/templates/mail.html b/skins/classic/templates/mail.html
index c7010e87c..c3b4004cc 100644
--- a/skins/classic/templates/mail.html
+++ b/skins/classic/templates/mail.html
@@ -140,7 +140,8 @@
<div id="mailboxoptionsmenu" class="popupmenu">
<ul>
<li><roundcube:button command="expunge" type="link" label="compact" classAct="active" /></li>
- <li class="separator_below"><roundcube:button command="purge" type="link" label="empty" classAct="active" /></li>
+ <li><roundcube:button command="purge" type="link" label="empty" classAct="active" /></li>
+ <li class="separator_below"><roundcube:button name="messageimport" type="link" class="active" label="importmessages" id="uploadformlink" onclick="rcmail_ui.show_popup('uploadform', true); return false" /></li>
<li><roundcube:button command="folders" task="settings" type="link" label="managefolders" classAct="active" /></li>
<roundcube:container name="mailboxoptions" id="mailboxoptionsmenu" />
</ul>
@@ -204,5 +205,7 @@
</div>
</div>
+<roundcube:object name="messageimportform" id="upload-form" attachmentFieldSize="40" class="popupmenu" />
+
</body>
</html>
diff --git a/skins/larry/addressbook.css b/skins/larry/addressbook.css
index ff3951497..090e54c7b 100644
--- a/skins/larry/addressbook.css
+++ b/skins/larry/addressbook.css
@@ -75,10 +75,6 @@
text-overflow: ellipsis;
}
-#contacts-table .contact.readonly td {
- font-style: italic;
-}
-
#directorylist li.addressbook a {
background-position: 6px -766px;
}
@@ -131,6 +127,28 @@
left: 20px;
}
+#contacts-table .contact.readonly td {
+ font-style: italic;
+}
+
+#contacts-table td.name {
+ width: 95%;
+}
+
+#contacts-table td.action {
+ width: 24px;
+ padding: 4px;
+}
+
+#contacts-table td.action a {
+ display: block;
+ width: 16px;
+ height: 14px;
+ text-indent: -5000px;
+ overflow: hidden;
+ background: url(images/listicons.png) -2px -1180px no-repeat;
+}
+
#contacts-table .contact td.name {
background-position: 6px -1603px;
}
@@ -141,6 +159,29 @@
font-weight: bold;
}
+#contacts-table .group td.name {
+ background-position: 6px -1555px;
+}
+
+#contacts-table .group.selected td.name,
+#contacts-table .group.unfocused td.name {
+ background-position: 6px -1579px;
+ font-weight: bold;
+}
+
+#addresslist .boxtitle {
+ padding-right: 95px;
+ overflow: hidden;
+ text-overflow: ellipsis;
+}
+
+#addresslist .boxtitle a.poplink {
+ color: #004458;
+ font-size: 14px;
+ line-height: 12px;
+ text-decoration: none;
+}
+
#contact-frame {
position: absolute;
top: 0;
diff --git a/skins/larry/ie7hacks.css b/skins/larry/ie7hacks.css
index 6d7af4787..fc4713361 100644
--- a/skins/larry/ie7hacks.css
+++ b/skins/larry/ie7hacks.css
@@ -41,6 +41,7 @@ a.deletebutton,
.boxfooter .listbutton .inner,
.attachmentslist li a.delete,
.attachmentslist li a.cancelupload,
+#contacts-table td.action a,
.previewheader .iconlink,
.minimal #taskbar .button-inner {
/* workaround for text-indent which also offsets the background image */
diff --git a/skins/larry/images/contactgroup.png b/skins/larry/images/contactgroup.png
new file mode 100644
index 000000000..8303cf02f
--- /dev/null
+++ b/skins/larry/images/contactgroup.png
Binary files differ
diff --git a/skins/larry/images/listicons.png b/skins/larry/images/listicons.png
index f4505d4fa..e4ffef660 100644
--- a/skins/larry/images/listicons.png
+++ b/skins/larry/images/listicons.png
Binary files differ
diff --git a/skins/larry/mail.css b/skins/larry/mail.css
index fe9e56ea3..d653c7804 100644
--- a/skins/larry/mail.css
+++ b/skins/larry/mail.css
@@ -1238,6 +1238,19 @@ div.message-partheaders .headers-table td.header {
text-overflow: ellipsis;
}
+#contacts-table td.contactgroup a {
+ color: #376572;
+ text-decoration: none;
+}
+
+#contacts-table td.contactgroup a span {
+ display: inline-block;
+ font-size: 16px;
+ font-weight: bold;
+ line-height: 11px;
+ margin-left: 0.3em;
+}
+
#contacts-table tr:first-child td {
border-top: 0;
}
diff --git a/skins/larry/styles.css b/skins/larry/styles.css
index cfbf9ac5f..8ddbb5960 100644
--- a/skins/larry/styles.css
+++ b/skins/larry/styles.css
@@ -1059,6 +1059,10 @@ table.listing tr.droptarget td {
background-color: #e8e798;
}
+.listbox table.listing {
+ background-color: #d9ecf4;
+}
+
table.listing,
table.layout {
border: 0;
diff --git a/skins/larry/templates/addressbook.html b/skins/larry/templates/addressbook.html
index 401640f1f..d9e491f99 100644
--- a/skins/larry/templates/addressbook.html
+++ b/skins/larry/templates/addressbook.html
@@ -50,7 +50,7 @@
<!-- contacts list -->
<div id="addresslist" class="uibox listbox">
-<h2 class="boxtitle"><roundcube:label name="contacts" /></h2>
+<roundcube:object name="addresslisttitle" label="contacts" tag="h2" class="boxtitle" />
<div class="scroller withfooter">
<roundcube:object name="addresslist" id="contacts-table" class="listing" noheader="true" />
</div>
diff --git a/skins/larry/templates/contact.html b/skins/larry/templates/contact.html
index d252049cd..59fe6f79f 100644
--- a/skins/larry/templates/contact.html
+++ b/skins/larry/templates/contact.html
@@ -13,7 +13,7 @@
<div id="sourcename"><roundcube:label name="addressbook" />: <roundcube:var name="env:sourcename" /></div>
<roundcube:endif />
- <div id="contactphoto"><roundcube:object name="contactphoto" id="contactpic" placeholder="/images/contactpic.png" /></div>
+ <div id="contactphoto"><roundcube:object name="contactphoto" id="contactpic" placeholder="/images/contactpic.png" placeholderGroup="/images/contactgroup.png" /></div>
<roundcube:object name="contacthead" id="contacthead" />
<br style="clear:both" />
diff --git a/skins/larry/ui.js b/skins/larry/ui.js
index ec4d03d00..38d8539c7 100644
--- a/skins/larry/ui.js
+++ b/skins/larry/ui.js
@@ -191,6 +191,8 @@ function rcube_mail_ui()
/*** addressbook task ***/
else if (rcmail.env.task == 'addressbook') {
rcmail.addEventListener('afterupload-photo', show_uploadform);
+ rcmail.addEventListener('beforepushgroup', push_contactgroup);
+ rcmail.addEventListener('beforepopgroup', pop_contactgroup);
if (rcmail.env.action == '') {
new rcube_splitter({ id:'addressviewsplitterd', p1:'#addressview-left', p2:'#addressview-right',
@@ -825,6 +827,35 @@ function rcube_mail_ui()
});
}
+ function push_contactgroup(p)
+ {
+ // lets the contacts list swipe to the left, nice!
+ var table = $('#contacts-table'),
+ scroller = table.parent().css('overflow', 'hidden');
+
+ table.clone()
+ .css({ position:'absolute', top:'0', left:'0', width:table.width()+'px', 'z-index':10 })
+ .appendTo(scroller)
+ .animate({ left: -(table.width()+5) + 'px' }, 300, 'swing', function(){
+ $(this).remove();
+ scroller.css('overflow', 'auto')
+ });
+ }
+
+ function pop_contactgroup(p)
+ {
+ // lets the contacts list swipe to the left, nice!
+ var table = $('#contacts-table'),
+ scroller = table.parent().css('overflow', 'hidden'),
+ clone = table.clone().appendTo(scroller);
+
+ table.css({ position:'absolute', top:'0', left:-(table.width()+5) + 'px', width:table.width()+'px', height:table.height()+'px', 'z-index':10 })
+ .animate({ left:'0' }, 300, 'linear', function(){
+ clone.remove();
+ $(this).css({ position:'relative', left:'0', width:'100%', height:'auto', 'z-index':1 });
+ scroller.css('overflow', 'auto')
+ });
+ }
function show_uploadform()
{
@@ -835,7 +866,7 @@ function rcube_mail_ui()
$dialog.dialog('close');
return;
}
-
+
// add icons to clone file input field
if (rcmail.env.action == 'compose' && !$dialog.data('extended')) {
$('<a>')