Merge branch 'release-0.9-ldap-groups' into release-0.9-kolab-ucs
4 files added
35 files modified
| | |
| | | // DN and password to bind as before searching for bind DN, if anonymous search is not allowed |
| | | 'search_bind_dn' => '', |
| | | 'search_bind_pw' => '', |
| | | // Optional map of replacement strings => attributes used when binding for an individual address book |
| | | 'search_bind_attrib' => array(), // e.g. array('%udc' => 'ou') |
| | | // Default for %dn variable if search doesn't return DN value |
| | | 'search_dn_default' => '', |
| | | // Optional authentication identifier to be used as SASL authorization proxy |
| | |
| | | 'phone:work' => 'telephoneNumber', |
| | | 'phone:mobile' => 'mobile', |
| | | 'phone:pager' => 'pager', |
| | | 'phone:workfax' => 'facsimileTelephoneNumber', |
| | | 'street' => 'street', |
| | | 'zipcode' => 'postalCode', |
| | | 'region' => 'st', |
| | |
| | | 'department' => 'ou', |
| | | 'jobtitle' => 'title', |
| | | 'notes' => 'description', |
| | | 'photo' => 'jpegPhoto', |
| | | // these currently don't work: |
| | | // 'phone:workfax' => 'facsimileTelephoneNumber', |
| | | // 'photo' => 'jpegPhoto', |
| | | // 'manager' => 'manager', |
| | | // 'assistant' => 'secretary', |
| | | ), |
| | |
| | | // '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)', |
| | | '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 |
| | | 'groups' => array( |
| | | 'base_dn' => '', |
| | | 'scope' => 'sub', // Search mode: sub|base|list |
| | | 'filter' => '(objectClass=groupOfNames)', |
| | | 'object_classes' => array('top', 'groupOfNames'), // Object classes to be assigned to new groups |
| | | 'member_attr' => 'member', // Name of the default member attribute, e.g. uniqueMember |
| | | 'name_attr' => 'cn', // Attribute to be used as group name |
| | | 'email_attr' => 'mail', // Group email address attribute (e.g. for mailing lists) |
| | | 'member_filter' => '(objectclass=*)', // Optional filter to use when querying for group members |
| | | 'vlv' => false, // Use VLV controls to list groups |
| | | 'class_member_attr' => array( // Mapping of group object class to member attribute used in these objects |
| | | 'groupofnames' => 'member', |
| | | 'groupofuniquenames' => 'uniquemember' |
| | | ), |
| | | ), |
| | | // 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))', |
| | | 'name_attr' => 'cn', |
| | | ), |
| | | 'customers' => array( |
| | | 'name' => 'Customers', |
| | | 'scope' => 'sub', |
| | | 'base_dn' => 'ou=Customers,dc=mydomain,dc=com', |
| | | 'filter' => '(objectClass=inetOrgPerson)', |
| | | 'name_attr' => 'sn', |
| | | ), |
| | | ), |
| | | ); |
| | | */ |
| | |
| | | // Scripts listed here will be not presented to the user. |
| | | $rcmail_config['managesieve_filename_exceptions'] = array(); |
| | | |
| | | // List of domains limiting destination emails in redirect action |
| | | // If not empty, user will need to select domain from a list |
| | | $config['managesieve_domains'] = array(); |
| | | |
| | | ?> |
| | |
| | | enabled = {}, |
| | | elems = { |
| | | mailbox: document.getElementById('action_mailbox' + id), |
| | | target: document.getElementById('action_target' + id), |
| | | target: document.getElementById('redirect_target' + id), |
| | | target_area: document.getElementById('action_target_area' + id), |
| | | flags: document.getElementById('action_flags' + id), |
| | | vacation: document.getElementById('action_vacation' + id), |
| | |
| | | $act_types = get_input_value('_action_type', RCUBE_INPUT_POST, true); |
| | | $mailboxes = get_input_value('_action_mailbox', RCUBE_INPUT_POST, true); |
| | | $act_targets = get_input_value('_action_target', RCUBE_INPUT_POST, true); |
| | | $domain_targets = get_input_value('_action_target_domain', RCUBE_INPUT_POST); |
| | | $area_targets = get_input_value('_action_target_area', RCUBE_INPUT_POST, true); |
| | | $reasons = get_input_value('_action_reason', RCUBE_INPUT_POST, true); |
| | | $addresses = get_input_value('_action_addresses', RCUBE_INPUT_POST, true); |
| | |
| | | $i = 0; |
| | | // actions |
| | | foreach($act_types as $idx => $type) { |
| | | $type = $this->strip_value($type); |
| | | $target = $this->strip_value($act_targets[$idx]); |
| | | $type = $this->strip_value($type); |
| | | |
| | | switch ($type) { |
| | | |
| | |
| | | |
| | | case 'redirect': |
| | | case 'redirect_copy': |
| | | $target = $this->strip_value($act_targets[$idx]); |
| | | $domain = $this->strip_value($domain_targets[$idx]); |
| | | |
| | | // force one of the configured domains |
| | | $domains = (array) $this->rc->config->get('managesieve_domains'); |
| | | if (!empty($domains) && !empty($target)) { |
| | | if (!$domain || !in_array($domain, $domains)) { |
| | | $domain = $domains[0]; |
| | | } |
| | | |
| | | $target .= '@' . $domain; |
| | | } |
| | | |
| | | $this->form['actions'][$i]['target'] = $target; |
| | | |
| | | if ($this->form['actions'][$i]['target'] == '') |
| | | if ($target == '') |
| | | $this->errors['actions'][$i]['target'] = $this->gettext('cannotbeempty'); |
| | | else if (!check_email($this->form['actions'][$i]['target'])) |
| | | $this->errors['actions'][$i]['target'] = $this->gettext('noemailwarning'); |
| | | else if (!rcube_utils::check_email($target)) |
| | | $this->errors['actions'][$i]['target'] = $this->plugin->gettext(!empty($domains) ? 'forbiddenchars' : 'noemailwarning'); |
| | | |
| | | if ($type == 'redirect_copy') { |
| | | $type = 'redirect'; |
| | |
| | | |
| | | // actions target inputs |
| | | $out .= '<td class="rowtargets">'; |
| | | // shared targets |
| | | $out .= '<input type="text" name="_action_target['.$id.']" id="action_target' .$id. '" ' |
| | | .'value="' .($action['type']=='redirect' ? Q($action['target'], 'strict', false) : ''). '" size="35" ' |
| | | .'style="display:' .($action['type']=='redirect' ? 'inline' : 'none') .'" ' |
| | | . $this->error_class($id, 'action', 'target', 'action_target') .' />'; |
| | | |
| | | // force domain selection in redirect email input |
| | | $domains = (array) $this->rc->config->get('managesieve_domains'); |
| | | if (!empty($domains)) { |
| | | sort($domains); |
| | | |
| | | $domain_select = new html_select(array('name' => "_action_target_domain[$id]", 'id' => 'action_target_domain'.$id)); |
| | | $domain_select->add(array_combine($domains, $domains)); |
| | | |
| | | $parts = explode('@', $action['target']); |
| | | |
| | | if (!empty($parts)) { |
| | | $action['domain'] = array_pop($parts); |
| | | $action['target'] = implode('@', $parts); |
| | | } |
| | | } |
| | | |
| | | // redirect target |
| | | $out .= '<span id="redirect_target' . $id . '" style="white-space:nowrap;' |
| | | . ' display:' . ($action['type'] == 'redirect' ? 'inline' : 'none') . '">' |
| | | . '<input type="text" name="_action_target['.$id.']" id="action_target' .$id. '"' |
| | | . ' value="' .($action['type'] == 'redirect' ? Q($action['target'], 'strict', false) : '') . '"' |
| | | . (!empty($domains) ? ' size="20"' : ' size="35"') |
| | | . $this->error_class($id, 'action', 'target', 'action_target') .' />' |
| | | . (!empty($domains) ? ' @ ' . $domain_select->show($action['domain']) : '') |
| | | . '</span>'; |
| | | |
| | | // (e)reject target |
| | | $out .= '<textarea name="_action_target_area['.$id.']" id="action_target_area' .$id. '" ' |
| | | .'rows="3" cols="35" '. $this->error_class($id, 'action', 'targetarea', 'action_target_area') |
| | | .'style="display:' .(in_array($action['type'], array('reject', 'ereject')) ? 'inline' : 'none') .'">' |
| | |
| | | $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) |
| | |
| | | $js_mailboxlist = array(); |
| | | $out = html::tag('ul', $attrib, $rcmail->render_folder_tree_html($a_mailboxes, $mbox_name, $js_mailboxlist, $attrib), html::$common_attrib); |
| | | |
| | | $rcmail->output->include_script('treelist.js'); |
| | | $rcmail->output->add_gui_object('mailboxlist', $attrib['id']); |
| | | $rcmail->output->set_env('mailboxes', $js_mailboxlist); |
| | | $rcmail->output->set_env('unreadwrap', $attrib['unreadwrap']); |
| | |
| | | 'id' => "rcmli".$folder_id, |
| | | 'class' => join(' ', $classes), |
| | | 'noclose' => true), |
| | | html::a($link_attrib, $html_name) . |
| | | (!empty($folder['folders']) ? html::div(array( |
| | | 'class' => ($is_collapsed ? 'collapsed' : 'expanded'), |
| | | 'style' => "position:absolute", |
| | | 'onclick' => sprintf("%s.command('collapse-folder', '%s')", rcmail_output::JS_OBJECT_NAME, $js_name) |
| | | ), ' ') : '')); |
| | | html::a($link_attrib, $html_name)); |
| | | |
| | | $jslist[$folder_id] = array( |
| | | if (!empty($folder['folders'])) { |
| | | $out .= html::div('treetoggle ' . ($is_collapsed ? 'collapsed' : 'expanded'), ' '); |
| | | } |
| | | |
| | | $jslist[$folder['id']] = array( |
| | | 'id' => $folder['id'], |
| | | 'name' => $foldername, |
| | | 'virtual' => $folder['virtual'] |
| | |
| | | | | |
| | | | This file is part of the Roundcube Webmail client | |
| | | | Copyright (C) 2005-2013, The Roundcube Dev Team | |
| | | | Copyright (C) 2011-2012, Kolab Systems AG | |
| | | | 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. | |
| | |
| | | } |
| | | } |
| | | 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') |
| | |
| | | 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, |
| | |
| | | this.display_message(this.pending_message[0], this.pending_message[1], this.pending_message[2]); |
| | | |
| | | // map implicit containers |
| | | if (this.gui_objects.folderlist) |
| | | if (this.gui_objects.folderlist) { |
| | | this.gui_containers.foldertray = $(this.gui_objects.folderlist); |
| | | |
| | | // init treelist widget |
| | | if (window.rcube_treelist_widget) { |
| | | this.treelist = new rcube_treelist_widget(this.gui_objects.folderlist, { |
| | | id_prefix: 'rcmli', |
| | | id_encode: this.html_identifier_encode, |
| | | id_decode: this.html_identifier_decode, |
| | | check_droptarget: function(node){ return !node.virtual && ref.check_droptarget(node.id) } |
| | | }); |
| | | this.treelist.addEventListener('collapse', function(node){ ref.folder_collapsed(node) }); |
| | | this.treelist.addEventListener('expand', function(node){ ref.folder_collapsed(node) }); |
| | | } |
| | | } |
| | | |
| | | // activate html5 file drop feature (if browser supports it and if configured) |
| | | if (this.gui_objects.filedrop && this.env.filedrop && ((window.XMLHttpRequest && XMLHttpRequest.prototype && XMLHttpRequest.prototype.sendAsBinary) || window.FormData)) { |
| | |
| | | case 'moveto': |
| | | if (this.task == 'mail') |
| | | this.move_messages(props); |
| | | else if (this.task == 'addressbook' && this.drag_active) |
| | | else if (this.task == 'addressbook') |
| | | this.copy_contact(null, props); |
| | | break; |
| | | |
| | |
| | | } |
| | | 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': |
| | |
| | | |
| | | this.html_identifier = function(str, encode) |
| | | { |
| | | str = String(str); |
| | | if (encode) |
| | | return Base64.encode(str).replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_'); |
| | | else |
| | | return str.replace(this.identifier_expr, '_'); |
| | | return encode ? this.html_identifier_encode(str) : String(str).replace(this.identifier_expr, '_'); |
| | | }; |
| | | |
| | | this.html_identifier_encode = function(str) |
| | | { |
| | | return Base64.encode(String(str)).replace(/=+$/, '').replace(/\+/g, '-').replace(/\//g, '_'); |
| | | }; |
| | | |
| | | this.html_identifier_decode = function(str) |
| | |
| | | if (this.preview_read_timer) |
| | | clearTimeout(this.preview_read_timer); |
| | | |
| | | // save folderlist and folders location/sizes for droptarget calculation in drag_move() |
| | | if (this.gui_objects.folderlist && model) { |
| | | this.initialBodyScrollTop = bw.ie ? 0 : window.pageYOffset; |
| | | this.initialListScrollTop = this.gui_objects.folderlist.parentNode.scrollTop; |
| | | |
| | | var k, li, height, |
| | | list = $(this.gui_objects.folderlist); |
| | | pos = list.offset(); |
| | | |
| | | this.env.folderlist_coords = { x1:pos.left, y1:pos.top, x2:pos.left + list.width(), y2:pos.top + list.height() }; |
| | | |
| | | this.env.folder_coords = []; |
| | | for (k in model) { |
| | | if (li = this.get_folder_li(k)) { |
| | | // only visible folders |
| | | if (height = li.firstChild.offsetHeight) { |
| | | pos = $(li.firstChild).offset(); |
| | | this.env.folder_coords[k] = { x1:pos.left, y1:pos.top, |
| | | x2:pos.left + li.firstChild.offsetWidth, y2:pos.top + height, on:0 }; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | // prepare treelist widget for dragging interactions |
| | | if (this.treelist) |
| | | this.treelist.drag_start(); |
| | | }; |
| | | |
| | | this.drag_end = function(e) |
| | |
| | | this.drag_active = false; |
| | | this.env.last_folder_target = null; |
| | | |
| | | if (this.folder_auto_timer) { |
| | | clearTimeout(this.folder_auto_timer); |
| | | this.folder_auto_timer = null; |
| | | this.folder_auto_expand = null; |
| | | } |
| | | |
| | | // over the folders |
| | | if (this.gui_objects.folderlist && this.env.folder_coords) { |
| | | for (var k in this.env.folder_coords) { |
| | | if (this.env.folder_coords[k].on) |
| | | $(this.get_folder_li(k)).removeClass('droptarget'); |
| | | } |
| | | } |
| | | if (this.treelist) |
| | | this.treelist.drag_end(); |
| | | }; |
| | | |
| | | this.drag_move = function(e) |
| | | { |
| | | if (this.gui_objects.folderlist && this.env.folder_coords) { |
| | | var k, li, div, check, oldclass, |
| | | if (this.gui_objects.folderlist) { |
| | | var drag_target, oldclass, |
| | | layerclass = 'draglayernormal', |
| | | mouse = rcube_event.get_mouse_pos(e), |
| | | pos = this.env.folderlist_coords, |
| | | // offsets to compensate for scrolling while dragging a message |
| | | boffset = bw.ie ? -document.documentElement.scrollTop : this.initialBodyScrollTop, |
| | | moffset = this.initialListScrollTop-this.gui_objects.folderlist.parentNode.scrollTop; |
| | | mouse = rcube_event.get_mouse_pos(e); |
| | | |
| | | if (this.contact_list && this.contact_list.draglayer) |
| | | oldclass = this.contact_list.draglayer.attr('class'); |
| | | |
| | | mouse.y += -moffset-boffset; |
| | | |
| | | // if mouse pointer is outside of folderlist |
| | | if (mouse.x < pos.x1 || mouse.x >= pos.x2 || mouse.y < pos.y1 || mouse.y >= pos.y2) { |
| | | if (this.env.last_folder_target) { |
| | | $(this.get_folder_li(this.env.last_folder_target)).removeClass('droptarget'); |
| | | this.env.folder_coords[this.env.last_folder_target].on = 0; |
| | | this.env.last_folder_target = null; |
| | | } |
| | | if (layerclass != oldclass && this.contact_list && this.contact_list.draglayer) |
| | | this.contact_list.draglayer.attr('class', layerclass); |
| | | return; |
| | | // mouse intersects a valid drop target on the treelist |
| | | if (this.treelist && (drag_target = this.treelist.intersects(mouse, true))) { |
| | | this.env.last_folder_target = drag_target; |
| | | layerclass = 'draglayer' + (this.check_droptarget(drag_target) > 1 ? 'copy' : 'normal'); |
| | | } |
| | | |
| | | // over the folders |
| | | for (k in this.env.folder_coords) { |
| | | pos = this.env.folder_coords[k]; |
| | | if (mouse.x >= pos.x1 && mouse.x < pos.x2 && mouse.y >= pos.y1 && mouse.y < pos.y2) { |
| | | if (check = this.check_droptarget(k)) { |
| | | li = this.get_folder_li(k); |
| | | div = $(li.getElementsByTagName('div')[0]); |
| | | |
| | | // if the folder is collapsed, expand it after 1sec and restart the drag & drop process. |
| | | if (div.hasClass('collapsed')) { |
| | | if (this.folder_auto_timer) |
| | | clearTimeout(this.folder_auto_timer); |
| | | |
| | | this.folder_auto_expand = this.env.mailboxes[k].id; |
| | | this.folder_auto_timer = setTimeout(function() { |
| | | rcmail.command('collapse-folder', rcmail.folder_auto_expand); |
| | | rcmail.drag_start(null); |
| | | }, 1000); |
| | | } |
| | | else if (this.folder_auto_timer) { |
| | | clearTimeout(this.folder_auto_timer); |
| | | this.folder_auto_timer = null; |
| | | this.folder_auto_expand = null; |
| | | } |
| | | |
| | | $(li).addClass('droptarget'); |
| | | this.env.folder_coords[k].on = 1; |
| | | this.env.last_folder_target = k; |
| | | layerclass = 'draglayer' + (check > 1 ? 'copy' : 'normal'); |
| | | } |
| | | // Clear target, otherwise drag end will trigger move into last valid droptarget |
| | | else |
| | | this.env.last_folder_target = null; |
| | | } |
| | | else if (pos.on) { |
| | | $(this.get_folder_li(k)).removeClass('droptarget'); |
| | | this.env.folder_coords[k].on = 0; |
| | | } |
| | | else { |
| | | // Clear target, otherwise drag end will trigger move into last valid droptarget |
| | | this.env.last_folder_target = null; |
| | | } |
| | | |
| | | if (layerclass != oldclass && this.contact_list && this.contact_list.draglayer) |
| | |
| | | |
| | | this.collapse_folder = function(name) |
| | | { |
| | | var li = this.get_folder_li(name, '', true), |
| | | div = $('div:first', li), |
| | | ul = $('ul:first', li); |
| | | if (this.treelist) |
| | | this.treelist.toggle(name); |
| | | }; |
| | | |
| | | if (div.hasClass('collapsed')) { |
| | | ul.show(); |
| | | div.removeClass('collapsed').addClass('expanded'); |
| | | var reg = new RegExp('&'+urlencode(name)+'&'); |
| | | this.env.collapsed_folders = this.env.collapsed_folders.replace(reg, ''); |
| | | } |
| | | else if (div.hasClass('expanded')) { |
| | | ul.hide(); |
| | | div.removeClass('expanded').addClass('collapsed'); |
| | | this.env.collapsed_folders = this.env.collapsed_folders+'&'+urlencode(name)+'&'; |
| | | this.folder_collapsed = function(node) |
| | | { |
| | | var prefname = this.env.task == 'addressbook' ? 'collapsed_abooks' : 'collapsed_folders'; |
| | | |
| | | if (node.collapsed) { |
| | | this.env[prefname] = this.env[prefname] + '&'+urlencode(node.id)+'&'; |
| | | |
| | | // select the folder if one of its childs is currently selected |
| | | // don't select if it's virtual (#1488346) |
| | | if (this.env.mailbox.indexOf(name + this.env.delimiter) == 0 && !$(li).hasClass('virtual')) |
| | | if (this.env.mailbox && this.env.mailbox.indexOf(name + this.env.delimiter) == 0 && !node.virtual) |
| | | this.command('list', name); |
| | | } |
| | | else |
| | | return; |
| | | |
| | | // Work around a bug in IE6 and IE7, see #1485309 |
| | | if (bw.ie6 || bw.ie7) { |
| | | var siblings = li.nextSibling ? li.nextSibling.getElementsByTagName('ul') : null; |
| | | if (siblings && siblings.length && (li = siblings[0]) && li.style && li.style.display != 'none') { |
| | | li.style.display = 'none'; |
| | | li.style.display = ''; |
| | | } |
| | | else { |
| | | var reg = new RegExp('&'+urlencode(node.id)+'&'); |
| | | this.env[prefname] = this.env[prefname].replace(reg, ''); |
| | | } |
| | | |
| | | this.command('save-pref', { name: 'collapsed_folders', value: this.env.collapsed_folders }); |
| | | this.set_unread_count_display(name, false); |
| | | if (!this.drag_active) { |
| | | this.command('save-pref', { name: prefname, value: this.env[prefname] }); |
| | | |
| | | if (this.env.unread_counts) |
| | | this.set_unread_count_display(node.id, false); |
| | | } |
| | | }; |
| | | |
| | | this.doc_mouse_up = function(e) |
| | |
| | | if (this.drag_active && model && this.env.last_folder_target) { |
| | | var target = model[this.env.last_folder_target]; |
| | | |
| | | $(this.get_folder_li(this.env.last_folder_target)).removeClass('droptarget'); |
| | | this.env.last_folder_target = null; |
| | | list.draglayer.hide(); |
| | | this.drag_end(e); |
| | | |
| | | if (!this.drag_menu(e, target)) |
| | | this.command('moveto', target); |
| | |
| | | |
| | | 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) |
| | |
| | | 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 |
| | |
| | | this.show_contentframe(false); |
| | | |
| | | if (list.selection.length) { |
| | | list.draggable = false; |
| | | |
| | | // no source = search result, we'll need to detect if any of |
| | | // selected contacts are in writable addressbook to enable edit/delete |
| | | // 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('edit', id && writable); |
| | | this.enable_command('delete', list.selection.length && writable); |
| | | this.enable_command('delete', list.selection.length > 0 && writable); |
| | | |
| | | return false; |
| | | }; |
| | |
| | | else if (!this.env.search_request) |
| | | folder = group ? 'G'+src+group : src; |
| | | |
| | | this.select_folder(folder); |
| | | |
| | | this.env.source = src; |
| | | this.env.group = group; |
| | | |
| | | // truncate groups listing stack |
| | | var index = $.inArray(this.env.group, this.env.address_group_stack); |
| | | if (index < 0) |
| | | this.env.address_group_stack = []; |
| | | else |
| | | this.env.address_group_stack = this.env.address_group_stack.slice(0,index); |
| | | |
| | | // make sure the current group is on top of the stack |
| | | if (this.env.group) { |
| | | this.env.address_group_stack.push(this.env.group); |
| | | |
| | | // mark the first group on the stack as selected in the directory list |
| | | folder = 'G'+src+this.env.address_group_stack[0]; |
| | | } |
| | | else if (this.gui_objects.addresslist_title) { |
| | | $(this.gui_objects.addresslist_title).html(this.get_label('contacts')); |
| | | } |
| | | |
| | | this.select_folder(folder, '', true); |
| | | |
| | | // load contacts remotely |
| | | if (this.gui_objects.contactslist) { |
| | |
| | | |
| | | this.list_contacts_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; |
| | |
| | | if (!cid) { |
| | | // unselect selected row(s) |
| | | this.contact_list.clear_selection(); |
| | | this.enable_command('delete', 'compose', false); |
| | | |
| | | this.enable_command('compose', rec && rec.email); |
| | | this.enable_command('delete', rec && rec._type != 'group'); |
| | | } |
| | | } |
| | | else if (framed) |
| | |
| | | }; |
| | | |
| | | // 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; |
| | | |
| | |
| | | list.selection[0] = newcid; |
| | | row.style.display = ''; |
| | | } |
| | | |
| | | 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; |
| | |
| | | row.appendChild(col); |
| | | } |
| | | |
| | | // store data in list member |
| | | list.data[cid] = data; |
| | | list.insert_row(row); |
| | | |
| | | this.enable_command('export', list.rowcount > 0); |
| | |
| | | this.name_input.bind('keydown', function(e){ return rcmail.add_input_keydown(e); }); |
| | | this.env.group_renaming = true; |
| | | |
| | | var link, li = this.get_folder_li(this.env.source+this.env.group, 'rcmliG'); |
| | | var link, li = this.get_folder_li('G'+this.env.source+this.env.group,'',true); |
| | | if (li && (link = li.firstChild)) { |
| | | $(link).hide().before(this.name_input); |
| | | } |
| | |
| | | this.remove_group_item = function(prop) |
| | | { |
| | | var li, key = 'G'+prop.source+prop.id; |
| | | if ((li = this.get_folder_li(key))) { |
| | | if ((li = this.get_folder_li(key,'',true))) { |
| | | this.triggerEvent('group_delete', { source:prop.source, id:prop.id, li:li }); |
| | | |
| | | li.parentNode.removeChild(li); |
| | |
| | | this.name_input.bind('keydown', function(e){ return rcmail.add_input_keydown(e); }); |
| | | this.name_input_li = $('<li>').addClass(type).append(this.name_input); |
| | | |
| | | var li = type == 'contactsearch' ? $('li:last', this.gui_objects.folderlist) : this.get_folder_li(this.env.source); |
| | | this.name_input_li.insertAfter(li); |
| | | var ul, li; |
| | | |
| | | // find list (UL) element |
| | | if (type == 'contactsearch') |
| | | ul = this.gui_objects.folderlist; |
| | | else |
| | | ul = $('ul.groups', this.get_folder_li(this.env.source,'',true)); |
| | | |
| | | // append to the list |
| | | li = $('li:last', ul); |
| | | if (li.length) |
| | | this.name_input_li.insertAfter(li); |
| | | else { |
| | | this.name_input_li.appendTo(ul); |
| | | ul.show(); // make sure the list is visible |
| | | } |
| | | } |
| | | |
| | | this.name_input.select().focus(); |
| | |
| | | this.reset_add_input = function() |
| | | { |
| | | if (this.name_input) { |
| | | var li = this.name_input.parent(); |
| | | if (this.env.group_renaming) { |
| | | var li = this.name_input.parent(); |
| | | li.children().last().show(); |
| | | this.env.group_renaming = false; |
| | | } |
| | | else if ($('li', li.parent()).length == 1) |
| | | li.parent().hide(); |
| | | |
| | | this.name_input.remove(); |
| | | |
| | |
| | | this.reset_add_input(); |
| | | |
| | | var key = 'G'+prop.source+prop.id, |
| | | li = this.get_folder_li(key), |
| | | li = this.get_folder_li(key,'',true), |
| | | link; |
| | | |
| | | // group ID has changed, replace link node and identifiers |
| | |
| | | this.add_contact_group_row = function(prop, li, reloc) |
| | | { |
| | | var row, name = prop.name.toUpperCase(), |
| | | sibling = this.get_folder_li(prop.source), |
| | | prefix = 'rcmliG' + this.html_identifier(prop.source); |
| | | sibling = this.get_folder_li(prop.source,'',true), |
| | | prefix = 'rcmli' + this.html_identifier('G'+prop.source, true); |
| | | |
| | | // When renaming groups, we need to remove it from DOM and insert it in the proper place |
| | | if (reloc) { |
| | |
| | | if (++colprop.count == colprop.limit && colprop.limit) |
| | | $(menu).children('option[value="'+col+'"]').prop('disabled', true); |
| | | } |
| | | |
| | | if (contact._type != 'group') |
| | | list.draggable = true; |
| | | } |
| | | } |
| | | }; |
| | |
| | | .attr('rel', id) |
| | | .click(function() { return rcmail.command('listsearch', id, this); }) |
| | | .html(name), |
| | | li = $('<li>').attr({id: 'rcmli' + this.html_identifier(key), 'class': 'contactsearch'}) |
| | | li = $('<li>').attr({ id:'rcmli' + this.html_identifier(key,true), 'class':'contactsearch' }) |
| | | .append(link), |
| | | prop = {name:name, id:id, li:li[0]}; |
| | | |
| | | this.add_saved_search_row(prop, li); |
| | | this.select_folder('S'+id); |
| | | this.select_folder(key,'',true); |
| | | this.enable_command('search-delete', true); |
| | | this.env.search_id = id; |
| | | |
| | |
| | | this.remove_search_item = function(id) |
| | | { |
| | | var li, key = 'S'+id; |
| | | if ((li = this.get_folder_li(key))) { |
| | | if ((li = this.get_folder_li(key,'',true))) { |
| | | this.triggerEvent('search_delete', { id:id, li:li }); |
| | | |
| | | li.parentNode.removeChild(li); |
| | |
| | | } |
| | | |
| | | this.reset_qsearch(); |
| | | this.select_folder('S'+id); |
| | | this.select_folder('S'+id, '', true); |
| | | |
| | | // reset vars |
| | | this.env.current_page = 1; |
New file |
| | |
| | | /* |
| | | +-----------------------------------------------------------------------+ |
| | | | Roundcube Treelist widget | |
| | | | | |
| | | | This file is part of the Roundcube Webmail client | |
| | | | Copyright (C) 2013, The Roundcube Dev Team | |
| | | | | |
| | | | 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. | |
| | | | | |
| | | +-----------------------------------------------------------------------+ |
| | | | Authors: Thomas Bruederli <roundcube@gmail.com> | |
| | | +-----------------------------------------------------------------------+ |
| | | | Requires: common.js | |
| | | +-----------------------------------------------------------------------+ |
| | | */ |
| | | |
| | | |
| | | /** |
| | | * Roundcube Treelist widget class |
| | | * @contructor |
| | | */ |
| | | function rcube_treelist_widget(node, p) |
| | | { |
| | | // apply some defaults to p |
| | | p = $.extend({ |
| | | id_prefix: '', |
| | | autoexpand: 1000, |
| | | selectable: false, |
| | | check_droptarget: function(node){ return !node.virtual } |
| | | }, p || {}); |
| | | |
| | | var container = $(node), |
| | | data = p.data || [], |
| | | indexbyid = {}, |
| | | selection = null, |
| | | drag_active = false, |
| | | box_coords = {}, |
| | | item_coords = [], |
| | | autoexpand_timer, |
| | | autoexpand_item, |
| | | body_scroll_top = 0, |
| | | list_scroll_top = 0, |
| | | me = this; |
| | | |
| | | |
| | | /////// export public members and methods |
| | | |
| | | this.container = container; |
| | | this.expand = expand; |
| | | this.collapse = collapse; |
| | | this.select = select; |
| | | this.render = render; |
| | | this.drag_start = drag_start; |
| | | this.drag_end = drag_end; |
| | | this.intersects = intersects; |
| | | this.update = update_node; |
| | | this.insert = insert; |
| | | this.remove = remove; |
| | | this.get_item = get_item; |
| | | this.get_selection = get_selection; |
| | | |
| | | /////// startup code (constructor) |
| | | |
| | | // abort if node not found |
| | | if (!container.length) |
| | | return; |
| | | |
| | | if (p.data) |
| | | index_data({ children:data }); |
| | | // load data from DOM |
| | | else |
| | | update_data(); |
| | | |
| | | // register click handlers on list |
| | | container.on('click', 'div.treetoggle', function(e){ |
| | | toggle(dom2id($(this).parent())); |
| | | }); |
| | | |
| | | container.on('click', 'li', function(e){ |
| | | var node = p.selectable ? indexbyid[dom2id($(this))] : null; |
| | | if (node && !node.virtual) { |
| | | select(node.id); |
| | | e.stopPropagation(); |
| | | } |
| | | }); |
| | | |
| | | |
| | | /////// private methods |
| | | |
| | | /** |
| | | * Collaps a the node with the given ID |
| | | */ |
| | | function collapse(id, recursive, set) |
| | | { |
| | | var node; |
| | | |
| | | if (node = indexbyid[id]) { |
| | | node.collapsed = typeof set == 'undefined' || set; |
| | | update_dom(node); |
| | | |
| | | // Work around a bug in IE6 and IE7, see #1485309 |
| | | if (window.bw && (bw.ie6 || bw.ie7) && node.collapsed) { |
| | | id2dom(node.id).next().children('ul:visible').hide().show(); |
| | | } |
| | | |
| | | if (recursive && node.children) { |
| | | for (var i=0; i < node.children.length; i++) { |
| | | collapse(node.children[i].id, recursive, set); |
| | | } |
| | | } |
| | | |
| | | me.triggerEvent(node.collapsed ? 'collapse' : 'expand', node); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Expand a the node with the given ID |
| | | */ |
| | | function expand(id, recursive) |
| | | { |
| | | collapse(id, recursive, false); |
| | | } |
| | | |
| | | /** |
| | | * Toggle collapsed state of a list node |
| | | */ |
| | | function toggle(id, recursive) |
| | | { |
| | | var node; |
| | | if (node = indexbyid[id]) { |
| | | collapse(id, recursive, !node.collapsed); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Select a tree node by it's ID |
| | | */ |
| | | function select(id) |
| | | { |
| | | if (selection) { |
| | | id2dom(selection).removeClass('selected'); |
| | | selection = null; |
| | | } |
| | | |
| | | var li = id2dom(id); |
| | | if (li.length) { |
| | | li.addClass('selected'); |
| | | selection = id; |
| | | // TODO: expand all parent nodes if collapsed |
| | | scroll_to_node(li); |
| | | } |
| | | |
| | | me.triggerEvent('select', indexbyid[id]); |
| | | } |
| | | |
| | | /** |
| | | * Getter for the currently selected node ID |
| | | */ |
| | | function get_selection() |
| | | { |
| | | return selection; |
| | | } |
| | | |
| | | /** |
| | | * Return the DOM element of the list item with the given ID |
| | | */ |
| | | function get_item(id) |
| | | { |
| | | return id2dom(id).get(0); |
| | | } |
| | | |
| | | /** |
| | | * Insert the given node |
| | | */ |
| | | function insert(node, parent_id, sort) |
| | | { |
| | | var li, parent_li, |
| | | parent_node = parent_id ? indexbyid[parent_id] : null; |
| | | |
| | | // insert as child of an existing node |
| | | if (parent_node) { |
| | | if (!parent_node.children) |
| | | parent_node.children = []; |
| | | |
| | | parent_node.children.push(node); |
| | | parent_li = id2dom(parent_id); |
| | | |
| | | // re-render the entire subtree |
| | | if (parent_node.children.length == 1) { |
| | | render_node(parent_node, parent_li.parent(), parent_li); |
| | | li = id2dom(node.id); |
| | | } |
| | | else { |
| | | // append new node to parent's child list |
| | | li = render_node(node, parent_li.children('ul').first()); |
| | | } |
| | | } |
| | | // insert at top level |
| | | else { |
| | | data.push(node); |
| | | li = render_node(node, container); |
| | | } |
| | | |
| | | indexbyid[node.id] = node; |
| | | |
| | | if (sort) { |
| | | resort_node(li, typeof sort == 'string' ? '[class~="' + sort + '"]' : ''); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Update properties of an existing node |
| | | */ |
| | | function update_node(id, updates, sort) |
| | | { |
| | | var li, node = indexbyid[id]; |
| | | |
| | | if (node) { |
| | | li = id2dom(id); |
| | | |
| | | if (updates.id || updates.html || updates.children || updates.classes) { |
| | | $.extend(node, updates); |
| | | render_node(node, li.parent(), li); |
| | | } |
| | | |
| | | if (node.id != id) { |
| | | delete indexbyid[id]; |
| | | indexbyid[node.id] = node; |
| | | } |
| | | |
| | | if (sort) { |
| | | resort_node(li, typeof sort == 'string' ? '[class~="' + sort + '"]' : ''); |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Helper method to sort the list of the given item |
| | | */ |
| | | function resort_node(li, filter) |
| | | { |
| | | var first, sibling, |
| | | myid = li.get(0).id, |
| | | sortname = li.children().first().text().toUpperCase(); |
| | | |
| | | li.parent().children('li' + filter).each(function(i, elem) { |
| | | if (i == 0) |
| | | first = elem; |
| | | if (elem.id == myid) { |
| | | // skip |
| | | } |
| | | else if (elem.id != myid && sortname >= $(elem).children().first().text().toUpperCase()) { |
| | | sibling = elem; |
| | | } |
| | | else { |
| | | return false; |
| | | } |
| | | }); |
| | | |
| | | if (sibling) { |
| | | li.insertAfter(sibling); |
| | | } |
| | | else if (first.id != myid) { |
| | | li.insertBefore(first); |
| | | } |
| | | |
| | | // reload data from dom |
| | | update_data(); |
| | | } |
| | | |
| | | /** |
| | | * Remove the item with the given ID |
| | | */ |
| | | function remove(id) |
| | | { |
| | | var node, li; |
| | | |
| | | if (node = indexbyid[id]) { |
| | | li = id2dom(id); |
| | | li.remove(); |
| | | |
| | | node.deleted = true; |
| | | delete indexbyid[id]; |
| | | |
| | | return true; |
| | | } |
| | | |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * (Re-)read tree data from DOM |
| | | */ |
| | | function update_data() |
| | | { |
| | | data = walk_list(container); |
| | | } |
| | | |
| | | /** |
| | | * Apply the 'collapsed' status of the data node to the corresponding DOM element(s) |
| | | */ |
| | | function update_dom(node) |
| | | { |
| | | var li = id2dom(node.id); |
| | | li.children('ul').first()[(node.collapsed ? 'hide' : 'show')](); |
| | | li.children('div.treetoggle').removeClass('collapsed expanded').addClass(node.collapsed ? 'collapsed' : 'expanded'); |
| | | me.triggerEvent('toggle', node); |
| | | } |
| | | |
| | | /** |
| | | * Render the tree list from the internal data structure |
| | | */ |
| | | function render() |
| | | { |
| | | if (me.triggerEvent('renderBefore', data) === false) |
| | | return; |
| | | |
| | | // remove all child nodes |
| | | container.html(''); |
| | | |
| | | // render child nodes |
| | | for (var i=0; i < data.length; i++) { |
| | | render_node(data[i], container); |
| | | } |
| | | |
| | | me.triggerEvent('renderAfter', container); |
| | | } |
| | | |
| | | /** |
| | | * Render a specific node into the DOM list |
| | | */ |
| | | function render_node(node, parent, replace) |
| | | { |
| | | if (node.deleted) |
| | | return; |
| | | |
| | | var li = $('<li>') |
| | | .attr('id', p.id_prefix + (p.id_encode ? p.id_encode(node.id) : node.id)) |
| | | .addClass((node.classes || []).join(' ')); |
| | | |
| | | if (replace) |
| | | replace.replaceWith(li); |
| | | else |
| | | li.appendTo(parent); |
| | | |
| | | if (typeof node.html == 'string') |
| | | li.html(node.html); |
| | | else if (typeof node.html == 'object') |
| | | li.append(node.html); |
| | | |
| | | if (node.virtual) |
| | | li.addClass('virtual'); |
| | | if (node.id == selection) |
| | | li.addClass('selected'); |
| | | |
| | | // add child list and toggle icon |
| | | if (node.children && node.children.length) { |
| | | $('<div class="treetoggle '+(node.collapsed ? 'collapsed' : 'expanded') + '"> </div>').appendTo(li); |
| | | var ul = $('<ul>').appendTo(li).attr('class', node.childlistclass); |
| | | if (node.collapsed) |
| | | ul.hide(); |
| | | |
| | | for (var i=0; i < node.children.length; i++) { |
| | | render_node(node.children[i], ul); |
| | | } |
| | | } |
| | | |
| | | return li; |
| | | } |
| | | |
| | | /** |
| | | * Recursively walk the DOM tree and build an internal data structure |
| | | * representing the skeleton of this tree list. |
| | | */ |
| | | function walk_list(ul) |
| | | { |
| | | var result = []; |
| | | ul.children('li').each(function(i,e){ |
| | | var li = $(e), sublist = li.children('ul'); |
| | | var node = { |
| | | id: dom2id(li), |
| | | classes: li.attr('class').split(' '), |
| | | virtual: li.hasClass('virtual'), |
| | | html: li.children().first().get(0).outerHTML, |
| | | children: walk_list(sublist) |
| | | } |
| | | |
| | | if (sublist.length) { |
| | | node.childlistclass = sublist.attr('class'); |
| | | } |
| | | if (node.children.length) { |
| | | node.collapsed = sublist.css('display') == 'none'; |
| | | } |
| | | if (li.hasClass('selected')) { |
| | | selection = node.id; |
| | | } |
| | | |
| | | result.push(node); |
| | | indexbyid[node.id] = node; |
| | | }) |
| | | |
| | | return result; |
| | | } |
| | | |
| | | /** |
| | | * Recursively walk the data tree and index nodes by their ID |
| | | */ |
| | | function index_data(node) |
| | | { |
| | | if (node.id) { |
| | | indexbyid[node.id] = node; |
| | | } |
| | | for (var c=0; node.children && c < node.children.length; c++) { |
| | | index_data(node.children[c]); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Get the (stripped) node ID from the given DOM element |
| | | */ |
| | | function dom2id(li) |
| | | { |
| | | var domid = li.attr('id').replace(new RegExp('^' + (p.id_prefix) || '%'), ''); |
| | | return p.id_decode ? p.id_decode(domid) : domid; |
| | | } |
| | | |
| | | /** |
| | | * Get the <li> element for the given node ID |
| | | */ |
| | | function id2dom(id) |
| | | { |
| | | var domid = p.id_encode ? p.id_encode(id) : id; |
| | | return $('#' + p.id_prefix + domid); |
| | | } |
| | | |
| | | /** |
| | | * Scroll the parent container to make the given list item visible |
| | | */ |
| | | function scroll_to_node(li) |
| | | { |
| | | var scroller = container.parent(), |
| | | current_offset = scroller.scrollTop(), |
| | | rel_offset = li.offset().top - scroller.offset().top; |
| | | |
| | | if (rel_offset < 0 || rel_offset + li.height() > scroller.height()) |
| | | scroller.scrollTop(rel_offset + current_offset); |
| | | } |
| | | |
| | | ///// drag & drop support |
| | | |
| | | /** |
| | | * When dragging starts, compute absolute bounding boxes of the list and it's items |
| | | * for faster comparisons while mouse is moving |
| | | */ |
| | | function drag_start() |
| | | { |
| | | var li, item, height, |
| | | pos = container.offset(); |
| | | |
| | | body_scroll_top = bw.ie ? 0 : window.pageYOffset; |
| | | list_scroll_top = container.parent().scrollTop(); |
| | | |
| | | drag_active = true; |
| | | box_coords = { |
| | | x1: pos.left, |
| | | y1: pos.top, |
| | | x2: pos.left + container.width(), |
| | | y2: pos.top + container.height() |
| | | }; |
| | | |
| | | item_coords = []; |
| | | for (var id in indexbyid) { |
| | | li = id2dom(id); |
| | | item = li.children().first().get(0); |
| | | if (height = item.offsetHeight) { |
| | | pos = $(item).offset(); |
| | | item_coords[id] = { |
| | | x1: pos.left, |
| | | y1: pos.top, |
| | | x2: pos.left + item.offsetWidth, |
| | | y2: pos.top + height, |
| | | on: id == autoexpand_item |
| | | }; |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Signal that dragging has stopped |
| | | */ |
| | | function drag_end() |
| | | { |
| | | drag_active = false; |
| | | |
| | | if (autoexpand_timer) { |
| | | clearTimeout(autoexpand_timer); |
| | | autoexpand_timer = null; |
| | | autoexpand_item = null; |
| | | } |
| | | |
| | | $('li.droptarget', container).removeClass('droptarget'); |
| | | } |
| | | |
| | | /** |
| | | * Determine if the given mouse coords intersect the list and one if its items |
| | | */ |
| | | function intersects(mouse, highlight) |
| | | { |
| | | // offsets to compensate for scrolling while dragging a message |
| | | var boffset = bw.ie ? -document.documentElement.scrollTop : body_scroll_top, |
| | | moffset = list_scroll_top - container.parent().scrollTop(), |
| | | result = null; |
| | | |
| | | mouse.top = mouse.y + -moffset - boffset; |
| | | |
| | | // no intersection with list bounding box |
| | | if (mouse.x < box_coords.x1 || mouse.x >= box_coords.x2 || mouse.top < box_coords.y1 || mouse.top >= box_coords.y2) { |
| | | // TODO: optimize performance for this operation |
| | | $('li.droptarget', container).removeClass('droptarget'); |
| | | return result; |
| | | } |
| | | |
| | | // check intersection with visible list items |
| | | var pos, node; |
| | | for (var id in item_coords) { |
| | | pos = item_coords[id]; |
| | | if (mouse.x >= pos.x1 && mouse.x < pos.x2 && mouse.top >= pos.y1 && mouse.top < pos.y2) { |
| | | node = indexbyid[id]; |
| | | |
| | | // if the folder is collapsed, expand it after the configured time |
| | | if (node.children && node.children.length && node.collapsed && p.autoexpand && autoexpand_item != id) { |
| | | if (autoexpand_timer) |
| | | clearTimeout(autoexpand_timer); |
| | | |
| | | autoexpand_item = id; |
| | | autoexpand_timer = setTimeout(function() { |
| | | expand(autoexpand_item); |
| | | drag_start(); // re-calculate item coords |
| | | autoexpand_item = null; |
| | | }, p.autoexpand); |
| | | } |
| | | else if (autoexpand_timer && autoexpand_item != id) { |
| | | clearTimeout(autoexpand_timer); |
| | | autoexpand_item = null; |
| | | autoexpand_timer = null; |
| | | } |
| | | |
| | | // check if this item is accepted as drop target |
| | | if (p.check_droptarget(node)) { |
| | | if (highlight) { |
| | | id2dom(id).addClass('droptarget'); |
| | | pos.on = true; |
| | | } |
| | | result = id; |
| | | } |
| | | else { |
| | | result = null; |
| | | } |
| | | } |
| | | else if (pos.on) { |
| | | id2dom(id).removeClass('droptarget'); |
| | | pos.on = false; |
| | | } |
| | | } |
| | | |
| | | return result; |
| | | } |
| | | } |
| | | |
| | | // use event processing functions from Roundcube's rcube_event_engine |
| | | rcube_treelist_widget.prototype.addEventListener = rcube_event_engine.prototype.addEventListener; |
| | | rcube_treelist_widget.prototype.removeEventListener = rcube_event_engine.prototype.removeEventListener; |
| | | rcube_treelist_widget.prototype.triggerEvent = rcube_event_engine.prototype.triggerEvent; |
| | |
| | | */ |
| | | public function get($name, $def = null) |
| | | { |
| | | if (isset($this->prop[$name])) { |
| | | if (array_key_exists($name, $this->prop)) { |
| | | $result = $this->prop[$name]; |
| | | } |
| | | else { |
| | |
| | | /* |
| | | +-----------------------------------------------------------------------+ |
| | | | 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. | |
| | |
| | | public $coltypes = array(); |
| | | |
| | | /** private properties */ |
| | | protected $conn; |
| | | protected $ldap; |
| | | protected $prop = array(); |
| | | protected $fieldmap = array(); |
| | | protected $sub_filter; |
| | |
| | | private $group_url = null; |
| | | private $cache; |
| | | |
| | | private $vlv_active = false; |
| | | private $vlv_count = 0; |
| | | |
| | | |
| | | /** |
| | | * Object constructor |
| | |
| | | function __construct($p, $debug = false, $mail_domain = null) |
| | | { |
| | | $this->prop = $p; |
| | | |
| | | $fetch_attributes = array('objectClass'); |
| | | |
| | | if (isset($p['searchonly'])) |
| | | $this->searchonly = $p['searchonly']; |
| | |
| | | $this->prop['groups']['name_attr'] = 'cn'; |
| | | if (empty($this->prop['groups']['scope'])) |
| | | $this->prop['groups']['scope'] = 'sub'; |
| | | // set default group objectclass => member attribute mapping |
| | | if (empty($this->prop['groups']['class_member_attr'])) { |
| | | $this->prop['groups']['class_member_attr'] = array( |
| | | 'group' => 'member', |
| | | 'groupofnames' => 'member', |
| | | 'kolabgroupofnames' => 'member', |
| | | 'groupofuniquenames' => 'uniquemember', |
| | | 'kolabgroupofuniquenames' => 'uniquemember', |
| | | ); |
| | | } |
| | | |
| | | // add group name attrib to the list of attributes to be fetched |
| | | $fetch_attributes[] = $this->prop['groups']['name_attr']; |
| | | } |
| | | if (is_array($p['group_filters']) && count($p['group_filters'])) { |
| | | $this->groups = true; |
| | | |
| | | foreach ($p['group_filters'] as $k => $group_filter) { |
| | | // set default name attribute to cn |
| | | if (empty($group_filter['name_attr']) && empty($this->prop['groups']['name_attr'])) |
| | | $this->prop['group_filters'][$k]['name_attr'] = $group_filter['name_attr'] = 'cn'; |
| | | |
| | | if ($group_filter['name_attr']) |
| | | $fetch_attributes[] = $group_filter['name_attr']; |
| | | } |
| | | } |
| | | |
| | | // fieldmap property is given |
| | |
| | | |
| | | // initialize cache |
| | | $rcube = rcube::get_instance(); |
| | | $this->cache = $rcube->get_cache('LDAP.' . asciiwords($this->prop['name']), 'db', 600); |
| | | if ($cache_type = $rcube->config->get('ldap_cache', 'db')) { |
| | | $cache_ttl = $rcube->config->get('ldap_cache_ttl', '10m'); |
| | | $cache_name = 'LDAP.' . asciiwords($this->prop['name']); |
| | | $this->cache = $rcube->get_cache($cache_name, $cache_type, $cache_ttl); |
| | | } |
| | | |
| | | // determine which attributes to fetch |
| | | $this->prop['list_attributes'] = array_unique($fetch_attributes); |
| | | $this->prop['attributes'] = array_merge(array_values($this->fieldmap), $fetch_attributes); |
| | | foreach ($rcube->config->get('contactlist_fields') as $col) { |
| | | $this->prop['list_attributes'] = array_merge($this->prop['list_attributes'], $this->_map_field($col)); |
| | | } |
| | | |
| | | // initialize ldap wrapper object |
| | | $this->ldap = new rcube_ldap_generic($this->prop, true); |
| | | $this->ldap->set_cache($this->cache); |
| | | |
| | | $this->_connect(); |
| | | } |
| | |
| | | { |
| | | $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; |
| | | } |
| | | |
| | |
| | | |
| | | $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']) { |
| | |
| | | |
| | | 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_attribs = array('uid'); |
| | | if ($search_bind_attrib = (array)$this->prop['search_bind_attrib']) { |
| | | foreach ($search_bind_attrib as $r => $attr) { |
| | | $search_attribs[] = $attr; |
| | | $replaces[$r] = ''; |
| | | } |
| | | } |
| | | |
| | | // Search for the dn to use to authenticate |
| | |
| | | |
| | | $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'], $search_attribs); |
| | | 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); |
| | | $replaces['%dn'] = $dn[0]; |
| | | |
| | | // add more replacements from 'search_bind_attrib' config |
| | | if ($search_bind_attrib) { |
| | | $res = ldap_get_attributes($this->ldap->conn, $entry); |
| | | foreach ($search_bind_attrib as $r => $attr) { |
| | | $sa_value = $res[$attr][0]; |
| | | $replaces[$r] = $sa_value; |
| | | } |
| | | } |
| | | } |
| | | } |
| | | else { |
| | | $this->_debug("S: ".ldap_error($this->conn)); |
| | | $this->_debug("S: ".ldap_error($this->ldap->conn)); |
| | | } |
| | | |
| | | // DN not found |
| | |
| | | $this->base_dn = strtr($this->base_dn, $replaces); |
| | | $this->groups_base_dn = strtr($this->groups_base_dn, $replaces); |
| | | |
| | | // replace placeholders in filter settings |
| | | if (!empty($this->prop['filter'])) |
| | | $this->prop['filter'] = strtr($this->prop['filter'], $replaces); |
| | | if (!empty($this->prop['groups']['filter'])) |
| | | $this->prop['groups']['filter'] = strtr($this->prop['groups']['filter'], $replaces); |
| | | if (!empty($this->prop['groups']['member_filter'])) |
| | | $this->prop['groups']['member_filter'] = strtr($this->prop['groups']['member_filter'], $replaces); |
| | | |
| | | if (!empty($this->prop['group_filters'])) { |
| | | foreach ($this->prop['group_filters'] as $i => $gf) { |
| | | if (!empty($gf['base_dn'])) |
| | | $this->prop['group_filters'][$i]['base_dn'] = strtr($gf['base_dn'], $replaces); |
| | | if (!empty($gf['filter'])) |
| | | $this->prop['group_filters'][$i]['filter'] = strtr($gf['filter'], $replaces); |
| | | } |
| | | } |
| | | |
| | | if (empty($bind_user)) { |
| | | $bind_user = $u; |
| | | } |
| | |
| | | } |
| | | 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); |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | } // 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; |
| | | } |
| | |
| | | |
| | | |
| | | /** |
| | | * 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(); |
| | | } |
| | | } |
| | | |
| | |
| | | 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); |
| | | } |
| | | |
| | | |
| | |
| | | */ |
| | | 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 |
| | |
| | | $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; |
| | |
| | | /** |
| | | * Get all members of the given group |
| | | * |
| | | * @param string Group DN |
| | | * @param array Group entries (if called recursively) |
| | | * @return array Accumulated group members |
| | | * @param string Group DN |
| | | * @param boolean Count only |
| | | * @param array Group entries (if called recursively) |
| | | * @return array Accumulated group members |
| | | */ |
| | | function list_group_members($dn, $count = false, $entries = null) |
| | | { |
| | |
| | | |
| | | // 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_merge(array('dn','objectClass','memberURL'), array_values($this->prop['groups']['class_member_attr']))); |
| | | if ($entries === false) { |
| | | return $group_members; |
| | | } |
| | | |
| | | $entries = @ldap_get_entries($this->conn, $result); |
| | | } |
| | | |
| | | for ($i=0; $i < $entries['count']; $i++) |
| | | { |
| | | $class_member_map = $this->prop['groups']['class_member_attr']; |
| | | |
| | | for ($i=0; $i < $entries['count']; $i++) { |
| | | $entry = $entries[$i]; |
| | | |
| | | if (empty($entry['objectclass'])) |
| | | continue; |
| | | |
| | | foreach ((array)$entry['objectclass'] as $objectclass) |
| | | { |
| | | switch (strtolower($objectclass)) { |
| | | case "group": |
| | | case "groupofnames": |
| | | case "kolabgroupofnames": |
| | | $group_members = array_merge($group_members, $this->_list_group_members($dn, $entry, 'member', $count)); |
| | | break; |
| | | case "groupofuniquenames": |
| | | case "kolabgroupofuniquenames": |
| | | $group_members = array_merge($group_members, $this->_list_group_members($dn, $entry, 'uniquemember', $count)); |
| | | break; |
| | | case "groupofurls": |
| | | $group_members = array_merge($group_members, $this->_list_group_memberurl($dn, $entry, $count)); |
| | | break; |
| | | foreach ((array)$entry['objectclass'] as $objectclass) { |
| | | if ($member_attr = $class_member_map[strtolower($objectclass)]) { |
| | | $group_members = array_merge($group_members, $this->_list_group_members($dn, $entry, $member_attr, $count)); |
| | | } |
| | | else if (!empty($entry['memberurl'])) { |
| | | $group_members = array_merge($group_members, $this->_list_group_memberurl($dn, $entry, $count)); |
| | | } |
| | | } |
| | | |
| | |
| | | * @param string Group DN |
| | | * @param array Group entry |
| | | * @param string Member attribute to use |
| | | * @param boolean Count only |
| | | * @return array Accumulated group members |
| | | */ |
| | | private function _list_group_members($dn, $entry, $attr, $count) |
| | |
| | | return $group_members; |
| | | |
| | | // read these attributes for all members |
| | | $attrib = $count ? array('dn') : array_values($this->fieldmap); |
| | | $attrib[] = 'objectClass'; |
| | | $attrib[] = 'member'; |
| | | $attrib[] = 'uniqueMember'; |
| | | $attrib = $count ? array('dn','objectClass') : $this->prop['list_attributes']; |
| | | $attrib = array_merge($attrib, array_values($this->prop['groups']['class_member_attr'])); |
| | | $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(); |
| | | } |
| | | |
| | |
| | | { |
| | | $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]; |
| | | } |
| | | } |
| | | } |
| | | |
| | |
| | | $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++; |
| | | } |
| | |
| | | $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)) { |
| | |
| | | } |
| | | } |
| | | |
| | | if ($fields == '*') |
| | | { |
| | | if ($fields == '*') { |
| | | // search_fields are required for fulltext search |
| | | if (empty($this->prop['search_fields'])) |
| | | { |
| | | if (empty($this->prop['search_fields'])) { |
| | | $this->set_error(self::ERROR_SEARCH, 'nofulltextsearch'); |
| | | $this->result = new rcube_result_set(); |
| | | return $this->result; |
| | | } |
| | | if (is_array($this->prop['search_fields'])) |
| | | { |
| | | if (is_array($this->prop['search_fields'])) { |
| | | foreach ($this->prop['search_fields'] as $field) { |
| | | $filter .= "($field=$wp" . $this->_quote_string($value) . "$ws)"; |
| | | $filter .= "($field=$wp" . rcube_ldap_generic::quote_string($value) . "$ws)"; |
| | | } |
| | | } |
| | | } |
| | | else |
| | | { |
| | | else { |
| | | foreach ((array)$fields as $idx => $field) { |
| | | $val = is_array($value) ? $value[$idx] : $value; |
| | | if ($attrs = $this->_map_field($field)) { |
| | | if (count($attrs) > 1) |
| | | $filter .= '(|'; |
| | | foreach ($attrs as $f) |
| | | $filter .= "($f=$wp" . $this->_quote_string($val) . "$ws)"; |
| | | $filter .= "($f=$wp" . rcube_ldap_generic::quote_string($val) . "$ws)"; |
| | | if (count($attrs) > 1) |
| | | $filter .= ')'; |
| | | } |
| | |
| | | |
| | | // set filter string and execute search |
| | | $this->set_search_set($filter); |
| | | $this->_exec_search(); |
| | | |
| | | if ($select) |
| | | $this->list_records(); |
| | |
| | | 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); |
| | |
| | | { |
| | | $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); |
| | |
| | | // 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); |
| | | } |
| | | } |
| | |
| | | } |
| | | |
| | | // Build the new entries DN. |
| | | $dn = $this->prop['LDAP_rdn'].'='.$this->_quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->base_dn; |
| | | $dn = $this->prop['LDAP_rdn'].'='.rcube_ldap_generic::quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->base_dn; |
| | | |
| | | // Remove attributes that need to be added separately (child objects) |
| | | $xfields = array(); |
| | |
| | | } |
| | | } |
| | | |
| | | if (!$this->ldap_add($dn, $newentry)) { |
| | | if (!$this->ldap->add($dn, $newentry)) { |
| | | $this->set_error(self::ERROR_SAVING, 'errorsaving'); |
| | | return false; |
| | | } |
| | | |
| | | foreach ($xfields as $xidx => $xf) { |
| | | $xdn = $xidx.'='.$this->_quote_string($xf).','.$dn; |
| | | $xdn = $xidx.'='.rcube_ldap_generic::quote_string($xf).','.$dn; |
| | | $xf = array( |
| | | $xidx => $xf, |
| | | 'objectClass' => (array) $this->prop['sub_fields'][$xidx], |
| | | ); |
| | | |
| | | $this->ldap_add($xdn, $xf); |
| | | $this->ldap->add($xdn, $xf); |
| | | } |
| | | |
| | | $dn = self::dn_encode($dn); |
| | |
| | | // 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; |
| | | } |
| | |
| | | // Handle RDN change |
| | | if ($replacedata[$this->prop['LDAP_rdn']]) { |
| | | $newdn = $this->prop['LDAP_rdn'].'=' |
| | | .$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true) |
| | | .rcube_ldap_generic::quote_string($replacedata[$this->prop['LDAP_rdn']], true) |
| | | .','.$this->base_dn; |
| | | if ($dn != $newdn) { |
| | | $newrdn = $this->prop['LDAP_rdn'].'=' |
| | | .$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true); |
| | | .rcube_ldap_generic::quote_string($replacedata[$this->prop['LDAP_rdn']], true); |
| | | unset($replacedata[$this->prop['LDAP_rdn']]); |
| | | } |
| | | } |
| | | // Replace the fields. |
| | | if (!empty($replacedata)) { |
| | | if (!$this->ldap_mod_replace($dn, $replacedata)) { |
| | | if (!$this->ldap->mod_replace($dn, $replacedata)) { |
| | | $this->set_error(self::ERROR_SAVING, 'errorsaving'); |
| | | return false; |
| | | } |
| | |
| | | // remove sub-entries |
| | | if (!empty($subdeldata)) { |
| | | foreach ($subdeldata as $fld => $val) { |
| | | $subdn = $fld.'='.$this->_quote_string($val).','.$dn; |
| | | if (!$this->ldap_delete($subdn)) { |
| | | $subdn = $fld.'='.rcube_ldap_generic::quote_string($val).','.$dn; |
| | | if (!$this->ldap->delete($subdn)) { |
| | | return false; |
| | | } |
| | | } |
| | |
| | | |
| | | 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; |
| | | } |
| | |
| | | |
| | | // 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; |
| | | } |
| | |
| | | // 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); |
| | |
| | | // add sub-entries |
| | | if (!empty($subnewdata)) { |
| | | foreach ($subnewdata as $fld => $val) { |
| | | $subdn = $fld.'='.$this->_quote_string($val).','.$dn; |
| | | $subdn = $fld.'='.rcube_ldap_generic::quote_string($val).','.$dn; |
| | | $xf = array( |
| | | $fld => $val, |
| | | 'objectClass' => (array) $this->prop['sub_fields'][$fld], |
| | | ); |
| | | $this->ldap_add($subdn, $xf); |
| | | $this->ldap->add($subdn, $xf); |
| | | } |
| | | } |
| | | |
| | |
| | | |
| | | // 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; |
| | | } |
| | |
| | | } |
| | | |
| | | // Delete the record. |
| | | if (!$this->ldap_delete($dn)) { |
| | | if (!$this->ldap->delete($dn)) { |
| | | $this->set_error(self::ERROR_SAVING, 'errorsaving'); |
| | | return false; |
| | | } |
| | |
| | | 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); |
| | | } |
| | | } |
| | |
| | | */ |
| | | 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) { |
| | |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Execute the LDAP search based on the stored credentials |
| | | */ |
| | | private function _exec_search($count = false) |
| | | { |
| | | if ($this->ready) |
| | | { |
| | | $filter = $this->filter ? $this->filter : '(objectclass=*)'; |
| | | $function = $this->_scope2func($this->prop['scope'], $ns_function); |
| | | |
| | | $this->_debug("C: Search [$filter][dn: $this->base_dn]"); |
| | | |
| | | // when using VLV, we get the total count by... |
| | | if (!$count && $function != 'ldap_read' && $this->prop['vlv'] && !$this->group_id) { |
| | | // ...either reading numSubOrdinates attribute |
| | | if ($this->prop['numsub_filter'] && ($result_count = @$ns_function($this->conn, $this->base_dn, $this->prop['numsub_filter'], array('numSubOrdinates'), 0, 0, 0))) { |
| | | $counts = ldap_get_entries($this->conn, $result_count); |
| | | for ($this->vlv_count = $j = 0; $j < $counts['count']; $j++) |
| | | $this->vlv_count += $counts[$j]['numsubordinates'][0]; |
| | | $this->_debug("D: total numsubordinates = " . $this->vlv_count); |
| | | } |
| | | else if (!function_exists('ldap_parse_virtuallist_control')) // ...or by fetching all records dn and count them |
| | | $this->vlv_count = $this->_exec_search(true); |
| | | |
| | | $this->vlv_active = $this->_vlv_set_controls($this->prop, $this->list_page, $this->page_size); |
| | | } |
| | | |
| | | // only fetch dn for count (should keep the payload low) |
| | | $attrs = $count ? array('dn') : array_values($this->fieldmap); |
| | | if ($this->ldap_result = @$function($this->conn, $this->base_dn, $filter, |
| | | $attrs, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']) |
| | | ) { |
| | | // when running on a patched PHP we can use the extended functions to retrieve the total count from the LDAP search result |
| | | if ($this->vlv_active && function_exists('ldap_parse_virtuallist_control')) { |
| | | if (ldap_parse_result($this->conn, $this->ldap_result, |
| | | $errcode, $matcheddn, $errmsg, $referrals, $serverctrls) |
| | | && $serverctrls // can be null e.g. in case of adm. limit error |
| | | ) { |
| | | ldap_parse_virtuallist_control($this->conn, $serverctrls, |
| | | $last_offset, $this->vlv_count, $vresult); |
| | | $this->_debug("S: VLV result: last_offset=$last_offset; content_count=$this->vlv_count"); |
| | | } |
| | | else { |
| | | $this->_debug("S: ".($errmsg ? $errmsg : ldap_error($this->conn))); |
| | | } |
| | | } |
| | | |
| | | $entries_count = ldap_count_entries($this->conn, $this->ldap_result); |
| | | $this->_debug("S: $entries_count record(s)"); |
| | | |
| | | return $count ? $entries_count : true; |
| | | } |
| | | else { |
| | | $this->_debug("S: ".ldap_error($this->conn)); |
| | | } |
| | | } |
| | | |
| | | return false; |
| | | } |
| | | |
| | | /** |
| | | * Choose the right PHP function according to scope property |
| | | */ |
| | | private function _scope2func($scope, &$ns_function = null) |
| | | { |
| | | switch ($scope) { |
| | | case 'sub': |
| | | $function = $ns_function = 'ldap_search'; |
| | | break; |
| | | case 'base': |
| | | $function = $ns_function = 'ldap_read'; |
| | | break; |
| | | default: |
| | | $function = 'ldap_list'; |
| | | $ns_function = 'ldap_read'; |
| | | break; |
| | | } |
| | | |
| | | return $function; |
| | | } |
| | | |
| | | /** |
| | | * Set server controls for Virtual List View (paginated listing) |
| | | */ |
| | | private function _vlv_set_controls($prop, $list_page, $page_size, $search = null) |
| | | { |
| | | $sort_ctrl = array('oid' => "1.2.840.113556.1.4.473", 'value' => $this->_sort_ber_encode((array)$prop['sort'])); |
| | | $vlv_ctrl = array('oid' => "2.16.840.1.113730.3.4.9", 'value' => $this->_vlv_ber_encode(($offset = ($list_page-1) * $page_size + 1), $page_size, $search), 'iscritical' => true); |
| | | |
| | | $sort = (array)$prop['sort']; |
| | | $this->_debug("C: set controls sort=" . join(' ', unpack('H'.(strlen($sort_ctrl['value'])*2), $sort_ctrl['value'])) . " ($sort[0]);" |
| | | . " vlv=" . join(' ', (unpack('H'.(strlen($vlv_ctrl['value'])*2), $vlv_ctrl['value']))) . " ($offset/$page_size)"); |
| | | |
| | | if (!ldap_set_option($this->conn, LDAP_OPT_SERVER_CONTROLS, array($sort_ctrl, $vlv_ctrl))) { |
| | | $this->_debug("S: ".ldap_error($this->conn)); |
| | | $this->set_error(self::ERROR_SEARCH, 'vlvnotsupported'); |
| | | return false; |
| | | } |
| | | |
| | | return true; |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Converts LDAP entry into an array |
| | | */ |
| | | private function _ldap2result($rec) |
| | | { |
| | | $out = array(); |
| | | $out = array('_type' => 'person'); |
| | | $fieldmap = $this->fieldmap; |
| | | |
| | | if ($rec['dn']) |
| | | $out[$this->primary_key] = self::dn_encode($rec['dn']); |
| | | |
| | | foreach ($this->fieldmap as $rf => $lf) |
| | | // determine record type |
| | | if (self::is_group_entry($rec)) { |
| | | $out['_type'] = 'group'; |
| | | $out['readonly'] = true; |
| | | $fieldmap['name'] = $this->group_data['name_attr'] ? $this->group_data['name_attr'] : $this->prop['groups']['name_attr']; |
| | | } |
| | | |
| | | foreach ($fieldmap as $rf => $lf) |
| | | { |
| | | for ($i=0; $i < $rec[$lf]['count']; $i++) { |
| | | if (!($value = $rec[$lf][$i])) |
| | |
| | | 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; |
| | | } |
| | | } |
| | | } |
| | | } |
| | |
| | | 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 |
| | |
| | | |
| | | |
| | | /** |
| | | * 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); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 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; |
| | | } |
| | |
| | | return array(); |
| | | |
| | | // use cached list for searching |
| | | $this->cache->expunge(); |
| | | if (!$search || ($group_cache = $this->cache->get('groups')) === null) |
| | | if (!$this->cache || !empty($this->prop['group_filters']) || ($group_cache = $this->cache->get('groups')) === null) { |
| | | $group_cache = $this->_fetch_groups(); |
| | | } |
| | | |
| | | $groups = array(); |
| | | if ($search) { |
| | |
| | | */ |
| | | 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']; |
| | |
| | | $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'], $ns_function); |
| | | $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 |
| | |
| | | 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); |
| | |
| | | array_multisort($group_sortnames, SORT_ASC, SORT_STRING, $groups); |
| | | |
| | | // cache this |
| | | $this->cache->set('groups', $groups); |
| | | if ($this->cache) { |
| | | $this->cache->set('groups', $groups); |
| | | } |
| | | |
| | | return $groups; |
| | | } |
| | | |
| | | /** |
| | | * Fetch a group entry from LDAP and save in local cache |
| | | */ |
| | | private function get_group_entry($group_id) |
| | | { |
| | | if (!$this->cache || ($group_cache = $this->cache->get('groups')) === null) { |
| | | $group_cache = $this->_fetch_groups(); |
| | | } |
| | | |
| | | // add group record to cache if it isn't yet there |
| | | if (!isset($group_cache[$group_id])) { |
| | | $name_attr = $this->prop['groups']['name_attr']; |
| | | $dn = self::dn_decode($group_id); |
| | | |
| | | $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; |
| | | } |
| | | |
| | | if ($this->cache) { |
| | | $this->cache->set('groups', $group_cache); |
| | | } |
| | | } |
| | | |
| | | return $group_cache[$group_id]; |
| | | } |
| | | |
| | | /** |
| | |
| | | */ |
| | | 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; |
| | |
| | | */ |
| | | function create_group($group_name) |
| | | { |
| | | $base_dn = $this->groups_base_dn; |
| | | $new_dn = "cn=$group_name,$base_dn"; |
| | | $new_gid = self::dn_encode($group_name); |
| | | $new_dn = 'cn=' . rcube_ldap_generic::quote_string($group_name, true) . ',' . $this->groups_base_dn; |
| | | $new_gid = self::dn_encode($new_dn); |
| | | $member_attr = $this->get_group_member_attr(); |
| | | $name_attr = $this->prop['groups']['name_attr'] ? $this->prop['groups']['name_attr'] : 'cn'; |
| | | |
| | |
| | | $member_attr => '', |
| | | ); |
| | | |
| | | if (!$this->ldap_add($new_dn, $new_entry)) { |
| | | if (!$this->ldap->add($new_dn, $new_entry)) { |
| | | $this->set_error(self::ERROR_SAVING, 'errorsaving'); |
| | | return false; |
| | | } |
| | | |
| | | $this->cache->remove('groups'); |
| | | if ($this->cache) { |
| | | $this->cache->remove('groups'); |
| | | } |
| | | |
| | | return array('id' => $new_gid, 'name' => $group_name); |
| | | } |
| | |
| | | */ |
| | | function delete_group($group_id) |
| | | { |
| | | if (($group_cache = $this->cache->get('groups')) === null) |
| | | if (!$this->cache || ($group_cache = $this->cache->get('groups')) === null) { |
| | | $group_cache = $this->_fetch_groups(); |
| | | } |
| | | |
| | | $base_dn = $this->groups_base_dn; |
| | | $group_name = $group_cache[$group_id]['name']; |
| | | $del_dn = "cn=$group_name,$base_dn"; |
| | | $del_dn = $group_cache[$group_id]['dn']; |
| | | |
| | | if (!$this->ldap_delete($del_dn)) { |
| | | if (!$this->ldap->delete($del_dn)) { |
| | | $this->set_error(self::ERROR_SAVING, 'errorsaving'); |
| | | return false; |
| | | } |
| | | |
| | | $this->cache->remove('groups'); |
| | | if ($this->cache) { |
| | | unset($group_cache[$group_id]); |
| | | $this->cache->set('groups', $group_cache); |
| | | } |
| | | |
| | | return true; |
| | | } |
| | |
| | | */ |
| | | function rename_group($group_id, $new_name, &$new_gid) |
| | | { |
| | | if (($group_cache = $this->cache->get('groups')) === null) |
| | | if (!$this->cache || ($group_cache = $this->cache->get('groups')) === null) { |
| | | $group_cache = $this->_fetch_groups(); |
| | | } |
| | | |
| | | $base_dn = $this->groups_base_dn; |
| | | $group_name = $group_cache[$group_id]['name']; |
| | | $old_dn = "cn=$group_name,$base_dn"; |
| | | $new_rdn = "cn=$new_name"; |
| | | $new_gid = self::dn_encode($new_name); |
| | | $old_dn = $group_cache[$group_id]['dn']; |
| | | $new_rdn = "cn=" . rcube_ldap_generic::quote_string($new_name, true); |
| | | $new_gid = self::dn_encode($new_rdn . ',' . $this->groups_base_dn); |
| | | |
| | | if (!$this->ldap_rename($old_dn, $new_rdn, null, true)) { |
| | | if (!$this->ldap->rename($old_dn, $new_rdn, null, true)) { |
| | | $this->set_error(self::ERROR_SAVING, 'errorsaving'); |
| | | return false; |
| | | } |
| | | |
| | | $this->cache->remove('groups'); |
| | | if ($this->cache) { |
| | | $this->cache->remove('groups'); |
| | | } |
| | | |
| | | return $new_name; |
| | | } |
| | |
| | | */ |
| | | function add_to_group($group_id, $contact_ids) |
| | | { |
| | | if (($group_cache = $this->cache->get('groups')) === null) |
| | | if (!$this->cache || ($group_cache = $this->cache->get('groups')) === null) { |
| | | $group_cache = $this->_fetch_groups(); |
| | | } |
| | | |
| | | if (!is_array($contact_ids)) |
| | | $contact_ids = explode(',', $contact_ids); |
| | |
| | | $base_dn = $this->groups_base_dn; |
| | | $group_name = $group_cache[$group_id]['name']; |
| | | $member_attr = $group_cache[$group_id]['member_attr']; |
| | | $group_dn = "cn=$group_name,$base_dn"; |
| | | $group_dn = $group_cache[$group_id]['dn']; |
| | | $new_attrs = array(); |
| | | |
| | | foreach ($contact_ids as $id) |
| | |
| | | return 0; |
| | | } |
| | | |
| | | $this->cache->remove('groups'); |
| | | if ($this->cache) { |
| | | $this->cache->remove('groups'); |
| | | } |
| | | |
| | | return count($new_attrs[$member_attr]); |
| | | } |
| | |
| | | */ |
| | | function remove_from_group($group_id, $contact_ids) |
| | | { |
| | | if (($group_cache = $this->cache->get('groups')) === null) |
| | | if (!$this->cache || ($group_cache = $this->cache->get('groups')) === null) { |
| | | $group_cache = $this->_fetch_groups(); |
| | | } |
| | | |
| | | if (!is_array($contact_ids)) |
| | | $contact_ids = explode(',', $contact_ids); |
| | |
| | | $base_dn = $this->groups_base_dn; |
| | | $group_name = $group_cache[$group_id]['name']; |
| | | $member_attr = $group_cache[$group_id]['member_attr']; |
| | | $group_dn = "cn=$group_name,$base_dn"; |
| | | $group_dn = $group_cache[$group_id]['dn']; |
| | | $del_attrs = array(); |
| | | |
| | | foreach ($contact_ids as $id) |
| | |
| | | return 0; |
| | | } |
| | | |
| | | $this->cache->remove('groups'); |
| | | if ($this->cache) { |
| | | $this->cache->remove('groups'); |
| | | } |
| | | |
| | | return count($del_attrs[$member_attr]); |
| | | } |
| | |
| | | $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; |
| | | } |
| | |
| | | $object_classes = $this->prop['groups']['object_classes']; |
| | | } |
| | | if (!empty($object_classes)) { |
| | | $class_member_map = $this->prop['groups']['class_member_attr']; |
| | | foreach ((array)$object_classes as $oc) { |
| | | switch (strtolower($oc)) { |
| | | case 'group': |
| | | case 'groupofnames': |
| | | case 'kolabgroupofnames': |
| | | $member_attr = 'member'; |
| | | break; |
| | | |
| | | case 'groupofuniquenames': |
| | | case 'kolabgroupofuniquenames': |
| | | $member_attr = 'uniqueMember'; |
| | | break; |
| | | $oc = strtolower($oc); |
| | | if (!empty($class_member_map[$oc])) { |
| | | $member_attr = $class_member_map[$oc]; |
| | | break; |
| | | } |
| | | } |
| | | } |
| | |
| | | return 'member'; |
| | | } |
| | | |
| | | |
| | | /** |
| | | * 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 |
| | |
| | | { |
| | | $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; |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | <?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 |
| | | */ |
| | | public static function quote_string($str, $dn=false) |
| | | { |
| | | // take firt entry if array given |
| | | if (is_array($str)) |
| | | $str = reset($str); |
| | | |
| | | if ($dn) |
| | | $replace = array(','=>'\2c', '='=>'\3d', '+'=>'\2b', '<'=>'\3c', |
| | | '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c', '"'=>'\22', '#'=>'\23'); |
| | | else |
| | | $replace = array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c', |
| | | '/'=>'\2f'); |
| | | |
| | | return strtr($str, $replace); |
| | | } |
| | | |
| | | |
| | | /** |
| | | * Prints debug info to the log |
| | | */ |
| | | private function _debug($str) |
| | | { |
| | | if ($this->debug && class_exists('rcube')) { |
| | | rcube::write_log('ldap', $str); |
| | | } |
| | | } |
| | | |
| | | |
| | | /***************** Virtual List View (VLV) related utility functions **************** */ |
| | | |
| | | /** |
| | | * Return the search string value to be used in VLV controls |
| | | */ |
| | | private function _vlv_search($sort, $search) |
| | | { |
| | | foreach ($search as $attr => $value) { |
| | | if (!in_array(strtolower($attr), $sort)) { |
| | | $this->_debug("d: Cannot use VLV search using attribute not indexed: $attr (not in " . var_export($sort, true) . ")"); |
| | | return null; |
| | | } else { |
| | | return $value; |
| | | } |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * Find a VLV index matching the given query attributes |
| | | * |
| | | * @return string Sort attribute or False if no match |
| | | */ |
| | | private function _find_vlv($base_dn, $filter, $scope, $sort_attrs = null) |
| | | { |
| | | if (!$this->config['vlv'] || $scope == 'base') { |
| | | return false; |
| | | } |
| | | |
| | | // get vlv config |
| | | $vlv_config = $this->_read_vlv_config(); |
| | | |
| | | if ($vlv = $vlv_config[$base_dn]) { |
| | | $this->_debug("D: Found a VLV for base_dn: " . $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; |
| | | } |
| | | |
| | | } |
New file |
| | |
| | | <?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; |
| | | } |
| | | |
| | | } |
| | |
| | | |
| | | $labels['group'] = 'Group'; |
| | | $labels['groups'] = 'Groups'; |
| | | $labels['listgroup'] = 'List group members'; |
| | | $labels['personaladrbook'] = 'Personal Addresses'; |
| | | |
| | | $labels['searchsave'] = 'Save search'; |
| | |
| | | foreach ($cid as $cid) { |
| | | $a_record = $CONTACTS->get_record($cid, true); |
| | | |
| | | // avoid copying groups |
| | | if ($a_record['_type'] == 'group') |
| | | continue; |
| | | |
| | | // Check if contact exists, if so, we'll need it's ID |
| | | // Note: Some addressbooks allows empty email address field |
| | | if (!empty($a_record['email'])) |
| | |
| | | $jsdata = array(); |
| | | |
| | | $line_templ = html::tag('li', array( |
| | | 'id' => 'rcmli%s', 'class' => '%s'), |
| | | 'id' => 'rcmli%s', 'class' => '%s', 'noclose' => true), |
| | | html::a(array('href' => '%s', |
| | | 'rel' => '%s', |
| | | 'onclick' => "return ".JS_OBJECT_NAME.".command('list','%s',this)"), '%s')); |
| | |
| | | |
| | | $name = !empty($source['name']) ? $source['name'] : $id; |
| | | $out .= sprintf($line_templ, |
| | | html_identifier($id), |
| | | rcube_utils::html_identifier($id, true), |
| | | $class_name, |
| | | Q(rcmail_url(null, array('_source' => $id))), |
| | | $source['id'], |
| | |
| | | $groupdata = rcmail_contact_groups($groupdata); |
| | | $jsdata = $groupdata['jsdata']; |
| | | $out = $groupdata['out']; |
| | | $out .= '</li>'; |
| | | } |
| | | |
| | | $line_templ = html::tag('li', array( |
| | | 'id' => 'rcmliS%s', 'class' => '%s'), |
| | | 'id' => 'rcmli%s', 'class' => '%s'), |
| | | html::a(array('href' => '#', 'rel' => 'S%s', |
| | | 'onclick' => "return ".JS_OBJECT_NAME.".command('listsearch', '%s', this)"), '%s')); |
| | | |
| | |
| | | $class_name .= ' ' . $source['class_name']; |
| | | |
| | | $out .= sprintf($line_templ, |
| | | html_identifier($id), |
| | | rcube_utils::html_identifier('S'.$id, true), |
| | | $class_name, |
| | | $id, |
| | | $js_id, (!empty($source['name']) ? Q($source['name']) : Q($id))); |
| | | } |
| | | |
| | | $OUTPUT->set_env('contactgroups', $jsdata); |
| | | $OUTPUT->set_env('collapsed_abooks', (string)$RCMAIL->config->get('collapsed_abooks','')); |
| | | $OUTPUT->add_gui_object('folderlist', $attrib['id']); |
| | | $OUTPUT->include_script('treelist.js'); |
| | | |
| | | // add some labels to client |
| | | $OUTPUT->add_label('deletegroupconfirm', 'groupdeleting', 'addingmember', 'removingmember'); |
| | | |
| | |
| | | global $RCMAIL; |
| | | |
| | | $groups = $RCMAIL->get_address_book($args['source'])->list_groups(); |
| | | $js_id = $RCMAIL->JQ($args['source']); |
| | | $groups_html = ''; |
| | | |
| | | if (!empty($groups)) { |
| | | $line_templ = html::tag('li', array( |
| | | 'id' => 'rcmliG%s', 'class' => 'contactgroup'), |
| | | 'id' => 'rcmli%s', 'class' => 'contactgroup'), |
| | | html::a(array('href' => '#', |
| | | 'rel' => '%s:%s', |
| | | 'onclick' => "return ".JS_OBJECT_NAME.".command('listgroup',{'source':'%s','id':'%s'},this)"), '%s')); |
| | | |
| | | // append collapse/expand toggle and open a new <ul> |
| | | $is_collapsed = strpos($RCMAIL->config->get('collapsed_abooks',''), '&'.rawurlencode($args['source']).'&') !== false; |
| | | $args['out'] .= html::div('treetoggle ' . ($is_collapsed ? 'collapsed' : 'expanded'), ' '); |
| | | |
| | | $jsdata = array(); |
| | | foreach ($groups as $group) { |
| | | $args['out'] .= sprintf($line_templ, |
| | | html_identifier($args['source'] . $group['ID']), |
| | | $groups_html .= sprintf($line_templ, |
| | | rcube_utils::html_identifier('G' . $args['source'] . $group['ID'], true), |
| | | $args['source'], $group['ID'], |
| | | $args['source'], $group['ID'], Q($group['name']) |
| | | ); |
| | |
| | | 'name' => $group['name'], 'type' => 'group'); |
| | | } |
| | | } |
| | | |
| | | $args['out'] .= html::tag('ul', |
| | | array('class' => 'groups', 'style' => ($is_collapsed || empty($groups) ? "display:none;" : null)), |
| | | $groups_html); |
| | | |
| | | return $args; |
| | | } |
| | |
| | | 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'])) |
| | |
| | | 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); |
| | | } |
| | | |
| | | |
| | |
| | | |
| | | 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( |
| | |
| | | $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 = ''; |
| | | |
| | |
| | | |
| | | 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']); |
| | | |
| | |
| | | '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') |
| | |
| | | $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 |
| | |
| | | $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); |
| | |
| | | $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']))); |
| | |
| | | $CONTACTS->set_pagesize($page_size); |
| | | $CONTACTS->set_page($list_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']); |
| | |
| | | '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']; |
| | |
| | | '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); |
| | | } |
| | | } |
| | |
| | | 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); |
| | | } |
| | | } |
| | | } |
| | |
| | | |
| | | #directorylistbox input |
| | | { |
| | | margin: 0px; |
| | | margin: 0 0 0 20px; |
| | | font-size: 11px; |
| | | width: 90%; |
| | | } |
| | |
| | | width: 280px; |
| | | } |
| | | |
| | | #directorylist |
| | | #directorylist, |
| | | #directorylist li ul |
| | | { |
| | | list-style: none; |
| | | margin: 0; |
| | |
| | | background-color: #FFFFFF; |
| | | } |
| | | |
| | | #directorylist li ul |
| | | { |
| | | border-top: 1px solid #EBEBEB; |
| | | } |
| | | |
| | | #directorylist li |
| | | { |
| | | display: block; |
| | | font-size: 11px; |
| | | background: url(images/icons/folders.png) 5px -108px no-repeat; |
| | | border-bottom: 1px solid #EBEBEB; |
| | | white-space: nowrap; |
| | | } |
| | |
| | | padding-left: 25px; |
| | | padding-top: 2px; |
| | | padding-bottom: 2px; |
| | | height: 16px; |
| | | text-decoration: none; |
| | | white-space: nowrap; |
| | | background: url(images/icons/folders.png) 5px -108px no-repeat; |
| | | } |
| | | |
| | | #directorylist li.contactgroup |
| | | #directorylist li ul li a |
| | | { |
| | | padding-left: 15px; |
| | | background-position: 20px -143px; |
| | | padding-left: 45px; |
| | | } |
| | | |
| | | #directorylist li.contactsearch |
| | | #directorylist li ul li:last-child |
| | | { |
| | | border-bottom: 0; |
| | | } |
| | | |
| | | #directorylist li.contactgroup a |
| | | { |
| | | background-position: 22px -143px; |
| | | } |
| | | |
| | | #directorylist li.contactsearch a |
| | | { |
| | | background-position: 6px -162px; |
| | | } |
| | | |
| | | #directorylist li.selected |
| | | { |
| | | background-color: #929292; |
| | | border-bottom: 1px solid #898989; |
| | | } |
| | | |
| | | #directorylist li.selected a |
| | | #directorylist li.selected > a |
| | | { |
| | | color: #FFF; |
| | | font-weight: bold; |
| | | background-color: #929292; |
| | | } |
| | | |
| | | #directorylist li.droptarget |
| | |
| | | -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; |
| | |
| | | background-color: #929292; |
| | | } |
| | | |
| | | ul.treelist li |
| | | { |
| | | position: relative; |
| | | } |
| | | |
| | | ul.treelist li div.treetoggle |
| | | { |
| | | position: absolute; |
| | | left: 8px !important; |
| | | left: -16px; |
| | | top: 1px; |
| | | width: 14px; |
| | | height: 16px; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | ul.treelist li div.collapsed |
| | | { |
| | | background: url(images/icons/collapsed.png) bottom right no-repeat; |
| | | } |
| | | |
| | | ul.treelist li div.expanded |
| | | { |
| | | background: url(images/icons/expanded.png) bottom right no-repeat; |
| | | } |
| | | |
| | | |
| | | /***** mac-style quicksearch field *****/ |
| | | |
| | |
| | | font-size: 11px; |
| | | padding: 0px; |
| | | border: none; |
| | | outline: none; |
| | | } |
| | | |
| | | /***** roundcube webmail pre-defined classes *****/ |
| | |
| | | border-bottom: none; |
| | | } |
| | | |
| | | #mailboxlist li div |
| | | { |
| | | position: absolute; |
| | | left: 8px !important; |
| | | left: -16px; |
| | | top: 1px; |
| | | width: 14px; |
| | | height: 16px; |
| | | } |
| | | |
| | | #mailboxlist li div.collapsed, |
| | | #mailboxlist li div.expanded |
| | | { |
| | | cursor: pointer; |
| | | } |
| | | |
| | | #mailboxlist li div.collapsed |
| | | { |
| | | background: url(images/icons/collapsed.png) bottom right no-repeat; |
| | | } |
| | | |
| | | #mailboxlist li div.expanded |
| | | { |
| | | background: url(images/icons/expanded.png) bottom right no-repeat; |
| | | } |
| | | |
| | | #mailboxlist li.inbox |
| | | { |
| | | background-position: 5px -18px; |
| | |
| | | -o-text-overflow: ellipsis; |
| | | } |
| | | |
| | | #contacts-table td span.email |
| | | { |
| | | display: inline; |
| | | color: #ccc; |
| | | font-style: italic; |
| | | margin-left: 0.5em; |
| | | } |
| | | |
| | | #abookcountbar |
| | | { |
| | | margin-top: 4px; |
| | |
| | | <div id="directorylistbox"> |
| | | <div id="directorylist-title" class="boxtitle"><roundcube:label name="groups" /></div> |
| | | <div id="directorylist-content" class="boxlistcontent"> |
| | | <roundcube:object name="directorylist" id="directorylist" /> |
| | | <roundcube:object name="groupslist" id="contactgroupslist" /> |
| | | <roundcube:object name="directorylist" id="directorylist" class="treelist" /> |
| | | </div> |
| | | <div id="directorylist-footer" class="boxfooter"> |
| | | <roundcube:button command="group-create" type="link" title="newcontactgroup" class="buttonPas addgroup" classAct="button addgroup" content=" " /> |
| | |
| | | <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> |
| | |
| | | <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"> |
| | |
| | | <div id="mailboxlist-container"> |
| | | <div id="mailboxlist-title" class="boxtitle"><roundcube:label name="mailboxlist" /></div> |
| | | <div id="mailboxlist-content" class="boxlistcontent"> |
| | | <roundcube:object name="mailboxlist" id="mailboxlist" folder_filter="mail" /> |
| | | <roundcube:object name="mailboxlist" id="mailboxlist" class="treelist" folder_filter="mail" /> |
| | | </div> |
| | | <div id="mailboxlist-footer" class="boxfooter"> |
| | | <roundcube:button name="mailboxmenulink" id="mailboxmenulink" type="link" title="folderactions" class="button groupactions" onclick="rcmail_ui.show_popup('mailboxmenu');return false" content=" " /> |
| | |
| | | <div id="mailboxlist-container"> |
| | | <div id="mailboxlist-title" class="boxtitle"><roundcube:label name="mailboxlist" /></div> |
| | | <div class="boxlistcontent"> |
| | | <roundcube:object name="mailboxlist" id="mailboxlist" maxlength="25" /> |
| | | <roundcube:object name="mailboxlist" id="mailboxlist" class="treelist" maxlength="25" /> |
| | | </div> |
| | | <div class="boxfooter"></div> |
| | | </div> |
| | |
| | | <div id="mailboxlist-container"> |
| | | <div class="boxtitle"><roundcube:label name="mailboxlist" /></div> |
| | | <div class="boxlistcontent"> |
| | | <roundcube:object name="mailboxlist" id="mailboxlist" folder_filter="mail" /> |
| | | <roundcube:object name="mailboxlist" id="mailboxlist" class="treelist" folder_filter="mail" /> |
| | | </div> |
| | | <div class="boxfooter"></div> |
| | | </div> |
| | |
| | | text-overflow: ellipsis; |
| | | } |
| | | |
| | | #contacts-table .contact.readonly td { |
| | | font-style: italic; |
| | | } |
| | | |
| | | #directorylist li.addressbook a { |
| | | background-position: 6px -766px; |
| | | } |
| | | |
| | | #directorylist li.addressbook.selected a { |
| | | #directorylist li.addressbook.selected > a { |
| | | background-position: 6px -791px; |
| | | } |
| | | |
| | | #directorylist li.addressbook ul li:last-child { |
| | | border-bottom: 0; |
| | | } |
| | | |
| | | #directorylist li.addressbook ul.groups { |
| | | margin: 0; |
| | | padding: 0; |
| | | } |
| | | |
| | | #directorylist li.addressbook ul.groups li { |
| | | width: 100%; |
| | | } |
| | | |
| | | #directorylist li.contactgroup a { |
| | |
| | | margin-left: 8px; |
| | | } |
| | | |
| | | #directorylist li.addressbook div.collapsed, |
| | | #directorylist li.addressbook div.expanded { |
| | | top: 15px; |
| | | 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; |
| | | } |
| | |
| | | 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; |
| | |
| | | .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 */ |
| | |
| | | }); |
| | | |
| | | </script> |
| | | <!--[if lte IE 8]> |
| | | <script type="text/javascript"> |
| | | |
| | | // fix missing :last-child selectors |
| | | $(document).ready(function(){ |
| | | $('ul.treelist ul').each(function(i,ul){ |
| | | $('li:last-child', ul).css('border-bottom', 0); |
| | | }); |
| | | }); |
| | | |
| | | </script> |
| | | <![endif]--> |
| | | |
| | |
| | | background-position: 6px 2px; |
| | | } |
| | | |
| | | #mailboxlist li:first-child { |
| | | #mailboxlist > li:first-child { |
| | | border-radius: 4px 4px 0 0; |
| | | border-top: 0; |
| | | } |
| | |
| | | background-position: 6px 3px; |
| | | } |
| | | |
| | | #mailboxlist li.mailbox.unread a { |
| | | #mailboxlist li.mailbox.unread > a { |
| | | padding-right: 36px; |
| | | } |
| | | |
| | |
| | | color: #017cb4; |
| | | } |
| | | |
| | | #mailboxlist li.mailbox div.treetoggle { |
| | | top: 13px; |
| | | left: 19px; |
| | | } |
| | | |
| | | #mailboxlist li.mailbox ul li:last-child { |
| | | border-bottom: 0; |
| | | } |
| | | |
| | | /* nested mailboxes */ |
| | | |
| | | #mailboxlist li.mailbox ul { |
| | | list-style: none; |
| | | margin: 0; |
| | |
| | | border-top: 1px solid #bbd3da; |
| | | } |
| | | |
| | | #mailboxlist li.mailbox ul li { |
| | | padding-left: 26px; |
| | | } |
| | | |
| | | #mailboxlist li.mailbox ul li a { |
| | | background-position: 6px -93px; |
| | | padding-left: 52px; /* 36 + 1 x 16 */ |
| | | background-position: 22px -93px; /* 6 + 1 x 16 */ |
| | | } |
| | | |
| | | #mailboxlist li.mailbox ul li.selected > a { |
| | | background-position: 6px -117px; |
| | | background-position: 22px -117px; |
| | | } |
| | | |
| | | #mailboxlist li.mailbox ul li:last-child { |
| | | border-bottom: 0; |
| | | } |
| | | |
| | | #mailboxlist li.mailbox div.collapsed, |
| | | #mailboxlist li.mailbox div.expanded { |
| | | position: absolute; |
| | | top: 13px; |
| | | left: 19px; |
| | | width: 13px; |
| | | height: 13px; |
| | | background: url(images/listicons.png) -3px -144px no-repeat; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | #mailboxlist li.mailbox div.expanded { |
| | | background-position: -3px -168px; |
| | | } |
| | | |
| | | #mailboxlist li.mailbox.selected > div.collapsed { |
| | | background-position: -23px -144px; |
| | | } |
| | | |
| | | #mailboxlist li.mailbox.selected > div.expanded { |
| | | background-position: -23px -168px; |
| | | } |
| | | |
| | | |
| | | #mailboxlist li.mailbox ul li div.collapsed, |
| | | #mailboxlist li.mailbox ul li div.expanded { |
| | | left: 43px; |
| | | #mailboxlist li.mailbox ul li div.treetoggle { |
| | | left: 33px; |
| | | top: 14px; |
| | | } |
| | | |
| | | #mailboxlist li.mailbox ul ul li.mailbox a { |
| | | padding-left: 68px; /* 2x */ |
| | | background-position: 38px -93px; |
| | | } |
| | | #mailboxlist li.mailbox ul ul li.selected > a { |
| | | background-position: 38px -117px; |
| | | } |
| | | #mailboxlist li.mailbox ul ul li div.treetoggle { |
| | | left: 48px; |
| | | } |
| | | |
| | | #mailboxlist li.mailbox ul ul ul li.mailbox a { |
| | | padding-left: 84px; /* 3x */ |
| | | background-position: 54px -93px; |
| | | } |
| | | #mailboxlist li.mailbox ul ul ul li.selected > a { |
| | | background-position: 54px -117px; |
| | | } |
| | | #mailboxlist li.mailbox ul ul ul li div.treetoggle { |
| | | left: 64px; |
| | | } |
| | | |
| | | #mailboxlist li.mailbox ul ul ul ul li.mailbox a { |
| | | padding-left: 100px; /* 4x */ |
| | | background-position: 70px -93px; |
| | | } |
| | | #mailboxlist li.mailbox ul ul ul ul li.selected > a { |
| | | background-position: 70px -117px; |
| | | } |
| | | #mailboxlist li.mailbox ul ul ul ul li div.treetoggle { |
| | | left: 80px; |
| | | } |
| | | |
| | | /* indent folders on levels > 4 */ |
| | | #mailboxlist li.mailbox ul ul ul ul ul li { |
| | | padding-left: 16px; |
| | | } |
| | | #mailboxlist li.mailbox ul ul ul ul ul li div.treetoggle { |
| | | left: 96px; |
| | | } |
| | | |
| | | #mailboxlist li.mailbox .unreadcount { |
| | |
| | | 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; |
| | | } |
| | |
| | | background-color: #d9ecf4; |
| | | } |
| | | |
| | | ul.listing li ul { |
| | | border-top: 1px solid #bbd3da; |
| | | } |
| | | |
| | | ul.listing li.droptarget, |
| | | table.listing tr.droptarget td { |
| | | background-color: #e8e798; |
| | | } |
| | | |
| | | .listbox table.listing { |
| | | background-color: #d9ecf4; |
| | | } |
| | | |
| | | table.listing, |
| | |
| | | vertical-align: top; |
| | | } |
| | | |
| | | ul.treelist li { |
| | | position: relative; |
| | | } |
| | | |
| | | ul.treelist li div.treetoggle { |
| | | position: absolute; |
| | | top: 13px; |
| | | left: 19px; |
| | | width: 13px; |
| | | height: 13px; |
| | | background: url(images/listicons.png) -3px -144px no-repeat; |
| | | cursor: pointer; |
| | | } |
| | | |
| | | ul.treelist li div.treetoggle.expanded { |
| | | background-position: -3px -168px; |
| | | } |
| | | |
| | | ul.treelist li.selected > div.collapsed { |
| | | background-position: -23px -144px; |
| | | } |
| | | |
| | | ul.treelist li.selected > div.expanded { |
| | | background-position: -23px -168px; |
| | | } |
| | | |
| | | .listbox .boxfooter { |
| | | position: absolute; |
| | | bottom: 0; |
| | |
| | | <div id="directorylistbox" class="uibox listbox"> |
| | | <h2 id="directorylist-header" class="boxtitle"><roundcube:label name="groups" /></h2> |
| | | <div id="directorylist-content" class="scroller withfooter"> |
| | | <roundcube:object name="directorylist" id="directorylist" class="listing" /> |
| | | <roundcube:object name="directorylist" id="directorylist" class="treelist listing" /> |
| | | </div> |
| | | <div id="directorylist-footer" class="boxfooter"> |
| | | <roundcube:button command="group-create" type="link" title="newcontactgroup" class="listbutton add disabled" classAct="listbutton add" innerClass="inner" content="+" /><roundcube:button name="groupoptions" id="groupoptionslink" type="link" title="moreactions" class="listbutton groupactions" onclick="UI.show_popup('groupoptions');return false" innerClass="inner" content="⚙" /> |
| | |
| | | |
| | | <!-- 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> |
| | |
| | | <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" /> |
| | | |
| | |
| | | <div id="folderlist-header"></div> |
| | | <div id="mailboxcontainer" class="uibox listbox"> |
| | | <div id="folderlist-content" class="scroller withfooter"> |
| | | <roundcube:object name="mailboxlist" id="mailboxlist" class="listing" folder_filter="mail" unreadwrap="%s" /> |
| | | <roundcube:object name="mailboxlist" id="mailboxlist" class="treelist listing" folder_filter="mail" unreadwrap="%s" /> |
| | | </div> |
| | | <div id="folderlist-footer" class="boxfooter"> |
| | | <roundcube:button name="mailboxmenulink" id="mailboxmenulink" type="link" title="folderactions" class="listbutton groupactions" onclick="UI.show_popup('mailboxmenu');return false" innerClass="inner" content="⚙" /> |
| | |
| | | <!-- folders list --> |
| | | <div id="mailboxcontainer" class="uibox listbox"> |
| | | <div class="scroller"> |
| | | <roundcube:object name="mailboxlist" id="mailboxlist" class="listing" folder_filter="mail" unreadwrap="%s" /> |
| | | <roundcube:object name="mailboxlist" id="mailboxlist" class="treelist listing" folder_filter="mail" unreadwrap="%s" /> |
| | | </div> |
| | | </div> |
| | | |
| | |
| | | <!-- folders list --> |
| | | <div id="mailboxcontainer" class="uibox listbox"> |
| | | <div class="scroller"> |
| | | <roundcube:object name="mailboxlist" id="mailboxlist" class="listing" folder_filter="mail" unreadwrap="%s" /> |
| | | <roundcube:object name="mailboxlist" id="mailboxlist" class="treelist listing" folder_filter="mail" unreadwrap="%s" /> |
| | | </div> |
| | | </div> |
| | | |
| | |
| | | /** |
| | | * Roundcube functions for default skin interface |
| | | * |
| | | * Copyright (c) 2013, The Roundcube Dev Team |
| | | * Copyright (c) 2011, The Roundcube Dev Team |
| | | * |
| | | * The contents are subject to the Creative Commons Attribution-ShareAlike |
| | | * License. It is allowed to copy, distribute, transmit and to adapt the work |
| | |
| | | /*** 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', |
| | |
| | | }); |
| | | } |
| | | |
| | | 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() |
| | | { |
| | |
| | | $dialog.dialog('close'); |
| | | return; |
| | | } |
| | | |
| | | |
| | | // add icons to clone file input field |
| | | if (rcmail.env.action == 'compose' && !$dialog.data('extended')) { |
| | | $('<a>') |
| | |
| | | this.delay = 500; |
| | | |
| | | this.top |
| | | .mouseenter(function() { if (rcmail.drag_active) ref.ts = window.setTimeout(function() { ref.scroll('down'); }, ref.delay); }) |
| | | .mouseenter(function() { ref.ts = window.setTimeout(function() { ref.scroll('down'); }, ref.delay); }) |
| | | .mouseout(function() { if (ref.ts) window.clearTimeout(ref.ts); }); |
| | | |
| | | this.bottom |
| | | .mouseenter(function() { if (rcmail.drag_active) ref.ts = window.setTimeout(function() { ref.scroll('up'); }, ref.delay); }) |
| | | .mouseenter(function() { ref.ts = window.setTimeout(function() { ref.scroll('up'); }, ref.delay); }) |
| | | .mouseout(function() { if (ref.ts) window.clearTimeout(ref.ts); }); |
| | | |
| | | this.scroll = function(dir) |