config/main.inc.php.dist
@@ -651,6 +651,7 @@ 'phone:work' => 'telephoneNumber', 'phone:mobile' => 'mobile', 'phone:pager' => 'pager', 'phone:workfax' => 'facsimileTelephoneNumber', 'street' => 'street', 'zipcode' => 'postalCode', 'region' => 'st', @@ -661,9 +662,8 @@ 'department' => 'ou', 'jobtitle' => 'title', 'notes' => 'description', 'photo' => 'jpegPhoto', // these currently don't work: // 'phone:workfax' => 'facsimileTelephoneNumber', // 'photo' => 'jpegPhoto', // 'manager' => 'manager', // 'assistant' => 'secretary', ), @@ -674,27 +674,48 @@ // '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)', ), ), ); */ program/include/rcmail.php
@@ -294,7 +294,7 @@ $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) program/js/app.js
@@ -255,7 +255,8 @@ } } 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 @@ 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,9 +1100,23 @@ } 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': @@ -3091,7 +3108,13 @@ 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 @@ 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 @@ // 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 { writable = writable || (!source.readonly && !contact.readonly); } } else { this.env.selection_sources.push(this.env.source); writable = !source.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,10 +4203,28 @@ 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) { @@ -4233,16 +4280,38 @@ 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(' » '); } 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 @@ // 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 @@ }; // 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 @@ } 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 @@ row.cols.push(col); } // store data in list member list.data[cid] = data; list.insert_row(row); this.enable_command('export', list.rowcount > 0); 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 @@ public $coltypes = array(); /** private properties */ protected $conn; protected $ldap; protected $prop = array(); protected $fieldmap = array(); protected $sub_filter; @@ -51,9 +51,6 @@ private $group_url = null; private $cache; private $vlv_active = false; private $vlv_count = 0; /** * Object constructor @@ -65,6 +62,8 @@ function __construct($p, $debug = false, $mail_domain = null) { $this->prop = $p; $fetch_attributes = array('objectClass'); if (isset($p['searchonly'])) $this->searchonly = $p['searchonly']; @@ -82,6 +81,12 @@ $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']; } else if (is_array($p['group_filters']) && count($p['group_filters'])) { $this->groups = true; } // fieldmap property is given @@ -192,6 +197,17 @@ $this->cache = $rcube->get_cache($cache_name, $cache_type, $cache_ttl); // determine which attributes to fetch $this->prop['attributes'] = array_merge(array_values($this->fieldmap), $fetch_attributes); $this->prop['list_attributes'] = $fetch_attributes; foreach ($rcube->config->get('contactlist_fields') as $col) { $this->prop['list_attributes'] = array_merge($this->prop['list_attributes'], $this->_map_field($col)); } // initialize ldap wrapper object $this->ldap = new rcube_ldap_generic($this->prop, true); $this->ldap->set_cache($this->cache); $this->_connect(); } @@ -203,49 +219,18 @@ { $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 +245,7 @@ $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']) { @@ -281,7 +266,8 @@ 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']); if (!$this->ldap->bind($this->prop['search_bind_dn'], $this->prop['search_bind_pw'])) continue; // bind failed, try neyt host } // Search for the dn to use to authenticate @@ -290,10 +276,11 @@ $this->_debug("S: searching with base {$this->prop['search_base_dn']} for {$this->prop['search_filter']}"); $res = @ldap_search($this->conn, $this->prop['search_base_dn'], $this->prop['search_filter'], array('uid')); // TODO: use $this->ldap->search() here $res = @ldap_search($this->ldap->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)) if (($entry = ldap_first_entry($this->ldap->conn, $res)) && ($bind_dn = ldap_get_dn($this->ldap->conn, $entry)) ) { $this->_debug("S: search returned dn: $bind_dn"); $dn = ldap_explode_dn($bind_dn, 1); @@ -301,7 +288,7 @@ } } else { $this->_debug("S: ".ldap_error($this->conn)); $this->_debug("S: ".ldap_error($this->ldap->conn)); } // DN not found @@ -333,13 +320,13 @@ } 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 +337,10 @@ } // 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,100 +350,12 @@ /** * 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, 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; } $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 * * @return boolean True on success, False on error */ public function bind($dn, $pass) { 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; } /** * Close connection to LDAP server */ function close() { if ($this->conn) { $this->_debug("C: Close"); ldap_unbind($this->conn); $this->conn = null; if ($this->ldap) { $this->ldap->close(); } } @@ -469,6 +368,29 @@ function get_name() { return $this->prop['name']; } /** * Set internal list page * * @param number Page number to list */ function set_page($page) { $this->list_page = (int)$page; $this->ldap->set_vlv_page($this->list_page, $this->page_size); } /** * Set internal page size * * @param number Number of records to display on one page */ function set_pagesize($size) { $this->page_size = (int)$size; $this->ldap->set_vlv_page($this->list_page, $this->page_size); } @@ -528,16 +450,14 @@ */ 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 +471,34 @@ $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 +523,20 @@ // 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)); $this->_debug("C: Read Group [dn: $dn]"); $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 +577,19 @@ 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 +615,22 @@ { $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 +666,11 @@ $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 +682,20 @@ $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 +721,27 @@ } } 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" . self::_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" . self::_quote_string($val) . "$ws)"; if (count($attrs) > 1) $filter .= ')'; } @@ -897,7 +774,6 @@ // set filter string and execute search $this->set_search_set($filter); $this->_exec_search(); if ($select) $this->list_records(); @@ -916,20 +792,21 @@ 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 +836,16 @@ { $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 +857,7 @@ // 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 +970,7 @@ } // 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'].'='.self::_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 +983,19 @@ } } 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.'='.self::_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 +1101,7 @@ // 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 +1111,17 @@ // Handle RDN change if ($replacedata[$this->prop['LDAP_rdn']]) { $newdn = $this->prop['LDAP_rdn'].'=' .$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true) .self::_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); .self::_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 +1137,8 @@ // 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.'='.self::_quote_string($val).','.$dn; if (!$this->ldap->delete($subdn)) { return false; } } @@ -1281,7 +1146,7 @@ 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 +1154,7 @@ // 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 +1165,7 @@ // 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 +1178,12 @@ // add sub-entries if (!empty($subnewdata)) { foreach ($subnewdata as $fld => $val) { $subdn = $fld.'='.$this->_quote_string($val).','.$dn; $subdn = $fld.'='.self::_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 +1211,9 @@ // 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 +1222,7 @@ } // Delete the record. if (!$this->ldap_delete($dn)) { if (!$this->ldap->delete($dn)) { $this->set_error(self::ERROR_SAVING, 'errorsaving'); return false; } @@ -1366,7 +1231,7 @@ 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 +1246,8 @@ */ 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 +1297,26 @@ } } /** * 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->prop['groups']['name_attr']; } foreach ($fieldmap as $rf => $lf) { for ($i=0; $i < $rec[$lf]['count']; $i++) { if (!($value = $rec[$lf][$i])) @@ -1607,8 +1378,10 @@ 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 +1432,16 @@ 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 @@ -1710,20 +1493,14 @@ /** * 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; } @@ -1769,6 +1546,23 @@ */ 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 +1570,46 @@ $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 +1617,7 @@ 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); @@ -1841,6 +1634,38 @@ } /** * Fetch a group entry from LDAP and save in local cache */ private function get_group_entry($group_id) { if (($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); $this->_debug("C: Read Group [dn: $dn]"); 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; } $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 +1673,7 @@ */ 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 +1687,8 @@ */ 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=' . self::_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,7 +1698,7 @@ $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; } @@ -1898,16 +1719,15 @@ if (($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'); unset($group_cache[$group_id]); $this->cache->set('groups', $group_cache); return true; } @@ -1925,13 +1745,11 @@ if (($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=" . self::_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; } @@ -1957,16 +1775,14 @@ 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; } @@ -1992,16 +1808,14 @@ 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; } @@ -2033,23 +1847,18 @@ $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 +1901,6 @@ /** * 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 @@ -2245,132 +1925,6 @@ { $str = str_pad(strtr($str, '-_', '+/'), strlen($str) % 4, '=', STR_PAD_RIGHT); 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; } } program/lib/Roundcube/rcube_ldap_generic.php
New file @@ -0,0 +1,1059 @@ <?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 * @param boolean $debug Enables debug mode */ function __construct($p, $debug = false) { $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']); $this->debug = $debug; } /** * 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 [$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: 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: $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: $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) { if (empty($filter)) $filter = $filter = '(objectclass=*)'; $this->_debug("C: Search [$filter][dn: $base_dn]"); $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 ($this->config['numsub_filter'] && ($result_count = @$ns_function($this->conn, $base_dn, $this->config['numsub_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: $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: $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: $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: $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: $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: $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() + 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: $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: $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 */ 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); } /** * 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: " . $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 " . $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 # 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 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; } } program/lib/Roundcube/rcube_ldap_result.php
New file @@ -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; } } program/localization/en_US/labels.inc
@@ -356,6 +356,7 @@ $labels['group'] = 'Group'; $labels['groups'] = 'Groups'; $labels['listgroup'] = 'List group members'; $labels['personaladrbook'] = 'Personal Addresses'; $labels['searchsave'] = 'Save search'; program/steps/addressbook/func.inc
@@ -307,7 +307,7 @@ 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,28 +336,70 @@ 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']), ), '»'); } else $val = ' '; 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); } @@ -429,7 +471,7 @@ 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 @@ $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_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 @@ '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') program/steps/addressbook/list.inc
@@ -81,6 +81,11 @@ $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 program/steps/addressbook/save.inc
@@ -134,11 +134,11 @@ $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); program/steps/addressbook/show.inc
@@ -213,8 +213,11 @@ $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']))); program/steps/mail/list_contacts.inc
@@ -73,8 +73,11 @@ $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 @@ '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']) . ' ' . html::span('action', '»'))), '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 @@ '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 @@ 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 ? ' ' . html::span('email', Q($email)) : '') )), 'person'); )), $classname); } } } 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; skins/classic/images/contactgroup.png
skins/classic/mail.css
@@ -1690,6 +1690,14 @@ -o-text-overflow: ellipsis; } #contacts-table td span.email { display: inline; color: #ccc; font-style: italic; margin-left: 0.5em; } #abookcountbar { margin-top: 4px; 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> 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"> 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; skins/larry/ie7hacks.css
@@ -41,6 +41,7 @@ .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 */ skins/larry/images/contactgroup.png
skins/larry/images/listicons.pngskins/larry/mail.css
@@ -1238,6 +1238,19 @@ 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; } skins/larry/styles.css
@@ -1059,6 +1059,10 @@ background-color: #e8e798; } .listbox table.listing { background-color: #d9ecf4; } table.listing, table.layout { border: 0; 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> 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" /> skins/larry/ui.js
@@ -191,6 +191,8 @@ /*** 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 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 @@ $dialog.dialog('close'); return; } // add icons to clone file input field if (rcmail.env.action == 'compose' && !$dialog.data('extended')) { $('<a>')