diff options
26 files changed, 1823 insertions, 803 deletions
| diff --git a/config/main.inc.php.dist b/config/main.inc.php.dist index 84a0a7a61..5c1bb1a07 100644 --- a/config/main.inc.php.dist +++ b/config/main.inc.php.dist @@ -651,6 +651,7 @@ $rcmail_config['ldap_public']['Verisign'] = array(      'phone:work'  => 'telephoneNumber',      'phone:mobile' => 'mobile',      'phone:pager' => 'pager', +    'phone:workfax' => 'facsimileTelephoneNumber',      'street'      => 'street',      'zipcode'     => 'postalCode',      'region'      => 'st', @@ -661,9 +662,8 @@ $rcmail_config['ldap_public']['Verisign'] = array(      'department'   => 'ou',      'jobtitle'     => 'title',      'notes'        => 'description', +    'photo'        => 'jpegPhoto',      // these currently don't work: -    // 'phone:workfax' => 'facsimileTelephoneNumber', -    // 'photo'         => 'jpegPhoto',      // 'manager'       => 'manager',      // 'assistant'     => 'secretary',    ), @@ -674,27 +674,48 @@ $rcmail_config['ldap_public']['Verisign'] = array(    // 'uid'  => 'md5(microtime())',               // You may specify PHP code snippets which are then eval'ed     // 'mail' => '{givenname}.{sn}@mydomain.com',  // or composite strings with placeholders for existing attributes    ), -  'sort'          => 'cn',    // The field to sort the listing by. -  'scope'         => 'sub',   // search mode: sub|base|list -  'filter'        => '(objectClass=inetOrgPerson)',      // used for basic listing (if not empty) and will be &'d with search queries. example: status=act -  'fuzzy_search'  => true,    // server allows wildcard search -  'vlv'           => false,   // Enable Virtual List View to more efficiently fetch paginated data (if server supports it) -  'numsub_filter' => '(objectClass=organizationalUnit)',   // with VLV, we also use numSubOrdinates to query the total number of records. Set this filter to get all numSubOrdinates attributes for counting -  'sizelimit'     => '0',     // Enables you to limit the count of entries fetched. Setting this to 0 means no limit. -  'timelimit'     => '0',     // Sets the number of seconds how long is spend on the search. Setting this to 0 means no limit. -  'referrals'     => true|false,  // Sets the LDAP_OPT_REFERRALS option. Mostly used in multi-domain Active Directory setups +  'sort'           => 'cn',         // The field to sort the listing by. +  'scope'          => 'sub',        // search mode: sub|base|list +  'filter'         => '(objectClass=inetOrgPerson)',      // used for basic listing (if not empty) and will be &'d with search queries. example: status=act +  'fuzzy_search'   => true,         // server allows wildcard search +  'vlv'            => false,        // Enable Virtual List View to more efficiently fetch paginated data (if server supports it) +  'vlv_search'     => false,        // Use Virtual List View functions for autocompletion searches (if server supports it) +  'numsub_filter'  => '(objectClass=organizationalUnit)',   // with VLV, we also use numSubOrdinates to query the total number of records. Set this filter to get all numSubOrdinates attributes for counting +  'config_root_dn' => 'cn=config',  // Root DN to search config entries (e.g. vlv indexes) +  'sizelimit'      => '0',          // Enables you to limit the count of entries fetched. Setting this to 0 means no limit. +  'timelimit'      => '0',          // Sets the number of seconds how long is spend on the search. Setting this to 0 means no limit. +  'referrals'      => false,        // Sets the LDAP_OPT_REFERRALS option. Mostly used in multi-domain Active Directory setups    // definition for contact groups (uncomment if no groups are supported)    // for the groups base_dn, the user replacements %fu, %u, $d and %dc work as for base_dn (see above)    // if the groups base_dn is empty, the contact base_dn is used for the groups as well    // -> in this case, assure that groups and contacts are separated due to the concernig filters!  -  'groups'        => array( -    'base_dn'     => '', -    'scope'       => 'sub',   // search mode: sub|base|list -    'filter'      => '(objectClass=groupOfNames)', +  'groups'  => array( +    'base_dn'        => '', +    'scope'          => 'sub',       // Search mode: sub|base|list +    'filter'         => '(objectClass=groupOfNames)',      'object_classes' => array("top", "groupOfNames"), -    'member_attr'  => 'member',   // name of the member attribute, e.g. uniqueMember -    'name_attr'    => 'cn',       // attribute to be used as group name +    'member_attr'    => 'member',   // Name of the member attribute, e.g. uniqueMember +    'name_attr'      => 'cn',       // Attribute to be used as group name +    'member_filter'  => '(objectclass=*)',  // Optional filter to use when querying for group members +    'vlv'            => false,      // Use VLV controls to list groups +  ), +  // this configuration replaces the regular groups listing in the directory tree with +  // a hard-coded list of groups, each listing entries with the configured base DN and filter. +  // if the 'groups' option from above is set, it'll be shown as the first entry with the name 'Groups' +  'group_filters' => array( +    'departments' => array( +      'name'    => 'Company Departments', +      'scope'   => 'list', +      'base_dn' => 'ou=Groups,dc=mydomain,dc=com', +      'filter'  => '(|(objectclass=groupofuniquenames)(objectclass=groupofurls))', +    ), +    'customers' => array( +      'name'    => 'Customers', +      'scope'   => 'sub', +      'base_dn' => 'ou=Customers,dc=mydomain,dc=com', +      'filter'  => '(objectClass=inetOrgPerson)', +    ),    ),  );  */ diff --git a/program/include/rcmail.php b/program/include/rcmail.php index 39d804d1f..eff0425c8 100644 --- a/program/include/rcmail.php +++ b/program/include/rcmail.php @@ -294,7 +294,7 @@ class rcmail extends rcube          $list[$id] = array(            'id'       => $id,            'name'     => html::quote($prop['name']), -          'groups'   => is_array($prop['groups']), +          'groups'   => !empty($prop['groups']) || !empty($prop['group_filters']),            'readonly' => !$prop['writable'],            'hidden'   => $prop['hidden'],            'autocomplete' => in_array($id, $autocomplete) diff --git a/program/js/app.js b/program/js/app.js index cb08ce29d..41be99c0f 100644 --- a/program/js/app.js +++ b/program/js/app.js @@ -255,7 +255,8 @@ function rcube_webmail()            }          }          else if (this.env.action == 'compose') { -          this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel', 'toggle-editor', 'list-adresses', 'search', 'reset-search', 'extwin']; +          this.env.address_group_stack = []; +          this.env.compose_commands = ['send-attachment', 'remove-attachment', 'send', 'cancel', 'toggle-editor', 'list-adresses', 'pushgroup', 'search', 'reset-search', 'extwin'];            if (this.env.drafts_mailbox)              this.env.compose_commands.push('savedraft') @@ -323,11 +324,13 @@ function rcube_webmail()          break;        case 'addressbook': +        this.env.address_group_stack = []; +          if (this.gui_objects.folderlist)            this.env.contactfolders = $.extend($.extend({}, this.env.address_sources), this.env.contactgroups);          this.enable_command('add', 'import', this.env.writable_source); -        this.enable_command('list', 'listgroup', 'listsearch', 'advanced-search', true); +        this.enable_command('list', 'listgroup', 'pushgroup', 'popgroup', 'listsearch', 'advanced-search', true);          if (this.gui_objects.contactslist) {            this.contact_list = new rcube_list_widget(this.gui_objects.contactslist, @@ -1097,11 +1100,25 @@ function rcube_webmail()          }          break; +      case 'pushgroup': +        // add group ID to stack +        this.env.address_group_stack.push(props.id); +        if (obj && event) +          rcube_event.cancel(event); +        case 'listgroup':          this.reset_qsearch();          this.list_contacts(props.source, props.id);          break; +      case 'popgroup': +        if (this.env.address_group_stack.length > 1) { +          this.env.address_group_stack.pop(); +          this.reset_qsearch(); +          this.list_contacts(props.source, this.env.address_group_stack[this.env.address_group_stack.length-1]); +        } +        break; +        case 'import-messages':          var form = props || this.gui_objects.importform;          $('input[name="_unlock"]', form).val(this.set_busy(true, 'importwait')); @@ -3091,7 +3108,13 @@ function rcube_webmail()    this.compose_recipient_select = function(list)    { -    this.enable_command('add-recipient', list.selection.length > 0); +    var id, n, recipients = 0; +    for (n=0; n < list.selection.length; n++) { +      id = list.selection[n]; +      if (this.env.contactdata[id]) +        recipients++; +    } +    this.enable_command('add-recipient', recipients);    };    this.compose_add_recipient = function(field) @@ -4109,7 +4132,7 @@ function rcube_webmail()      if (this.preview_timer)        clearTimeout(this.preview_timer); -    var n, id, sid, ref = this, writable = false, +    var n, id, sid, contact, ref = this, writable = false,        source = this.env.source ? this.env.address_sources[this.env.source] : null;      // we don't have dblclick handler here, so use 200 instead of this.dblclick_time @@ -4124,29 +4147,35 @@ function rcube_webmail()        // we'll also need to know sources used in selection for copy        // and group-addmember operations (drag&drop)        this.env.selection_sources = []; -      if (!source) { -        for (n in list.selection) { + +      if (source) { +        this.env.selection_sources.push(this.env.source); +      } + +      for (n in list.selection) { +        contact = list.data[list.selection[n]]; +        if (!source) {            sid = String(list.selection[n]).replace(/^[^-]+-/, '');            if (sid && this.env.address_sources[sid]) { -            writable = writable || !this.env.address_sources[sid].readonly; +            writable = writable || (!this.env.address_sources[sid].readonly && !contact.readonly);              this.env.selection_sources.push(sid);            }          } -        this.env.selection_sources = $.unique(this.env.selection_sources); -      } -      else { -        this.env.selection_sources.push(this.env.source); -        writable = !source.readonly; +        else { +          writable = writable || (!source.readonly && !contact.readonly); +        }        } + +      this.env.selection_sources = $.unique(this.env.selection_sources);      }      // if a group is currently selected, and there is at least one contact selected      // thend we can enable the group-remove-selected command -    this.enable_command('group-remove-selected', this.env.group && list.selection.length > 0); +    this.enable_command('group-remove-selected', this.env.group && list.selection.length > 0 && writable);      this.enable_command('compose', this.env.group || list.selection.length > 0);      this.enable_command('export-selected', list.selection.length > 0);      this.enable_command('edit', id && writable); -    this.enable_command('delete', list.selection.length && writable); +    this.enable_command('delete', list.selection.length > 0 && writable);      return false;    }; @@ -4174,11 +4203,29 @@ function rcube_webmail()      else if (!this.env.search_request)        folder = group ? 'G'+src+group : src; -    this.select_folder(folder, '', true); -      this.env.source = src;      this.env.group = group; +    // truncate groups listing stack +    var index = $.inArray(this.env.group, this.env.address_group_stack); +    if (index < 0) +      this.env.address_group_stack = []; +    else +      this.env.address_group_stack = this.env.address_group_stack.slice(0,index); + +    // make sure the current group is on top of the stack +    if (this.env.group) { +      this.env.address_group_stack.push(this.env.group); + +      // mark the first group on the stack as selected in the directory list +      folder = 'G'+src+this.env.address_group_stack[0]; +    } +    else if (this.gui_objects.addresslist_title) { +        $(this.gui_objects.addresslist_title).html(this.get_label('contacts')); +    } + +    this.select_folder(folder, '', true); +      // load contacts remotely      if (this.gui_objects.contactslist) {        this.list_contacts_remote(src, group, page); @@ -4233,16 +4280,38 @@ function rcube_webmail()    this.list_contacts_clear = function()    { +    this.contact_list.data = {};      this.contact_list.clear(true);      this.show_contentframe(false);      this.enable_command('delete', false);      this.enable_command('compose', this.env.group ? true : false);    }; +  this.set_group_prop = function(prop) +  { +    if (this.gui_objects.addresslist_title) { +      var boxtitle = $(this.gui_objects.addresslist_title).html('');  // clear contents + +      // add link to pop back to parent group +      if (this.env.address_group_stack.length > 1) { +        $('<a href="#list">...</a>') +          .addClass('poplink') +          .appendTo(boxtitle) +          .click(function(e){ return ref.command('popgroup','',this); }); +        boxtitle.append(' » '); +      } + +      boxtitle.append($('<span>'+prop.name+'</span>')); +    } + +    this.triggerEvent('groupupdate', prop); +  }; +    // load contact record    this.load_contact = function(cid, action, framed)    { -    var win, url = {}, target = window; +    var win, url = {}, target = window, +      rec = this.contact_list ? this.contact_list.data[cid] : null;      if (win = this.get_frame_window(this.env.contentframe)) {        url._framed = 1; @@ -4252,7 +4321,9 @@ function rcube_webmail()        // load dummy content, unselect selected row(s)        if (!cid)          this.contact_list.clear_selection(); -      this.enable_command('delete', 'compose', 'export-selected', cid); + +      this.enable_command('compose', rec && rec.email); +      this.enable_command('export-selected', rec && rec._type != 'group');      }      else if (framed)        return false; @@ -4362,7 +4433,7 @@ function rcube_webmail()    };    // update a contact record in the list -  this.update_contact_row = function(cid, cols_arr, newcid, source) +  this.update_contact_row = function(cid, cols_arr, newcid, source, data)    {      var c, row, list = this.contact_list; @@ -4376,10 +4447,11 @@ function rcube_webmail()      }      list.update_row(cid, cols_arr, newcid, true); +    list.data[cid] = data;    };    // add row to contacts list -  this.add_contact_row = function(cid, cols, classes) +  this.add_contact_row = function(cid, cols, classes, data)    {      if (!this.gui_objects.contactslist)        return false; @@ -4401,6 +4473,8 @@ function rcube_webmail()        row.cols.push(col);      } +    // store data in list member +    list.data[cid] = data;      list.insert_row(row);      this.enable_command('export', list.rowcount > 0); diff --git a/program/lib/Roundcube/rcube_ldap.php b/program/lib/Roundcube/rcube_ldap.php index 39a48b456..ca50b6056 100644 --- a/program/lib/Roundcube/rcube_ldap.php +++ b/program/lib/Roundcube/rcube_ldap.php @@ -3,8 +3,8 @@  /*   +-----------------------------------------------------------------------+   | This file is part of the Roundcube Webmail client                     | - | Copyright (C) 2006-2012, The Roundcube Dev Team                       | - | Copyright (C) 2011-2012, Kolab Systems AG                             | + | Copyright (C) 2006-2013, The Roundcube Dev Team                       | + | Copyright (C) 2011-2013, Kolab Systems AG                             |   |                                                                       |   | Licensed under the GNU General Public License version 3 or            |   | any later version with exceptions for skins & plugins.                | @@ -36,7 +36,7 @@ class rcube_ldap extends rcube_addressbook      public $coltypes = array();      /** private properties */ -    protected $conn; +    protected $ldap;      protected $prop = array();      protected $fieldmap = array();      protected $sub_filter; @@ -51,9 +51,6 @@ class rcube_ldap extends rcube_addressbook      private $group_url = null;      private $cache; -    private $vlv_active = false; -    private $vlv_count = 0; -      /**      * Object constructor @@ -66,6 +63,8 @@ class rcube_ldap extends rcube_addressbook      {          $this->prop = $p; +        $fetch_attributes = array('objectClass'); +          if (isset($p['searchonly']))              $this->searchonly = $p['searchonly']; @@ -82,6 +81,12 @@ class rcube_ldap extends rcube_addressbook                  $this->prop['groups']['name_attr'] = 'cn';              if (empty($this->prop['groups']['scope']))                  $this->prop['groups']['scope'] = 'sub'; + +            // add group name attrib to the list of attributes to be fetched +            $fetch_attributes[] = $this->prop['groups']['name_attr']; +        } +        else if (is_array($p['group_filters']) && count($p['group_filters'])) { +            $this->groups = true;          }          // fieldmap property is given @@ -192,6 +197,17 @@ class rcube_ldap extends rcube_addressbook          $this->cache = $rcube->get_cache($cache_name, $cache_type, $cache_ttl); +        // determine which attributes to fetch +        $this->prop['attributes'] = array_merge(array_values($this->fieldmap), $fetch_attributes); +        $this->prop['list_attributes'] = $fetch_attributes; +        foreach ($rcube->config->get('contactlist_fields') as $col) { +            $this->prop['list_attributes'] = array_merge($this->prop['list_attributes'], $this->_map_field($col)); +        } + +        // initialize ldap wrapper object +        $this->ldap = new rcube_ldap_generic($this->prop, true); +        $this->ldap->set_cache($this->cache); +          $this->_connect();      } @@ -203,49 +219,18 @@ class rcube_ldap extends rcube_addressbook      {          $rcube = rcube::get_instance(); -        if (!function_exists('ldap_connect')) -            rcube::raise_error(array('code' => 100, 'type' => 'ldap', -                'file' => __FILE__, 'line' => __LINE__, -                'message' => "No ldap support in this installation of PHP"), -                true, true); - -        if (is_resource($this->conn)) +        if ($this->ready)              return true;          if (!is_array($this->prop['hosts']))              $this->prop['hosts'] = array($this->prop['hosts']); -        if (empty($this->prop['ldap_version'])) -            $this->prop['ldap_version'] = 3; -          // try to connect + bind for every host configured          // with OpenLDAP 2.x ldap_connect() always succeeds but ldap_bind will fail if host isn't reachable          // see http://www.php.net/manual/en/function.ldap-connect.php          foreach ($this->prop['hosts'] as $host) { -            $host     = rcube_utils::idn_to_ascii(rcube_utils::parse_host($host)); -            $hostname = $host.($this->prop['port'] ? ':'.$this->prop['port'] : ''); - -            $this->_debug("C: Connect [$hostname] [{$this->prop['name']}]"); - -            if ($lc = @ldap_connect($host, $this->prop['port'])) { -                if ($this->prop['use_tls'] === true) -                    if (!ldap_start_tls($lc)) -                        continue; - -                $this->_debug("S: OK"); - -                ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->prop['ldap_version']); -                $this->prop['host'] = $host; -                $this->conn = $lc; - -                if (!empty($this->prop['network_timeout'])) -                  ldap_set_option($lc, LDAP_OPT_NETWORK_TIMEOUT, $this->prop['network_timeout']); - -                if (isset($this->prop['referrals'])) -                    ldap_set_option($lc, LDAP_OPT_REFERRALS, $this->prop['referrals']); -            } -            else { -                $this->_debug("S: NOT OK"); +            // skip host if connection failed +            if (!$this->ldap->connect($host)) {                  continue;              } @@ -260,7 +245,7 @@ class rcube_ldap extends rcube_addressbook              $this->base_dn        = $this->prop['base_dn'];              $this->groups_base_dn = ($this->prop['groups']['base_dn']) ? -            $this->prop['groups']['base_dn'] : $this->base_dn; +                $this->prop['groups']['base_dn'] : $this->base_dn;              // User specific access, generate the proper values to use.              if ($this->prop['user_specific']) { @@ -281,7 +266,8 @@ class rcube_ldap extends rcube_addressbook                  if ($this->prop['search_base_dn'] && $this->prop['search_filter']) {                      if (!empty($this->prop['search_bind_dn']) && !empty($this->prop['search_bind_pw'])) { -                        $this->bind($this->prop['search_bind_dn'], $this->prop['search_bind_pw']); +                        if (!$this->ldap->bind($this->prop['search_bind_dn'], $this->prop['search_bind_pw'])) +                            continue;  // bind failed, try neyt host                      }                      // Search for the dn to use to authenticate @@ -290,10 +276,11 @@ class rcube_ldap extends rcube_addressbook                      $this->_debug("S: searching with base {$this->prop['search_base_dn']} for {$this->prop['search_filter']}"); -                    $res = @ldap_search($this->conn, $this->prop['search_base_dn'], $this->prop['search_filter'], array('uid')); +                    // TODO: use $this->ldap->search() here +                    $res = @ldap_search($this->ldap->conn, $this->prop['search_base_dn'], $this->prop['search_filter'], array('uid'));                      if ($res) { -                        if (($entry = ldap_first_entry($this->conn, $res)) -                            && ($bind_dn = ldap_get_dn($this->conn, $entry)) +                        if (($entry = ldap_first_entry($this->ldap->conn, $res)) +                            && ($bind_dn = ldap_get_dn($this->ldap->conn, $entry))                          ) {                              $this->_debug("S: search returned dn: $bind_dn");                              $dn = ldap_explode_dn($bind_dn, 1); @@ -301,7 +288,7 @@ class rcube_ldap extends rcube_addressbook                          }                      }                      else { -                        $this->_debug("S: ".ldap_error($this->conn)); +                        $this->_debug("S: ".ldap_error($this->ldap->conn));                      }                      // DN not found @@ -333,13 +320,13 @@ class rcube_ldap extends rcube_addressbook              }              else {                  if (!empty($bind_dn)) { -                    $this->ready = $this->bind($bind_dn, $bind_pass); +                    $this->ready = $this->ldap->bind($bind_dn, $bind_pass);                  }                  else if (!empty($this->prop['auth_cid'])) { -                    $this->ready = $this->sasl_bind($this->prop['auth_cid'], $bind_pass, $bind_user); +                    $this->ready = $this->ldap->sasl_bind($this->prop['auth_cid'], $bind_pass, $bind_user);                  }                  else { -                    $this->ready = $this->sasl_bind($bind_user, $bind_pass); +                    $this->ready = $this->ldap->sasl_bind($bind_user, $bind_pass);                  }              } @@ -350,10 +337,10 @@ class rcube_ldap extends rcube_addressbook          }  // end foreach hosts -        if (!is_resource($this->conn)) { +        if (!is_resource($this->ldap->conn)) {              rcube::raise_error(array('code' => 100, 'type' => 'ldap',                  'file' => __FILE__, 'line' => __LINE__, -                'message' => "Could not connect to any LDAP server, last tried $hostname"), true); +                'message' => "Could not connect to any LDAP server, last tried $host"), true);              return false;          } @@ -363,112 +350,47 @@ class rcube_ldap extends rcube_addressbook      /** -     * Bind connection with (SASL-) user and password -     * -     * @param string $authc Authentication user -     * @param string $pass  Bind password -     * @param string $authz Autorization user -     * -     * @return boolean True on success, False on error +     * Close connection to LDAP server       */ -    public function sasl_bind($authc, $pass, $authz=null) +    function close()      { -        if (!$this->conn) { -            return false; -        } - -        if (!function_exists('ldap_sasl_bind')) { -            rcube::raise_error(array('code' => 100, 'type' => 'ldap', -                'file' => __FILE__, 'line' => __LINE__, -                'message' => "Unable to bind: ldap_sasl_bind() not exists"), -                true, true); -        } - -        if (!empty($authz)) { -            $authz = 'u:' . $authz; -        } - -        if (!empty($this->prop['auth_method'])) { -            $method = $this->prop['auth_method']; +        if ($this->ldap) { +            $this->ldap->close();          } -        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 +     * Returns address book name       * -     * @return boolean True on success, False on error +     * @return string Address book name       */ -    public function bind($dn, $pass) +    function get_name()      { -        if (!$this->conn) { -            return false; -        } - -        $this->_debug("C: Bind [dn: $dn] [pass: $pass]"); - -        if (@ldap_bind($this->conn, $dn, $pass)) { -            $this->_debug("S: OK"); -            return true; -        } - -        $this->_debug("S: ".ldap_error($this->conn)); - -        rcube::raise_error(array( -            'code' => ldap_errno($this->conn), 'type' => 'ldap', -            'file' => __FILE__, 'line' => __LINE__, -            'message' => "Bind failed for dn=$dn: ".ldap_error($this->conn)), -            true); - -        return false; +        return $this->prop['name'];      }      /** -     * Close connection to LDAP server +     * Set internal list page +     * +     * @param  number  Page number to list       */ -    function close() +    function set_page($page)      { -        if ($this->conn) -        { -            $this->_debug("C: Close"); -            ldap_unbind($this->conn); -            $this->conn = null; -        } +        $this->list_page = (int)$page; +        $this->ldap->set_vlv_page($this->list_page, $this->page_size);      } -      /** -     * Returns address book name +     * Set internal page size       * -     * @return string Address book name +     * @param  number  Number of records to display on one page       */ -    function get_name() +    function set_pagesize($size)      { -        return $this->prop['name']; +        $this->page_size = (int)$size; +        $this->ldap->set_vlv_page($this->list_page, $this->page_size);      } @@ -528,16 +450,14 @@ class rcube_ldap extends rcube_addressbook       */      function list_records($cols=null, $subset=0)      { -        if ($this->prop['searchonly'] && empty($this->filter) && !$this->group_id) -        { +        if ($this->prop['searchonly'] && empty($this->filter) && !$this->group_id) {              $this->result = new rcube_result_set(0);              $this->result->searchonly = true;              return $this->result;          }          // fetch group members recursively -        if ($this->group_id && $this->group_data['dn']) -        { +        if ($this->group_id && $this->group_data['dn']) {              $entries = $this->list_group_members($this->group_data['dn']);              // make list of entries unique and sort it @@ -551,34 +471,34 @@ class rcube_ldap extends rcube_addressbook              $entries['count'] = count($entries);              $this->result = new rcube_result_set($entries['count'], ($this->list_page-1) * $this->page_size);          } -        else -        { -            // add general filter to query -            if (!empty($this->prop['filter']) && empty($this->filter)) -                $this->set_search_set($this->prop['filter']); +        else { +            $prop = $this->group_id ? $this->group_data : $this->prop; + +            // use global search filter +            if (!empty($this->filter)) +                $prop['filter'] = $this->filter;              // exec LDAP search if no result resource is stored -            if ($this->conn && !$this->ldap_result) -                $this->_exec_search(); +            if ($this->ready && !$this->ldap_result) +                $this->ldap_result = $this->ldap->search($prop['base_dn'], $prop['filter'], $prop['scope'], $this->prop['attributes'], $prop);              // count contacts for this user              $this->result = $this->count();              // we have a search result resource -            if ($this->ldap_result && $this->result->count > 0) -            { +            if ($this->ldap_result && $this->result->count > 0) {                  // sorting still on the ldap server -                if ($this->sort_col && $this->prop['scope'] !== 'base' && !$this->vlv_active) -                    ldap_sort($this->conn, $this->ldap_result, $this->sort_col); +                if ($this->sort_col && $prop['scope'] !== 'base' && !$this->ldap->vlv_active) +                    $this->ldap_result->sort($this->sort_col);                  // get all entries from the ldap server -                $entries = ldap_get_entries($this->conn, $this->ldap_result); +                $entries = $this->ldap_result->entries();              }          }  // end else          // start and end of the page -        $start_row = $this->vlv_active ? 0 : $this->result->first; +        $start_row = $this->ldap->vlv_active ? 0 : $this->result->first;          $start_row = $subset < 0 ? $start_row + $this->page_size + $subset : $start_row;          $last_row = $this->result->first + $this->page_size;          $last_row = $subset != 0 ? $start_row + abs($subset) : $last_row; @@ -603,25 +523,20 @@ class rcube_ldap extends rcube_addressbook          // fetch group object          if (empty($entries)) { -            $result = @ldap_read($this->conn, $dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL')); -            if ($result === false) -            { -                $this->_debug("S: ".ldap_error($this->conn)); +            $this->_debug("C: Read Group [dn: $dn]"); +            $entries = $this->ldap->read_entries($dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL')); +            if ($entries === false) {                  return $group_members;              } - -            $entries = @ldap_get_entries($this->conn, $result);          } -        for ($i=0; $i < $entries['count']; $i++) -        { +        for ($i=0; $i < $entries['count']; $i++) {              $entry = $entries[$i];              if (empty($entry['objectclass']))                  continue; -            foreach ((array)$entry['objectclass'] as $objectclass) -            { +            foreach ((array)$entry['objectclass'] as $objectclass) {                  switch (strtolower($objectclass)) {                      case "group":                      case "groupofnames": @@ -662,24 +577,19 @@ class rcube_ldap extends rcube_addressbook              return $group_members;          // read these attributes for all members -        $attrib = $count ? array('dn') : array_values($this->fieldmap); -        $attrib[] = 'objectClass'; +        $attrib = $count ? array('dn','objectClass') : $this->prop['list_attributes'];          $attrib[] = 'member';          $attrib[] = 'uniqueMember';          $attrib[] = 'memberURL'; -        for ($i=0; $i < $entry[$attr]['count']; $i++) -        { +        $filter = $this->prop['groups']['member_filter'] ? $this->prop['groups']['member_filter'] : '(objectclass=*)'; + +        for ($i=0; $i < $entry[$attr]['count']; $i++) {              if (empty($entry[$attr][$i]))                  continue; -            $result = @ldap_read($this->conn, $entry[$attr][$i], '(objectclass=*)', -                $attrib, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']); - -            $members = @ldap_get_entries($this->conn, $result); -            if ($members == false) -            { -                $this->_debug("S: ".ldap_error($this->conn)); +            $members = $this->ldap->read_entries($entry[$attr][$i], $filter, $attrib); +            if ($members == false) {                  $members = array();              } @@ -705,34 +615,22 @@ class rcube_ldap extends rcube_addressbook      {          $group_members = array(); -        for ($i=0; $i < $entry['memberurl']['count']; $i++) -        { +        for ($i=0; $i < $entry['memberurl']['count']; $i++) {              // extract components from url              if (!preg_match('!ldap:///([^\?]+)\?\?(\w+)\?(.*)$!', $entry['memberurl'][$i], $m))                  continue;              // add search filter if any              $filter = $this->filter ? '(&(' . $m[3] . ')(' . $this->filter . '))' : $m[3]; -            $func = $m[2] == 'sub' ? 'ldap_search' : ($m[2] == 'base' ? 'ldap_read' : 'ldap_list'); - -            $attrib = $count ? array('dn') : array_values($this->fieldmap); -            if ($result = @$func($this->conn, $m[1], $filter, -                $attrib, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']) -            ) { -                $this->_debug("S: ".ldap_count_entries($this->conn, $result)." record(s) for ".$m[1]); -            } -            else { -                $this->_debug("S: ".ldap_error($this->conn)); -                return $group_members; -            } - -            $entries = @ldap_get_entries($this->conn, $result); -            for ($j = 0; $j < $entries['count']; $j++) -            { -                if ($nested_group_members = $this->list_group_members($entries[$j]['dn'], $count)) -                    $group_members = array_merge($group_members, $nested_group_members); -                else -                    $group_members[] = $entries[$j]; +            $attrs = $count ? array('dn','objectClass') : $this->prop['list_attributes']; +            if ($result = $this->ldap->search($m[1], $filter, $m[2], $attrs, $this->group_data)) { +                $entries = $result->entries(); +                for ($j = 0; $j < $entries['count']; $j++) { +                    if (self::is_group_entry($entries[$j]) && ($nested_group_members = $this->list_group_members($entries[$j]['dn'], $count))) +                        $group_members = array_merge($group_members, $nested_group_members); +                    else +                        $group_members[] = $entries[$j]; +                }              }          } @@ -768,14 +666,11 @@ class rcube_ldap extends rcube_addressbook          $mode = intval($mode);          // special treatment for ID-based search -        if ($fields == 'ID' || $fields == $this->primary_key) -        { +        if ($fields == 'ID' || $fields == $this->primary_key) {              $ids = !is_array($value) ? explode(',', $value) : $value;              $result = new rcube_result_set(); -            foreach ($ids as $id) -            { -                if ($rec = $this->get_record($id, true)) -                { +            foreach ($ids as $id) { +                if ($rec = $this->get_record($id, true)) {                      $result->add($rec);                      $result->count++;                  } @@ -787,34 +682,20 @@ class rcube_ldap extends rcube_addressbook          $rcube = rcube::get_instance();          $list_fields = $rcube->config->get('contactlist_fields'); -        if ($this->prop['vlv_search'] && $this->conn && join(',', (array)$fields) == join(',', $list_fields)) -        { -            // add general filter to query -            if (!empty($this->prop['filter']) && empty($this->filter)) -                $this->set_search_set($this->prop['filter']); - -            // set VLV controls with encoded search string -            $this->_vlv_set_controls($this->prop, $this->list_page, $this->page_size, $value); - -            $function = $this->_scope2func($this->prop['scope']); -            $this->ldap_result = @$function($this->conn, $this->base_dn, $this->filter ? $this->filter : '(objectclass=*)', -                array_values($this->fieldmap), 0, $this->page_size, (int)$this->prop['timelimit']); - +        if ($this->prop['vlv_search'] && $this->ready && join(',', (array)$fields) == join(',', $list_fields)) {              $this->result = new rcube_result_set(0); -            if (!$this->ldap_result) { -                $this->_debug("S: ".ldap_error($this->conn)); +            $search_suffix = $this->prop['fuzzy_search'] && $mode != 1 ? '*' : ''; +            $ldap_data = $this->ldap->search($this->base_dn, $this->prop['filter'], $this->prop['scope'], $this->prop['attributes'], +                array('search' => $value . $search_suffix /*, 'sort' => $this->prop['sort'] */)); +            if ($ldap_data === false) {                  return $this->result;              } -            $this->_debug("S: ".ldap_count_entries($this->conn, $this->ldap_result)." record(s)"); -              // get all entries of this page and post-filter those that really match the query -            $search  = mb_strtolower($value); -            $entries = ldap_get_entries($this->conn, $this->ldap_result); - -            for ($i = 0; $i < $entries['count']; $i++) { -                $rec = $this->_ldap2result($entries[$i]); +            $search = mb_strtolower($value); +            foreach ($ldap_data as $i => $entry) { +                $rec = $this->_ldap2result($entry);                  foreach ($fields as $f) {                      foreach ((array)$rec[$f] as $val) {                          if ($this->compare_search_value($f, $val, $search, $mode)) { @@ -840,31 +721,27 @@ class rcube_ldap extends rcube_addressbook              }          } -        if ($fields == '*') -        { +        if ($fields == '*') {              // search_fields are required for fulltext search -            if (empty($this->prop['search_fields'])) -            { +            if (empty($this->prop['search_fields'])) {                  $this->set_error(self::ERROR_SEARCH, 'nofulltextsearch');                  $this->result = new rcube_result_set();                  return $this->result;              } -            if (is_array($this->prop['search_fields'])) -            { +            if (is_array($this->prop['search_fields'])) {                  foreach ($this->prop['search_fields'] as $field) { -                    $filter .= "($field=$wp" . $this->_quote_string($value) . "$ws)"; +                    $filter .= "($field=$wp" . rcube_ldap_generic::quote_string($value) . "$ws)";                  }              }          } -        else -        { +        else {              foreach ((array)$fields as $idx => $field) {                  $val = is_array($value) ? $value[$idx] : $value;                  if ($attrs = $this->_map_field($field)) {                      if (count($attrs) > 1)                          $filter .= '(|';                      foreach ($attrs as $f) -                        $filter .= "($f=$wp" . $this->_quote_string($val) . "$ws)"; +                        $filter .= "($f=$wp" . rcube_ldap_generic::quote_string($val) . "$ws)";                      if (count($attrs) > 1)                          $filter .= ')';                  } @@ -875,6 +752,8 @@ class rcube_ldap extends rcube_addressbook          // add required (non empty) fields filter          $req_filter = '';          foreach ((array)$required as $field) { +            if (in_array($field, (array)$fields))  // required field is already in search filter +                continue;              if ($attrs = $this->_map_field($field)) {                  if (count($attrs) > 1)                      $req_filter .= '(|'; @@ -897,7 +776,6 @@ class rcube_ldap extends rcube_addressbook          // set filter string and execute search          $this->set_search_set($filter); -        $this->_exec_search();          if ($select)              $this->list_records(); @@ -916,20 +794,21 @@ class rcube_ldap extends rcube_addressbook      function count()      {          $count = 0; -        if ($this->conn && $this->ldap_result) { -            $count = $this->vlv_active ? $this->vlv_count : ldap_count_entries($this->conn, $this->ldap_result); +        if ($this->ldap_result) { +            $count = $this->ldap_result->count();          }          else if ($this->group_id && $this->group_data['dn']) {              $count = count($this->list_group_members($this->group_data['dn'], true));          } -        else if ($this->conn) { -            // We have a connection but no result set, attempt to get one. -            if (empty($this->filter)) { -                // The filter is not set, set it. -                $this->filter = $this->prop['filter']; +        // We have a connection but no result set, attempt to get one. +        else if ($this->ready) { +            $prop = $this->group_id ? $this->group_data : $this->prop; + +            if (!empty($this->filter)) {  // Use global search filter +                $prop['filter'] = $this->filter;              } -            $count = (int) $this->_exec_search(true); +            $count = $this->ldap->search($prop['base_dn'], $prop['filter'], $prop['scope'], array('dn'), $prop, true);          }          return new rcube_result_set($count, ($this->list_page-1) * $this->page_size); @@ -959,28 +838,16 @@ class rcube_ldap extends rcube_addressbook      {          $res = $this->result = null; -        if ($this->conn && $dn) -        { +        if ($this->ready && $dn) {              $dn = self::dn_decode($dn); -            $this->_debug("C: Read [dn: $dn] [(objectclass=*)]"); - -            if ($ldap_result = @ldap_read($this->conn, $dn, '(objectclass=*)', array_values($this->fieldmap))) { -                $this->_debug("S: OK"); - -                $entry = ldap_first_entry($this->conn, $ldap_result); - -                if ($entry && ($rec = ldap_get_attributes($this->conn, $entry))) { -                    $rec = array_change_key_case($rec, CASE_LOWER); -                } -            } -            else { -                $this->_debug("S: ".ldap_error($this->conn)); +            if ($rec = $this->ldap->get_entry($dn)) { +                $rec = array_change_key_case($rec, CASE_LOWER);              }              // Use ldap_list to get subentries like country (c) attribute (#1488123)              if (!empty($rec) && $this->sub_filter) { -                if ($entries = $this->ldap_list($dn, $this->sub_filter, array_keys($this->prop['sub_fields']))) { +                if ($entries = $this->ldap->list_entries($dn, $this->sub_filter, array_keys($this->prop['sub_fields']))) {                      foreach ($entries as $entry) {                          $lrec = array_change_key_case($entry, CASE_LOWER);                          $rec  = array_merge($lrec, $rec); @@ -992,7 +859,7 @@ class rcube_ldap extends rcube_addressbook                  // Add in the dn for the entry.                  $rec['dn'] = $dn;                  $res = $this->_ldap2result($rec); -                $this->result = new rcube_result_set(); +                $this->result = new rcube_result_set(1);                  $this->result->add($res);              }          } @@ -1105,7 +972,7 @@ class rcube_ldap extends rcube_addressbook          }          // Build the new entries DN. -        $dn = $this->prop['LDAP_rdn'].'='.$this->_quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->base_dn; +        $dn = $this->prop['LDAP_rdn'].'='.rcube_ldap_generic::quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->base_dn;          // Remove attributes that need to be added separately (child objects)          $xfields = array(); @@ -1118,19 +985,19 @@ class rcube_ldap extends rcube_addressbook              }          } -        if (!$this->ldap_add($dn, $newentry)) { +        if (!$this->ldap->add($dn, $newentry)) {              $this->set_error(self::ERROR_SAVING, 'errorsaving');              return false;          }          foreach ($xfields as $xidx => $xf) { -            $xdn = $xidx.'='.$this->_quote_string($xf).','.$dn; +            $xdn = $xidx.'='.rcube_ldap_generic::quote_string($xf).','.$dn;              $xf = array(                  $xidx => $xf,                  'objectClass' => (array) $this->prop['sub_fields'][$xidx],              ); -            $this->ldap_add($xdn, $xf); +            $this->ldap->add($xdn, $xf);          }          $dn = self::dn_encode($dn); @@ -1236,7 +1103,7 @@ class rcube_ldap extends rcube_addressbook          // Update the entry as required.          if (!empty($deletedata)) {              // Delete the fields. -            if (!$this->ldap_mod_del($dn, $deletedata)) { +            if (!$this->ldap->mod_del($dn, $deletedata)) {                  $this->set_error(self::ERROR_SAVING, 'errorsaving');                  return false;              } @@ -1246,17 +1113,17 @@ class rcube_ldap extends rcube_addressbook              // Handle RDN change              if ($replacedata[$this->prop['LDAP_rdn']]) {                  $newdn = $this->prop['LDAP_rdn'].'=' -                    .$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true) +                    .rcube_ldap_generic::quote_string($replacedata[$this->prop['LDAP_rdn']], true)                      .','.$this->base_dn;                  if ($dn != $newdn) {                      $newrdn = $this->prop['LDAP_rdn'].'=' -                    .$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true); +                    .rcube_ldap_generic::quote_string($replacedata[$this->prop['LDAP_rdn']], true);                      unset($replacedata[$this->prop['LDAP_rdn']]);                  }              }              // Replace the fields.              if (!empty($replacedata)) { -                if (!$this->ldap_mod_replace($dn, $replacedata)) { +                if (!$this->ldap->mod_replace($dn, $replacedata)) {                      $this->set_error(self::ERROR_SAVING, 'errorsaving');                      return false;                  } @@ -1272,8 +1139,8 @@ class rcube_ldap extends rcube_addressbook          // remove sub-entries          if (!empty($subdeldata)) {              foreach ($subdeldata as $fld => $val) { -                $subdn = $fld.'='.$this->_quote_string($val).','.$dn; -                if (!$this->ldap_delete($subdn)) { +                $subdn = $fld.'='.rcube_ldap_generic::quote_string($val).','.$dn; +                if (!$this->ldap->delete($subdn)) {                      return false;                  }              } @@ -1281,7 +1148,7 @@ class rcube_ldap extends rcube_addressbook          if (!empty($newdata)) {              // Add the fields. -            if (!$this->ldap_mod_add($dn, $newdata)) { +            if (!$this->ldap->mod_add($dn, $newdata)) {                  $this->set_error(self::ERROR_SAVING, 'errorsaving');                  return false;              } @@ -1289,7 +1156,7 @@ class rcube_ldap extends rcube_addressbook          // Handle RDN change          if (!empty($newrdn)) { -            if (!$this->ldap_rename($dn, $newrdn, null, true)) { +            if (!$this->ldap->rename($dn, $newrdn, null, true)) {                  $this->set_error(self::ERROR_SAVING, 'errorsaving');                  return false;              } @@ -1300,7 +1167,7 @@ class rcube_ldap extends rcube_addressbook              // change the group membership of the contact              if ($this->groups) {                  $group_ids = $this->get_record_groups($dn); -                foreach ($group_ids as $group_id) +                foreach ($group_ids as $group_id => $group_prop)                  {                      $this->remove_from_group($group_id, $dn);                      $this->add_to_group($group_id, $newdn); @@ -1313,12 +1180,12 @@ class rcube_ldap extends rcube_addressbook          // add sub-entries          if (!empty($subnewdata)) {              foreach ($subnewdata as $fld => $val) { -                $subdn = $fld.'='.$this->_quote_string($val).','.$dn; +                $subdn = $fld.'='.rcube_ldap_generic::quote_string($val).','.$dn;                  $xf = array(                      $fld => $val,                      'objectClass' => (array) $this->prop['sub_fields'][$fld],                  ); -                $this->ldap_add($subdn, $xf); +                $this->ldap->add($subdn, $xf);              }          } @@ -1346,9 +1213,9 @@ class rcube_ldap extends rcube_addressbook              // Need to delete all sub-entries first              if ($this->sub_filter) { -                if ($entries = $this->ldap_list($dn, $this->sub_filter)) { +                if ($entries = $this->ldap->list_entries($dn, $this->sub_filter)) {                      foreach ($entries as $entry) { -                        if (!$this->ldap_delete($entry['dn'])) { +                        if (!$this->ldap->delete($entry['dn'])) {                              $this->set_error(self::ERROR_SAVING, 'errorsaving');                              return false;                          } @@ -1357,7 +1224,7 @@ class rcube_ldap extends rcube_addressbook              }              // Delete the record. -            if (!$this->ldap_delete($dn)) { +            if (!$this->ldap->delete($dn)) {                  $this->set_error(self::ERROR_SAVING, 'errorsaving');                  return false;              } @@ -1366,7 +1233,7 @@ class rcube_ldap extends rcube_addressbook              if ($this->groups) {                  $dn = self::dn_encode($dn);                  $group_ids = $this->get_record_groups($dn); -                foreach ($group_ids as $group_id) { +                foreach ($group_ids as $group_id => $group_prop) {                      $this->remove_from_group($group_id, $dn);                  }              } @@ -1381,8 +1248,8 @@ class rcube_ldap extends rcube_addressbook       */      function delete_all()      { -        //searching for contact entries -        $dn_list = $this->ldap_list($this->base_dn, $this->prop['filter'] ? $this->prop['filter'] : '(objectclass=*)'); +        // searching for contact entries +        $dn_list = $this->ldap->list_entries($this->base_dn, $this->prop['filter'] ? $this->prop['filter'] : '(objectclass=*)');          if (!empty($dn_list)) {              foreach ($dn_list as $idx => $entry) { @@ -1432,120 +1299,26 @@ class rcube_ldap extends rcube_addressbook          }      } -    /** -     * Execute the LDAP search based on the stored credentials -     */ -    private function _exec_search($count = false) -    { -        if ($this->ready) -        { -            $filter = $this->filter ? $this->filter : '(objectclass=*)'; -            $function = $this->_scope2func($this->prop['scope'], $ns_function); - -            $this->_debug("C: Search [$filter][dn: $this->base_dn]"); - -            // when using VLV, we get the total count by... -            if (!$count && $function != 'ldap_read' && $this->prop['vlv'] && !$this->group_id) { -                // ...either reading numSubOrdinates attribute -                if ($this->prop['numsub_filter'] && ($result_count = @$ns_function($this->conn, $this->base_dn, $this->prop['numsub_filter'], array('numSubOrdinates'), 0, 0, 0))) { -                    $counts = ldap_get_entries($this->conn, $result_count); -                    for ($this->vlv_count = $j = 0; $j < $counts['count']; $j++) -                        $this->vlv_count += $counts[$j]['numsubordinates'][0]; -                    $this->_debug("D: total numsubordinates = " . $this->vlv_count); -                } -                else if (!function_exists('ldap_parse_virtuallist_control'))  // ...or by fetching all records dn and count them -                    $this->vlv_count = $this->_exec_search(true); - -                $this->vlv_active = $this->_vlv_set_controls($this->prop, $this->list_page, $this->page_size); -            } - -            // only fetch dn for count (should keep the payload low) -            $attrs = $count ? array('dn') : array_values($this->fieldmap); -            if ($this->ldap_result = @$function($this->conn, $this->base_dn, $filter, -                $attrs, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']) -            ) { -                // when running on a patched PHP we can use the extended functions to retrieve the total count from the LDAP search result -                if ($this->vlv_active && function_exists('ldap_parse_virtuallist_control')) { -                    if (ldap_parse_result($this->conn, $this->ldap_result, -                        $errcode, $matcheddn, $errmsg, $referrals, $serverctrls) -                        && $serverctrls // can be null e.g. in case of adm. limit error -                    ) { -                        ldap_parse_virtuallist_control($this->conn, $serverctrls, -                            $last_offset, $this->vlv_count, $vresult); -                        $this->_debug("S: VLV result: last_offset=$last_offset; content_count=$this->vlv_count"); -                    } -                    else { -                        $this->_debug("S: ".($errmsg ? $errmsg : ldap_error($this->conn))); -                    } -                } - -                $entries_count = ldap_count_entries($this->conn, $this->ldap_result); -                $this->_debug("S: $entries_count record(s)"); - -                return $count ? $entries_count : true; -            } -            else { -                $this->_debug("S: ".ldap_error($this->conn)); -            } -        } - -        return false; -    } - -    /** -     * Choose the right PHP function according to scope property -     */ -    private function _scope2func($scope, &$ns_function = null) -    { -        switch ($scope) { -          case 'sub': -            $function = $ns_function  = 'ldap_search'; -            break; -          case 'base': -            $function = $ns_function = 'ldap_read'; -            break; -          default: -            $function = 'ldap_list'; -            $ns_function = 'ldap_read'; -            break; -        } - -        return $function; -    } - -    /** -     * Set server controls for Virtual List View (paginated listing) -     */ -    private function _vlv_set_controls($prop, $list_page, $page_size, $search = null) -    { -        $sort_ctrl = array('oid' => "1.2.840.113556.1.4.473",  'value' => $this->_sort_ber_encode((array)$prop['sort'])); -        $vlv_ctrl  = array('oid' => "2.16.840.1.113730.3.4.9", 'value' => $this->_vlv_ber_encode(($offset = ($list_page-1) * $page_size + 1), $page_size, $search), 'iscritical' => true); - -        $sort = (array)$prop['sort']; -        $this->_debug("C: set controls sort=" . join(' ', unpack('H'.(strlen($sort_ctrl['value'])*2), $sort_ctrl['value'])) . " ($sort[0]);" -            . " vlv=" . join(' ', (unpack('H'.(strlen($vlv_ctrl['value'])*2), $vlv_ctrl['value']))) . " ($offset/$page_size)"); - -        if (!ldap_set_option($this->conn, LDAP_OPT_SERVER_CONTROLS, array($sort_ctrl, $vlv_ctrl))) { -            $this->_debug("S: ".ldap_error($this->conn)); -            $this->set_error(self::ERROR_SEARCH, 'vlvnotsupported'); -            return false; -        } - -        return true; -    } -      /**       * Converts LDAP entry into an array       */      private function _ldap2result($rec)      { -        $out = array(); +        $out = array('_type' => 'person'); +        $fieldmap = $this->fieldmap;          if ($rec['dn'])              $out[$this->primary_key] = self::dn_encode($rec['dn']); -        foreach ($this->fieldmap as $rf => $lf) +        // determine record type +        if (self::is_group_entry($rec)) { +            $out['_type'] = 'group'; +            $out['readonly'] = true; +            $fieldmap['name'] = $this->prop['groups']['name_attr']; +        } + +        foreach ($fieldmap as $rf => $lf)          {              for ($i=0; $i < $rec[$lf]['count']; $i++) {                  if (!($value = $rec[$lf][$i])) @@ -1607,8 +1380,10 @@ class rcube_ldap extends rcube_addressbook              if (is_array($colprop['serialized'])) {                 foreach ($colprop['serialized'] as $subtype => $delim) {                    $key = $col.':'.$subtype; -                  foreach ((array)$save_cols[$key] as $i => $val) -                     $save_cols[$key][$i] = join($delim, array($val['street'], $val['locality'], $val['zipcode'], $val['country'])); +                  foreach ((array)$save_cols[$key] as $i => $val) { +                     $values = array($val['street'], $val['locality'], $val['zipcode'], $val['country']); +                     $save_cols[$key][$i] = count(array_filter($values)) ? join($delim, $values) : null; +                 }                 }              }          } @@ -1659,6 +1434,16 @@ class rcube_ldap extends rcube_addressbook          return (isset($aliases[$name]) ? $aliases[$name] : $name) . $suffix;      } +    /** +     * Determines whether the given LDAP entry is a group record +     */ +    private static function is_group_entry($entry) +    { +        return array_intersect( +            array('group', 'groupofnames', 'kolabgroupofnames', 'groupofuniquenames','kolabgroupofuniquenames','groupofurls'), +            array_map('strtolower', (array)$entry['objectclass']) +        ); +    }      /**       * Prints debug info to the log @@ -1684,46 +1469,15 @@ class rcube_ldap extends rcube_addressbook      /** -     * 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;          } @@ -1769,6 +1523,23 @@ class rcube_ldap extends rcube_addressbook       */      private function _fetch_groups($vlv_page = 0)      { +        // special case: list groups from 'group_filters' config +        if (!empty($this->prop['group_filters'])) { +            $groups = array(); + +            // list regular groups configuration as special filter +            if (!empty($this->prop['groups']['filter'])) { +                $id = '__groups__'; +                $groups[$id] = array('ID' => $id, 'name' => rcube_label('groups'), 'virtual' => true) + $this->prop['groups']; +            } + +            foreach ($this->prop['group_filters'] as $id => $prop) { +                $groups[$id] = $prop + array('ID' => $id, 'name' => ucfirst($id), 'virtual' => true, 'base_dn' => $this->base_dn); +            } + +            return $groups; +        } +          $base_dn = $this->groups_base_dn;          $filter = $this->prop['groups']['filter'];          $name_attr = $this->prop['groups']['name_attr']; @@ -1776,46 +1547,46 @@ class rcube_ldap extends rcube_addressbook          $sort_attrs = $this->prop['groups']['sort'] ? (array)$this->prop['groups']['sort'] : array($name_attr);          $sort_attr = $sort_attrs[0]; -        $this->_debug("C: Search [$filter][dn: $base_dn]"); +        $ldap = $this->ldap;          // use vlv to list groups          if ($this->prop['groups']['vlv']) {              $page_size = 200;              if (!$this->prop['groups']['sort'])                  $this->prop['groups']['sort'] = $sort_attrs; -            $vlv_active = $this->_vlv_set_controls($this->prop['groups'], $vlv_page+1, $page_size); + +            $ldap = clone $this->ldap; +            $ldap->set_config($this->prop['groups']); +            $ldap->set_vlv_page($vlv_page+1, $page_size);          } -        $function = $this->_scope2func($this->prop['groups']['scope']); -        $res = @$function($this->conn, $base_dn, $filter, array_unique(array('dn', 'objectClass', $name_attr, $email_attr, $sort_attr))); -        if ($res === false) -        { -            $this->_debug("S: ".ldap_error($this->conn)); +        $attrs = array_unique(array('dn', 'objectClass', $name_attr, $email_attr, $sort_attr)); +        $ldap_data = $ldap->search($base_dn, $filter, $this->prop['groups']['scope'], $attrs, $this->prop['groups']); +        if ($ldap_data === false) {              return array();          } -        $ldap_data = ldap_get_entries($this->conn, $res); -        $this->_debug("S: ".ldap_count_entries($this->conn, $res)." record(s)"); -          $groups = array();          $group_sortnames = array(); -        $group_count = $ldap_data["count"]; -        for ($i=0; $i < $group_count; $i++) -        { -            $group_name = is_array($ldap_data[$i][$name_attr]) ? $ldap_data[$i][$name_attr][0] : $ldap_data[$i][$name_attr]; -            $group_id = self::dn_encode($group_name); +        $group_count = $ldap_data->count(); +        foreach ($ldap_data as $entry) { +            if (!$entry['dn'])  // DN is mandatory +                $entry['dn'] = $ldap_data->get_dn(); + +            $group_name = is_array($entry[$name_attr]) ? $entry[$name_attr][0] : $entry[$name_attr]; +            $group_id = self::dn_encode($entry['dn']);              $groups[$group_id]['ID'] = $group_id; -            $groups[$group_id]['dn'] = $ldap_data[$i]['dn']; +            $groups[$group_id]['dn'] = $entry['dn'];              $groups[$group_id]['name'] = $group_name; -            $groups[$group_id]['member_attr'] = $this->get_group_member_attr($ldap_data[$i]['objectclass']); +            $groups[$group_id]['member_attr'] = $this->get_group_member_attr($entry['objectclass']);              // list email attributes of a group -            for ($j=0; $ldap_data[$i][$email_attr] && $j < $ldap_data[$i][$email_attr]['count']; $j++) { -                if (strpos($ldap_data[$i][$email_attr][$j], '@') > 0) -                    $groups[$group_id]['email'][] = $ldap_data[$i][$email_attr][$j]; +            for ($j=0; $entry[$email_attr] && $j < $entry[$email_attr]['count']; $j++) { +                if (strpos($entry[$email_attr][$j], '@') > 0) +                    $groups[$group_id]['email'][] = $entry[$email_attr][$j];              } -            $group_sortnames[] = mb_strtolower($ldap_data[$i][$sort_attr][0]); +            $group_sortnames[] = mb_strtolower($entry[$sort_attr][0]);          }          // recursive call can exit here @@ -1823,8 +1594,7 @@ class rcube_ldap extends rcube_addressbook              return $groups;          // call recursively until we have fetched all groups -        while ($vlv_active && $group_count == $page_size) -        { +        while ($this->prop['groups']['vlv'] && $group_count == $page_size) {              $next_page = $this->_fetch_groups(++$vlv_page);              $groups = array_merge($groups, $next_page);              $group_count = count($next_page); @@ -1841,6 +1611,38 @@ class rcube_ldap extends rcube_addressbook      }      /** +     * Fetch a group entry from LDAP and save in local cache +     */ +    private function get_group_entry($group_id) +    { +        if (($group_cache = $this->cache->get('groups')) === null) +            $group_cache = $this->_fetch_groups(); + +        // add group record to cache if it isn't yet there +        if (!isset($group_cache[$group_id])) { +            $name_attr = $this->prop['groups']['name_attr']; +            $dn = self::dn_decode($group_id); + +            $this->_debug("C: Read Group [dn: $dn]"); +            if ($list = $this->ldap->read_entries($dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL',$name_attr,$this->fieldmap['email']))) { +                $entry = $list[0]; +                $group_name = is_array($entry[$name_attr]) ? $entry[$name_attr][0] : $entry[$name_attr]; +                $group_cache[$group_id]['ID'] = $group_id; +                $group_cache[$group_id]['dn'] = $dn; +                $group_cache[$group_id]['name'] = $group_name; +                $group_cache[$group_id]['member_attr'] = $this->get_group_member_attr($entry['objectclass']); +            } +            else { +                $group_cache[$group_id] = false; +            } + +            $this->cache->set('groups', $group_cache); +        } + +        return $group_cache[$group_id]; +    } + +    /**       * Get group properties such as name and email address(es)       *       * @param string Group identifier @@ -1848,10 +1650,7 @@ class rcube_ldap extends rcube_addressbook       */      function get_group($group_id)      { -        if (($group_cache = $this->cache->get('groups')) === null) -            $group_cache = $this->_fetch_groups(); - -        $group_data = $group_cache[$group_id]; +        $group_data = $this->get_group_entry($group_id);          unset($group_data['dn'], $group_data['member_attr']);          return $group_data; @@ -1865,9 +1664,8 @@ class rcube_ldap extends rcube_addressbook       */      function create_group($group_name)      { -        $base_dn = $this->groups_base_dn; -        $new_dn = "cn=$group_name,$base_dn"; -        $new_gid = self::dn_encode($group_name); +        $new_dn = 'cn=' . rcube_ldap_generic::quote_string($group_name, true) . ',' . $this->groups_base_dn; +        $new_gid = self::dn_encode($new_dn);          $member_attr = $this->get_group_member_attr();          $name_attr = $this->prop['groups']['name_attr'] ? $this->prop['groups']['name_attr'] : 'cn'; @@ -1877,7 +1675,7 @@ class rcube_ldap extends rcube_addressbook              $member_attr => '',          ); -        if (!$this->ldap_add($new_dn, $new_entry)) { +        if (!$this->ldap->add($new_dn, $new_entry)) {              $this->set_error(self::ERROR_SAVING, 'errorsaving');              return false;          } @@ -1898,16 +1696,15 @@ class rcube_ldap extends rcube_addressbook          if (($group_cache = $this->cache->get('groups')) === null)              $group_cache = $this->_fetch_groups(); -        $base_dn = $this->groups_base_dn; -        $group_name = $group_cache[$group_id]['name']; -        $del_dn = "cn=$group_name,$base_dn"; +        $del_dn = $group_cache[$group_id]['dn']; -        if (!$this->ldap_delete($del_dn)) { +        if (!$this->ldap->delete($del_dn)) {              $this->set_error(self::ERROR_SAVING, 'errorsaving');              return false;          } -        $this->cache->remove('groups'); +        unset($group_cache[$group_id]); +        $this->cache->set('groups', $group_cache);          return true;      } @@ -1925,13 +1722,11 @@ class rcube_ldap extends rcube_addressbook          if (($group_cache = $this->cache->get('groups')) === null)              $group_cache = $this->_fetch_groups(); -        $base_dn = $this->groups_base_dn; -        $group_name = $group_cache[$group_id]['name']; -        $old_dn = "cn=$group_name,$base_dn"; -        $new_rdn = "cn=$new_name"; -        $new_gid = self::dn_encode($new_name); +        $old_dn = $group_cache[$group_id]['dn']; +        $new_rdn = "cn=" . 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;          } @@ -1957,16 +1752,14 @@ class rcube_ldap extends rcube_addressbook          if (!is_array($contact_ids))              $contact_ids = explode(',', $contact_ids); -        $base_dn     = $this->groups_base_dn; -        $group_name  = $group_cache[$group_id]['name'];          $member_attr = $group_cache[$group_id]['member_attr']; -        $group_dn    = "cn=$group_name,$base_dn"; +        $group_dn    = $group_cache[$group_id]['dn'];          $new_attrs   = array();          foreach ($contact_ids as $id)              $new_attrs[$member_attr][] = self::dn_decode($id); -        if (!$this->ldap_mod_add($group_dn, $new_attrs)) { +        if (!$this->ldap->mod_add($group_dn, $new_attrs)) {              $this->set_error(self::ERROR_SAVING, 'errorsaving');              return 0;          } @@ -1992,16 +1785,14 @@ class rcube_ldap extends rcube_addressbook          if (!is_array($contact_ids))              $contact_ids = explode(',', $contact_ids); -        $base_dn     = $this->groups_base_dn; -        $group_name  = $group_cache[$group_id]['name'];          $member_attr = $group_cache[$group_id]['member_attr']; -        $group_dn    = "cn=$group_name,$base_dn"; -        $del_attrs   = array(); +        $group_dn    = $group_cache[$group_id]['dn']; +        $del_attrs = array();          foreach ($contact_ids as $id)              $del_attrs[$member_attr][] = self::dn_decode($id); -        if (!$this->ldap_mod_del($group_dn, $del_attrs)) { +        if (!$this->ldap->mod_del($group_dn, $del_attrs)) {              $this->set_error(self::ERROR_SAVING, 'errorsaving');              return 0;          } @@ -2033,23 +1824,18 @@ class rcube_ldap extends rcube_addressbook              $add_filter = "($member_attr=$contact_dn)";          $filter = strtr("(|(member=$contact_dn)(uniqueMember=$contact_dn)$add_filter)", array('\\' => '\\\\')); -        $this->_debug("C: Search [$filter][dn: $base_dn]"); - -        $res = @ldap_search($this->conn, $base_dn, $filter, array($name_attr)); -        if ($res === false) -        { -            $this->_debug("S: ".ldap_error($this->conn)); +        $ldap_data = $this->ldap->search($base_dn, $filter, 'sub', array('dn', $name_attr)); +        if ($res === false) {              return array();          } -        $ldap_data = ldap_get_entries($this->conn, $res); -        $this->_debug("S: ".ldap_count_entries($this->conn, $res)." record(s)");          $groups = array(); -        for ($i=0; $i<$ldap_data["count"]; $i++) -        { -            $group_name = $ldap_data[$i][$name_attr][0]; -            $group_id = self::dn_encode($group_name); -            $groups[$group_id] = $group_id; +        foreach ($ldap_data as $entry) { +            if (!$entry['dn']) +                $entry['dn'] = $ldap_data->get_dn(); +            $group_name = $entry[$name_attr][0]; +            $group_id = self::dn_encode($entry['dn']); +            $groups[$group_id] = array('ID' => $group_id, 'name' => $group_name, 'dn' => $entry['dn']);          }          return $groups;      } @@ -2092,135 +1878,6 @@ class rcube_ldap extends rcube_addressbook      /** -     * Generate BER encoded string for Virtual List View option -     * -     * @param integer List offset (first record) -     * @param integer Records per page -     * @return string BER encoded option value -     */ -    private function _vlv_ber_encode($offset, $rpp, $search = '') -    { -        # this string is ber-encoded, php will prefix this value with: -        # 04 (octet string) and 10 (length of 16 bytes) -        # the code behind this string is broken down as follows: -        # 30 = ber sequence with a length of 0e (14) bytes following -        # 02 = type integer (in two's complement form) with 2 bytes following (beforeCount): 01 00 (ie 0) -        # 02 = type integer (in two's complement form) with 2 bytes following (afterCount):  01 18 (ie 25-1=24) -        # a0 = type context-specific/constructed with a length of 06 (6) bytes following -        # 02 = type integer with 2 bytes following (offset): 01 01 (ie 1) -        # 02 = type integer with 2 bytes following (contentCount):  01 00 - -        # whith a search string present: -        # 81 = type context-specific/constructed with a length of 04 (4) bytes following (the length will change here) -        # 81 indicates a user string is present where as a a0 indicates just a offset search -        # 81 = type context-specific/constructed with a length of 06 (6) bytes following - -        # the following info was taken from the ISO/IEC 8825-1:2003 x.690 standard re: the -        # encoding of integer values (note: these values are in -        # two-complement form so since offset will never be negative bit 8 of the -        # leftmost octet should never by set to 1): -        # 8.3.2: If the contents octets of an integer value encoding consist -        # of more than one octet, then the bits of the first octet (rightmost) and bit 8 -        # of the second (to the left of first octet) octet: -        # a) shall not all be ones; and -        # b) shall not all be zero - -        if ($search) -        { -            $search = preg_replace('/[^-[:alpha:] ,.()0-9]+/', '', $search); -            $ber_val = self::_string2hex($search); -            $str = self::_ber_addseq($ber_val, '81'); -        } -        else -        { -            # construct the string from right to left -            $str = "020100"; # contentCount - -            $ber_val = self::_ber_encode_int($offset);  // returns encoded integer value in hex format - -            // calculate octet length of $ber_val -            $str = self::_ber_addseq($ber_val, '02') . $str; - -            // now compute length over $str -            $str = self::_ber_addseq($str, 'a0'); -        } - -        // now tack on records per page -        $str = "020100" . self::_ber_addseq(self::_ber_encode_int($rpp-1), '02') . $str; - -        // now tack on sequence identifier and length -        $str = self::_ber_addseq($str, '30'); - -        return pack('H'.strlen($str), $str); -    } - - -    /** -     * create ber encoding for sort control -     * -     * @param array List of cols to sort by -     * @return string BER encoded option value -     */ -    private function _sort_ber_encode($sortcols) -    { -        $str = ''; -        foreach (array_reverse((array)$sortcols) as $col) { -            $ber_val = self::_string2hex($col); - -            # 30 = ber sequence with a length of octet value -            # 04 = octet string with a length of the ascii value -            $oct = self::_ber_addseq($ber_val, '04'); -            $str = self::_ber_addseq($oct, '30') . $str; -        } - -        // now tack on sequence identifier and length -        $str = self::_ber_addseq($str, '30'); - -        return pack('H'.strlen($str), $str); -    } - -    /** -     * Add BER sequence with correct length and the given identifier -     */ -    private static function _ber_addseq($str, $identifier) -    { -        $len = dechex(strlen($str)/2); -        if (strlen($len) % 2 != 0) -            $len = '0'.$len; - -        return $identifier . $len . $str; -    } - -    /** -     * Returns BER encoded integer value in hex format -     */ -    private static function _ber_encode_int($offset) -    { -        $val = dechex($offset); -        $prefix = ''; - -        // check if bit 8 of high byte is 1 -        if (preg_match('/^[89abcdef]/', $val)) -            $prefix = '00'; - -        if (strlen($val)%2 != 0) -            $prefix .= '0'; - -        return $prefix . $val; -    } - -    /** -     * Returns ascii string encoded in hex -     */ -    private static function _string2hex($str) -    { -        $hex = ''; -        for ($i=0; $i < strlen($str); $i++) -            $hex .= dechex(ord($str[$i])); -        return $hex; -    } - -    /**       * HTML-safe DN string encoding       *       * @param string $str DN string @@ -2247,130 +1904,4 @@ class rcube_ldap extends rcube_addressbook          return base64_decode($str);      } -    /** -     * Wrapper for ldap_add() -     */ -    protected function ldap_add($dn, $entry) -    { -        $this->_debug("C: Add [dn: $dn]: ".print_r($entry, true)); - -        $res = ldap_add($this->conn, $dn, $entry); -        if ($res === false) { -            $this->_debug("S: ".ldap_error($this->conn)); -            return false; -        } - -        $this->_debug("S: OK"); -        return true; -    } - -    /** -     * Wrapper for ldap_delete() -     */ -    protected function ldap_delete($dn) -    { -        $this->_debug("C: Delete [dn: $dn]"); - -        $res = ldap_delete($this->conn, $dn); -        if ($res === false) { -            $this->_debug("S: ".ldap_error($this->conn)); -            return false; -        } - -        $this->_debug("S: OK"); -        return true; -    } - -    /** -     * Wrapper for ldap_mod_replace() -     */ -    protected function ldap_mod_replace($dn, $entry) -    { -        $this->_debug("C: Replace [dn: $dn]: ".print_r($entry, true)); - -        if (!ldap_mod_replace($this->conn, $dn, $entry)) { -            $this->_debug("S: ".ldap_error($this->conn)); -            return false; -        } - -        $this->_debug("S: OK"); -        return true; -    } - -    /** -     * Wrapper for ldap_mod_add() -     */ -    protected function ldap_mod_add($dn, $entry) -    { -        $this->_debug("C: Add [dn: $dn]: ".print_r($entry, true)); - -        if (!ldap_mod_add($this->conn, $dn, $entry)) { -            $this->_debug("S: ".ldap_error($this->conn)); -            return false; -        } - -        $this->_debug("S: OK"); -        return true; -    } - -    /** -     * Wrapper for ldap_mod_del() -     */ -    protected function ldap_mod_del($dn, $entry) -    { -        $this->_debug("C: Delete [dn: $dn]: ".print_r($entry, true)); - -        if (!ldap_mod_del($this->conn, $dn, $entry)) { -            $this->_debug("S: ".ldap_error($this->conn)); -            return false; -        } - -        $this->_debug("S: OK"); -        return true; -    } - -    /** -     * Wrapper for ldap_rename() -     */ -    protected function ldap_rename($dn, $newrdn, $newparent = null, $deleteoldrdn = true) -    { -        $this->_debug("C: Rename [dn: $dn] [dn: $newrdn]"); - -        if (!ldap_rename($this->conn, $dn, $newrdn, $newparent, $deleteoldrdn)) { -            $this->_debug("S: ".ldap_error($this->conn)); -            return false; -        } - -        $this->_debug("S: OK"); -        return true; -    } - -    /** -     * Wrapper for ldap_list() -     */ -    protected function ldap_list($dn, $filter, $attrs = array('')) -    { -        $list = array(); -        $this->_debug("C: List [dn: $dn] [{$filter}]"); - -        if ($result = ldap_list($this->conn, $dn, $filter, $attrs)) { -            $list = ldap_get_entries($this->conn, $result); - -            if ($list === false) { -                $this->_debug("S: ".ldap_error($this->conn)); -                return array(); -            } - -            $count = $list['count']; -            unset($list['count']); - -            $this->_debug("S: $count record(s)"); -        } -        else { -            $this->_debug("S: ".ldap_error($this->conn)); -        } - -        return $list; -    } -  } diff --git a/program/lib/Roundcube/rcube_ldap_generic.php b/program/lib/Roundcube/rcube_ldap_generic.php new file mode 100644 index 000000000..e845727ca --- /dev/null +++ b/program/lib/Roundcube/rcube_ldap_generic.php @@ -0,0 +1,1059 @@ +<?php + +/* + +-----------------------------------------------------------------------+ + | Roundcube/rcube_ldap_generic.php                                      | + |                                                                       | + | This file is part of the Roundcube Webmail client                     | + | Copyright (C) 2006-2013, The Roundcube Dev Team                       | + | Copyright (C) 2012-2013, Kolab Systems AG                             | + |                                                                       | + | Licensed under the GNU General Public License version 3 or            | + | any later version with exceptions for skins & plugins.                | + | See the README file for a full license statement.                     | + |                                                                       | + | PURPOSE:                                                              | + |   Provide basic functionality for accessing LDAP directories          | + |                                                                       | + +-----------------------------------------------------------------------+ + | Author: Thomas Bruederli <roundcube@gmail.com>                        | + |         Aleksander Machniak <machniak@kolabsys.com>                   | + +-----------------------------------------------------------------------+ +*/ + + +/* +  LDAP connection properties +  -------------------------- + +  $prop = array( +      'host'            => '<ldap-server-address>', +      // or +      'hosts'           => array('directory.verisign.com'), +      'port'            => 389, +      'use_tls'	        => true|false, +      'ldap_version'    => 3,             // using LDAPv3 +      'auth_method'     => '',            // SASL authentication method (for proxy auth), e.g. DIGEST-MD5 +      'attributes'      => array('dn'),   // List of attributes to read from the server +      'vlv'             => false,         // Enable Virtual List View to more efficiently fetch paginated data (if server supports it) +      'config_root_dn'  => 'cn=config',   // Root DN to read config (e.g. vlv indexes) from +      'numsub_filter'   => '(objectClass=organizationalUnit)',   // with VLV, we also use numSubOrdinates to query the total number of records. Set this filter to get all numSubOrdinates attributes for counting +      'sizelimit'       => '0',           // Enables you to limit the count of entries fetched. Setting this to 0 means no limit. +      'timelimit'       => '0',           // Sets the number of seconds how long is spend on the search. Setting this to 0 means no limit. +      'network_timeout' => 10,            // The timeout (in seconds) for connect + bind arrempts. This is only supported in PHP >= 5.3.0 with OpenLDAP 2.x +      'referrals'       => true|false,    // Sets the LDAP_OPT_REFERRALS option. Mostly used in multi-domain Active Directory setups +  ); +*/ + +/** + * Model class to access an LDAP directories + * + * @package    Framework + * @subpackage LDAP + */ +class rcube_ldap_generic +{ +    const UPDATE_MOD_ADD = 1; +    const UPDATE_MOD_DELETE = 2; +    const UPDATE_MOD_REPLACE = 4; +    const UPDATE_MOD_FULL = 7; + +    public $conn; +    public $vlv_active = false; + +    /** private properties */ +    protected $cache = null; +    protected $config = array(); +    protected $attributes = array('dn'); +    protected $entries = null; +    protected $result = null; +    protected $debug = false; +    protected $list_page = 1; +    protected $page_size = 10; +    protected $vlv_config = null; + + +    /** +    * Object constructor +    * +    * @param array   $p       LDAP connection properties +    * @param boolean $debug   Enables debug mode +    */ +    function __construct($p, $debug = false) +    { +        $this->config = $p; + +        if (is_array($p['attributes'])) +            $this->attributes = $p['attributes']; + +        if (!is_array($p['hosts']) && !empty($p['host'])) +            $this->config['hosts'] = array($p['host']); + +        $this->debug = $debug; +    } + +    /** +     * Activate/deactivate debug mode +     * +     * @param boolean $dbg True if LDAP commands should be logged +     */ +    public function set_debug($dbg = true) +    { +        $this->debug = $dbg; +    } + +    /** +     * Set connection options +     * +     * @param mixed $opt Option name as string or hash array with multiple options +     * @param mixed $val Option value +     */ +    public function set_config($opt, $val = null) +    { +        if (is_array($opt)) +            $this->config = array_merge($this->config, $opt); +        else +            $this->config[$opt] = $value; +    } + +    /** +     * Enable caching by passing an instance of rcube_cache to be used by this object +     * +     * @param object rcube_cache Instance or False to disable caching +     */ +    public function set_cache($cache_engine) +    { +        $this->cache = $cache_engine; +    } + +    /** +     * Set properties for VLV-based paging +     * +     * @param  number $page  Page number to list (starting at 1) +     * @param  number $size  Number of entries to display on one page +     */ +    public function set_vlv_page($page, $size = 10) +    { +        $this->list_page = $page; +        $this->page_size = $size; +    } + + +    /** +    * Establish a connection to the LDAP server +    */ +    public function connect($host = null) +    { +        if (!function_exists('ldap_connect')) { +            rcube::raise_error(array('code' => 100, 'type' => 'ldap', +                'file' => __FILE__, 'line' => __LINE__, +                'message' => "No ldap support in this installation of PHP"), +                true); +            return false; +        } + +        if (is_resource($this->conn) && $this->config['host'] == $host) +            return true; + +        if (empty($this->config['ldap_version'])) +            $this->config['ldap_version'] = 3; + +        // iterate over hosts if none specified +        if (!$host) { +            if (!is_array($this->config['hosts'])) +                $this->config['hosts'] = array($this->config['hosts']); + +            foreach ($this->config['hosts'] as $host) { +                if ($this->connect($host)) { +                    return true; +                } +            } + +            return false; +        } + +        // open connection to the given $host +        $host     = rcube_utils::idn_to_ascii(rcube_utils::parse_host($host)); +        $hostname = $host . ($this->config['port'] ? ':'.$this->config['port'] : ''); + +        $this->_debug("C: Connect [$hostname] [{$this->config['name']}]"); + +        if ($lc = @ldap_connect($host, $this->config['port'])) { +            if ($this->config['use_tls'] === true) +                if (!ldap_start_tls($lc)) +                    continue; + +            $this->_debug("S: OK"); + +            ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->config['ldap_version']); +            $this->config['host'] = $host; +            $this->conn = $lc; + +            if (!empty($this->config['network_timeout'])) +              ldap_set_option($lc, LDAP_OPT_NETWORK_TIMEOUT, $this->config['network_timeout']); + +            if (isset($this->config['referrals'])) +                ldap_set_option($lc, LDAP_OPT_REFERRALS, $this->config['referrals']); +        } +        else { +            $this->_debug("S: NOT OK"); +        } + +        if (!is_resource($this->conn)) { +            rcube::raise_error(array('code' => 100, 'type' => 'ldap', +                'file' => __FILE__, 'line' => __LINE__, +                'message' => "Could not connect to any LDAP server, last tried $hostname"), +                true); +            return false; +        } + +        return true; +    } + + +    /** +     * Bind connection with (SASL-) user and password +     * +     * @param string $authc Authentication user +     * @param string $pass  Bind password +     * @param string $authz Autorization user +     * +     * @return boolean True on success, False on error +     */ +    public function sasl_bind($authc, $pass, $authz=null) +    { +        if (!$this->conn) { +            return false; +        } + +        if (!function_exists('ldap_sasl_bind')) { +            rcube::raise_error(array('code' => 100, 'type' => 'ldap', +                'file' => __FILE__, 'line' => __LINE__, +                'message' => "Unable to bind: ldap_sasl_bind() not exists"), +                true); +            return false; +        } + +        if (!empty($authz)) { +            $authz = 'u:' . $authz; +        } + +        if (!empty($this->config['auth_method'])) { +            $method = $this->config['auth_method']; +        } +        else { +            $method = 'DIGEST-MD5'; +        } + +        $this->_debug("C: Bind [mech: $method, authc: $authc, authz: $authz] [pass: $pass]"); + +        if (ldap_sasl_bind($this->conn, NULL, $pass, $method, NULL, $authc, $authz)) { +            $this->_debug("S: OK"); +            return true; +        } + +        $this->_debug("S: ".ldap_error($this->conn)); + +        rcube::raise_error(array( +            'code' => ldap_errno($this->conn), 'type' => 'ldap', +            'file' => __FILE__, 'line' => __LINE__, +            'message' => "SASL Bind failed for authcid=$authc ".ldap_error($this->conn)), +            true); +        return false; +    } + + +    /** +     * Bind connection with DN and password +     * +     * @param string $dn   Bind DN +     * @param string $pass Bind password +     * +     * @return boolean True on success, False on error +     */ +    public function bind($dn, $pass) +    { +        if (!$this->conn) { +            return false; +        } + +        $this->_debug("C: Bind [dn: $dn] [pass: $pass]"); + +        if (@ldap_bind($this->conn, $dn, $pass)) { +            $this->_debug("S: OK"); +            return true; +        } + +        $this->_debug("S: ".ldap_error($this->conn)); + +        rcube::raise_error(array( +            'code' => ldap_errno($this->conn), 'type' => 'ldap', +            'file' => __FILE__, 'line' => __LINE__, +            'message' => "Bind failed for dn=$dn: ".ldap_error($this->conn)), +            true); + +        return false; +    } + + +    /** +     * Close connection to LDAP server +     */ +    public function close() +    { +        if ($this->conn) { +            $this->_debug("C: Close"); +            ldap_unbind($this->conn); +            $this->conn = null; +        } +    } + + +    /** +     * Return the last result set +     * +     * @return object rcube_ldap_result Result object +     */ +    function get_result() +    { +        return $this->result; +    } + + +    /** +     * Get a specific LDAP entry, identified by its DN +     * +     * @param string $dn Record identifier +     * @return array     Hash array +     */ +    function get_entry($dn) +    { +        $rec = null; + +        if ($this->conn && $dn) { +            $this->_debug("C: Read [dn: $dn] [(objectclass=*)]"); + +            if ($ldap_result = @ldap_read($this->conn, $dn, '(objectclass=*)', $this->attributes)) { +                $this->_debug("S: OK"); + +                if ($entry = ldap_first_entry($this->conn, $ldap_result)) { +                    $rec = ldap_get_attributes($this->conn, $entry); +                } +            } +            else { +                $this->_debug("S: ".ldap_error($this->conn)); +            } + +            if (!empty($rec)) { +                $rec['dn'] = $dn; // Add in the dn for the entry. +            } +        } + +        return $rec; +    } + + +    /** +     * Execute the LDAP search based on the stored credentials +     * +     * @param string $base_dn  The base DN to query +     * @param string $filter   The LDAP filter for search +     * @param string $scope    The LDAP scope (list|sub|base) +     * @param array  $attrs    List of entry attributes to read +     * @param array  $prop     Hash array with query configuration properties: +     *   - sort: array of sort attributes (has to be in sync with the VLV index) +     *   - search: search string used for VLV controls +     * @param boolean $count_only Set to true if only entry count is requested +     * +     * @return mixed  rcube_ldap_result object or number of entries (if count_only=true) or false on error +     */ +    public function search($base_dn, $filter = '', $scope = 'sub', $attrs = array('dn'), $prop = array(), $count_only = false) +    { +        if ($this->conn) { +            if (empty($filter)) +                $filter = $filter = '(objectclass=*)'; + +            $this->_debug("C: Search [$filter][dn: $base_dn]"); + +            $function = self::scope2func($scope, $ns_function); + +            // find available VLV index for this query +            if (!$count_only && ($vlv_sort = $this->_find_vlv($base_dn, $filter, $scope, $prop['sort']))) { +                // when using VLV, we get the total count by... +                // ...either reading numSubOrdinates attribute +                if ($this->config['numsub_filter'] && ($result_count = @$ns_function($this->conn, $base_dn, $this->config['numsub_filter'], array('numSubOrdinates'), 0, 0, 0))) { +                    $counts = ldap_get_entries($this->conn, $result_count); +                    for ($vlv_count = $j = 0; $j < $counts['count']; $j++) +                        $vlv_count += $counts[$j]['numsubordinates'][0]; +                    $this->_debug("D: total numsubordinates = " . $vlv_count); +                } +                // ...or by fetching all records dn and count them +                else if (!function_exists('ldap_parse_virtuallist_control')) { +                    $vlv_count = $this->search($base_dn, $filter, $scope, array('dn'), $prop, true); +                } + +                $this->vlv_active = $this->_vlv_set_controls($vlv_sort, $this->list_page, $this->page_size, $prop['search']); +            } +            else { +                $this->vlv_active = false; +            } + +            // only fetch dn for count (should keep the payload low) +            if ($ldap_result = $function($this->conn, $base_dn, $filter, +                $attrs, 0, (int)$this->config['sizelimit'], (int)$this->config['timelimit']) +            ) { +                // when running on a patched PHP we can use the extended functions to retrieve the total count from the LDAP search result +                if ($this->vlv_active && function_exists('ldap_parse_virtuallist_control')) { +                    if (ldap_parse_result($this->conn, $ldap_result, $errcode, $matcheddn, $errmsg, $referrals, $serverctrls)) { +                        ldap_parse_virtuallist_control($this->conn, $serverctrls, $last_offset, $vlv_count, $vresult); +                        $this->_debug("S: VLV result: last_offset=$last_offset; content_count=$vlv_count"); +                    } +                    else { +                        $this->_debug("S: ".($errmsg ? $errmsg : ldap_error($this->conn))); +                    } +                } +                else if ($this->debug) { +                    $this->_debug("S: ".ldap_count_entries($this->conn, $ldap_result)." record(s) found"); +                } + +                $this->result = new rcube_ldap_result($this->conn, $ldap_result, $base_dn, $filter, $vlv_count); + +                return $count_only ? $this->result->count() : $this->result; +            } +            else { +                $this->_debug("S: ".ldap_error($this->conn)); +            } +        } + +        return false; +    } + + +    /** +     * Modify an LDAP entry on the server +     * +     * @param string $dn      Entry DN +     * @param array  $params  Hash array of entry attributes +     * @param int    $mode    Update mode (UPDATE_MOD_ADD | UPDATE_MOD_DELETE | UPDATE_MOD_REPLACE) +     */ +    public function modify($dn, $parms, $mode = 255) +    { +        // TODO: implement this + +        return false; +    } + +    /** +     * Wrapper for ldap_add() +     * +     * @see ldap_add() +     */ +    public function add($dn, $entry) +    { +        $this->_debug("C: Add [dn: $dn]: ".print_r($entry, true)); + +        $res = ldap_add($this->conn, $dn, $entry); +        if ($res === false) { +            $this->_debug("S: ".ldap_error($this->conn)); +            return false; +        } + +        $this->_debug("S: OK"); +        return true; +    } + +    /** +     * Wrapper for ldap_delete() +     * +     * @see ldap_delete() +     */ +    public function delete($dn) +    { +        $this->_debug("C: Delete [dn: $dn]"); + +        $res = ldap_delete($this->conn, $dn); +        if ($res === false) { +            $this->_debug("S: ".ldap_error($this->conn)); +            return false; +        } + +        $this->_debug("S: OK"); +        return true; +    } + +    /** +     * Wrapper for ldap_mod_replace() +     * +     * @see ldap_mod_replace() +     */ +    public function mod_replace($dn, $entry) +    { +        $this->_debug("C: Replace [dn: $dn]: ".print_r($entry, true)); + +        if (!ldap_mod_replace($this->conn, $dn, $entry)) { +            $this->_debug("S: ".ldap_error($this->conn)); +            return false; +        } + +        $this->_debug("S: OK"); +        return true; +    } + +    /** +     * Wrapper for ldap_mod_add() +     * +     * @see ldap_mod_add() +     */ +    public function mod_add($dn, $entry) +    { +        $this->_debug("C: Add [dn: $dn]: ".print_r($entry, true)); + +        if (!ldap_mod_add($this->conn, $dn, $entry)) { +            $this->_debug("S: ".ldap_error($this->conn)); +            return false; +        } + +        $this->_debug("S: OK"); +        return true; +    } + +    /** +     * Wrapper for ldap_mod_del() +     * +     * @see ldap_mod_del() +     */ +    public function mod_del($dn, $entry) +    { +        $this->_debug("C: Delete [dn: $dn]: ".print_r($entry, true)); + +        if (!ldap_mod_del($this->conn, $dn, $entry)) { +            $this->_debug("S: ".ldap_error($this->conn)); +            return false; +        } + +        $this->_debug("S: OK"); +        return true; +    } + +    /** +     * Wrapper for ldap_rename() +     * +     * @see ldap_rename() +     */ +    public function rename($dn, $newrdn, $newparent = null, $deleteoldrdn = true) +    { +        $this->_debug("C: Rename [dn: $dn] [dn: $newrdn]"); + +        if (!ldap_rename($this->conn, $dn, $newrdn, $newparent, $deleteoldrdn)) { +            $this->_debug("S: ".ldap_error($this->conn)); +            return false; +        } + +        $this->_debug("S: OK"); +        return true; +    } + +    /** +     * Wrapper for ldap_list() + ldap_get_entries() +     * +     * @see ldap_list() +     * @see ldap_get_entries() +     */ +    public function list_entries($dn, $filter, $attributes = array('dn')) +    { +        $list = array(); +        $this->_debug("C: List [dn: $dn] [{$filter}]"); + +        if ($result = ldap_list($this->conn, $dn, $filter, $attributes)) { +            $list = ldap_get_entries($this->conn, $result); + +            if ($list === false) { +                $this->_debug("S: ".ldap_error($this->conn)); +                return array(); +            } + +            $count = $list['count']; +            unset($list['count']); + +            $this->_debug("S: $count record(s)"); +        } +        else { +            $this->_debug("S: ".ldap_error($this->conn)); +        } + +        return $list; +    } + +    /** +     * Wrapper for ldap_read() + ldap_get_entries() +     * +     * @see ldap_read() +     * @see ldap_get_entries() +     */ +    public function read_entries($dn, $filter, $attributes = null) +    { +        $this->_debug("C: Read [dn: $dn] [{$filter}]"); + +        if ($this->conn && $dn) { +            if (!$attributes) +                $attributes = $this->attributes; + +            $result = @ldap_read($this->conn, $dn, $filter, $attributes, 0, (int)$this->config['sizelimit'], (int)$this->config['timelimit']); +            if ($result === false) { +                $this->_debug("S: ".ldap_error($this->conn)); +                return false; +            } + +            $this->_debug("S: OK"); +            return ldap_get_entries($this->conn, $result); +        } + +        return false; +    } + + +    /** +     * Choose the right PHP function according to scope property +     * +     * @param string $scope         The LDAP scope (sub|base|list) +     * @param string $ns_function   Function to be used for numSubOrdinates queries +     * @return string  PHP function to be used to query directory +     */ +    public static function scope2func($scope, &$ns_function = null) +    { +        switch ($scope) { +          case 'sub': +            $function = $ns_function  = 'ldap_search'; +            break; +          case 'base': +            $function = $ns_function = 'ldap_read'; +            break; +          default: +            $function = 'ldap_list'; +            $ns_function = 'ldap_read'; +            break; +        } + +        return $function; +    } + +    /** +     * Convert the given scope integer value to a string representation +     */ +    public static function scopeint2str($scope) +    { +        switch ($scope) { +            case 2:  return 'sub'; +            case 1:  return 'one'; +            case 0:  return 'base'; +            default: $this->_debug("Scope $scope is not a valid scope integer"); +        } + +        return ''; +    } + +    /** +     * Escapes the given value according to RFC 2254 so that it can be safely used in LDAP filters. +     * +     * @param string $val Value to quote +     * @return string The escaped value +     */ +    public static function escape_value($val) +    { +        return strtr($str, array('*'=>'\2a', '('=>'\28', ')'=>'\29', +            '\\'=>'\5c', '/'=>'\2f')); +    } + +    /** +     * Escapes a DN value according to RFC 2253 +     * +     * @param string $dn DN value o quote +     * @return string The escaped value +     */ +    public static function escape_dn($dn) +    { +        return strtr($str, array(','=>'\2c', '='=>'\3d', '+'=>'\2b', +            '<'=>'\3c', '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c', +            '"'=>'\22', '#'=>'\23')); +    } + +    /** +     * Normalize a LDAP result by converting entry attributes arrays into single values +     * +     * @param array $result LDAP result set fetched with ldap_get_entries() +     * @return array        Hash array with normalized entries, indexed by their DNs +     */ +    public static function normalize_result($result) +    { +        if (!is_array($result)) { +            return array(); +        } + +        $entries  = array(); +        for ($i = 0; $i < $result['count']; $i++) { +            $key = $result[$i]['dn'] ? $result[$i]['dn'] : $i; +            $entries[$key] = self::normalize_entry($result[$i]); +        } + +        return $entries; +    } +     +    /** +     * Turn an LDAP entry into a regular PHP array with attributes as keys. +     * +     * @param array $entry Attributes array as retrieved from ldap_get_attributes() or ldap_get_entries() +     * @return array       Hash array with attributes as keys +     */ +    public static function normalize_entry($entry) +    { +        $rec = array(); +        for ($i=0; $i < $entry['count']; $i++) { +            $attr = $entry[$i]; +            if ($entry[$attr]['count'] == 1) { +                switch ($attr) { +                    case 'objectclass': +                        $rec[$attr] = array(strtolower($entry[$attr][0])); +                        break; +                    default: +                        $rec[$attr] = $entry[$attr][0]; +                        break; +                } +            } +            else { +                for ($j=0; $j < $entry[$attr]['count']; $j++) { +                    $rec[$attr][$j] = $entry[$attr][$j]; +                } +            } +        } + +        return $rec; +    } + +    /** +     * Set server controls for Virtual List View (paginated listing) +     */ +    private function _vlv_set_controls($sort, $list_page, $page_size, $search = null) +    { +        $sort_ctrl = array('oid' => "1.2.840.113556.1.4.473",  'value' => self::_sort_ber_encode((array)$sort)); +        $vlv_ctrl  = array('oid' => "2.16.840.1.113730.3.4.9", 'value' => self::_vlv_ber_encode(($offset = ($list_page-1) * $page_size + 1), $page_size, $search), 'iscritical' => true); + +        $this->_debug("C: set controls sort=" . join(' ', unpack('H'.(strlen($sort_ctrl['value'])*2), $sort_ctrl['value'])) . " ($sort[0]);" +            . " vlv=" . join(' ', (unpack('H'.(strlen($vlv_ctrl['value'])*2), $vlv_ctrl['value']))) . " ($offset/$page_size; $search)"); + +        if (!ldap_set_option($this->conn, LDAP_OPT_SERVER_CONTROLS, array($sort_ctrl, $vlv_ctrl))) { +            $this->_debug("S: ".ldap_error($this->conn)); +            $this->set_error(self::ERROR_SEARCH, 'vlvnotsupported'); +            return false; +        } + +        return true; +    } + + +    /** +     * Returns unified attribute name (resolving aliases) +     */ +    private static function _attr_name($namev) +    { +        // list of known attribute aliases +        static $aliases = array( +            'gn' => 'givenname', +            'rfc822mailbox' => 'email', +            'userid' => 'uid', +            'emailaddress' => 'email', +            'pkcs9email' => 'email', +        ); + +        list($name, $limit) = explode(':', $namev, 2); +        $suffix = $limit ? ':'.$limit : ''; + +        return (isset($aliases[$name]) ? $aliases[$name] : $name) . $suffix; +    } + + +    /** +     * Quotes attribute value string +     * +     * @param string $str Attribute value +     * @param bool   $dn  True if the attribute is a DN +     * +     * @return string Quoted string +     */ +    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; +    } + +} diff --git a/program/lib/Roundcube/rcube_ldap_result.php b/program/lib/Roundcube/rcube_ldap_result.php new file mode 100644 index 000000000..efc3331bc --- /dev/null +++ b/program/lib/Roundcube/rcube_ldap_result.php @@ -0,0 +1,130 @@ +<?php + +/* + +-----------------------------------------------------------------------+ + | Roundcube/rcube_ldap_result.php                                       | + |                                                                       | + | This file is part of the Roundcube Webmail client                     | + | Copyright (C) 2006-2013, The Roundcube Dev Team                       | + | Copyright (C) 2013, Kolab Systems AG                                  | + |                                                                       | + | Licensed under the GNU General Public License version 3 or            | + | any later version with exceptions for skins & plugins.                | + | See the README file for a full license statement.                     | + |                                                                       | + | PURPOSE:                                                              | + |   Model class that represents an LDAP search result                   | + |                                                                       | + +-----------------------------------------------------------------------+ + | Author: Thomas Bruederli <roundcube@gmail.com>                        | + +-----------------------------------------------------------------------+ +*/ + + +/** + * Model class representing an LDAP search result + * + * @package    Framework + * @subpackage LDAP + */ +class rcube_ldap_result implements Iterator +{ +    public $conn; +    public $ldap; +    public $base_dn; +    public $filter; + +    private $count = null; +    private $current = null; +    private $iteratorkey = 0; + +    /** +     * Default constructor +     * +     * @param resource $conn LDAP link identifier +     * @param resource $ldap LDAP result entry identifier +     * @param string   $base_dn   Base DN used to get this result +     * @param string   $filter    Filter query used to get this result +     * @param integer  $count     Record count value (pre-calculated) +     */ +    function __construct($conn, $ldap, $base_dn, $filter, $count = null) +    { +        $this->conn = $conn; +        $this->ldap = $ldap; +        $this->base_dn = $base_dn; +        $this->filter = $filter; +        $this->count = $count; +    } + +    /** +     * Wrapper for ldap_sort() +     */ +    public function sort($attr) +    { +        return ldap_sort($this->conn, $this->ldap, $attr); +    } + +    /** +     * Get entries count +     */ +    public function count() +    { +        if (!isset($this->count)) +            $this->count = ldap_count_entries($this->conn, $this->ldap); + +        return $this->count; +    } + +    /** +     * Wrapper for ldap_get_entries() +     * +     * @param boolean $normalize Optionally normalize the entries to a list of hash arrays +     * @return array  List of LDAP entries +     */ +    public function entries($normalize = false) +    { +        $entries = ldap_get_entries($this->conn, $this->ldap); +        return $normalize ? rcube_ldap_generic::normalize_result($entries) : $entries; +    } + +    /** +     * Wrapper for ldap_get_dn() using the current entry pointer +     */ +    public function get_dn() +    { +        return $this->current ? ldap_get_dn($this->conn, $this->current) : null; +    } + + +    /***  Implements the PHP 5 Iterator interface to make foreach work  ***/ + +    function current() +    { +        $attrib = ldap_get_attributes($this->conn, $this->current); +        $attrib['dn'] = ldap_get_dn($this->conn, $this->current); +        return $attrib; +    } + +    function key() +    { +        return $this->iteratorkey; +    } + +    function rewind() +    { +        $this->iteratorkey = 0; +        $this->current = ldap_first_entry($this->conn, $this->ldap); +    } + +    function next() +    { +        $this->iteratorkey++; +        $this->current = ldap_next_entry($this->conn, $this->current); +    } + +    function valid() +    { +        return (bool)$this->current; +    } + +} diff --git a/program/localization/en_US/labels.inc b/program/localization/en_US/labels.inc index c5e6cae4c..046f2f488 100644 --- a/program/localization/en_US/labels.inc +++ b/program/localization/en_US/labels.inc @@ -356,6 +356,7 @@ $labels['lastpage']       = 'Show last page';  $labels['group'] = 'Group';  $labels['groups'] = 'Groups'; +$labels['listgroup'] = 'List group members';  $labels['personaladrbook'] = 'Personal Addresses';  $labels['searchsave'] = 'Save search'; diff --git a/program/steps/addressbook/func.inc b/program/steps/addressbook/func.inc index 3db2409e8..c7f7fb479 100644 --- a/program/steps/addressbook/func.inc +++ b/program/steps/addressbook/func.inc @@ -307,7 +307,7 @@ function rcmail_contacts_list($attrib)      global $CONTACTS, $OUTPUT;      // define list of cols to be displayed -    $a_show_cols = array('name'); +    $a_show_cols = array('name','action');      // add id to message list table if not specified      if (!strlen($attrib['id'])) @@ -336,31 +336,73 @@ function rcmail_js_contacts_list($result, $prefix='')          return;      // define list of cols to be displayed -    $a_show_cols = array('name'); +    $a_show_cols = array('name','action');      while ($row = $result->next()) { +        $row['CID'] = $row['ID']; +        $row['email'] = reset(rcube_addressbook::get_col_values('email', $row, true)); + +        $source_id = $OUTPUT->get_env('source');          $a_row_cols = array(); -        $classes = array('person');  // org records will follow some day +        $classes = array($row['_type'] ? $row['_type'] : 'person');          // build contact ID with source ID          if (isset($row['sourceid'])) {              $row['ID'] = $row['ID'].'-'.$row['sourceid']; +            $source_id = $row['sourceid'];          }          // format each col          foreach ($a_show_cols as $col) { -            $val = $col == 'name' ? rcube_addressbook::compose_list_name($row) : $row[$col]; -            $a_row_cols[$col] = Q($val); +            $val = ''; +            switch ($col) { +                case 'name': +                    $val = Q(rcube_addressbook::compose_list_name($row)); +                    break; + +                case 'action': +                    if ($row['_type'] == 'group') { +                        $val = html::a(array( +                            'href' => '#list', +                            'rel' => $row['ID'], +                            'title' => rcube_label('listgroup'), +                            'onclick' => sprintf("return %s.command('pushgroup',{'source':'%s','id':'%s'},this,event)", JS_OBJECT_NAME, $source_id, $row['CID']), +                        ), '»'); +                    } +                    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); +} + +  // similar function as /steps/settings/identities.inc::rcmail_identity_frame()  function rcmail_contact_frame($attrib)  { @@ -429,7 +471,7 @@ function rcmail_get_type_label($type)  function rcmail_contact_form($form, $record, $attrib = null)  { -    global $RCMAIL, $CONFIG; +    global $RCMAIL;      // Allow plugins to modify contact form content      $plugin = $RCMAIL->plugins->exec_hook('contact_form', array( @@ -438,7 +480,7 @@ function rcmail_contact_form($form, $record, $attrib = null)      $form = $plugin['form'];      $record = $plugin['record'];      $edit_mode = $RCMAIL->action != 'show'; -    $del_button = $attrib['deleteicon'] ? html::img(array('src' => $CONFIG['skin_path'] . $attrib['deleteicon'], 'alt' => rcube_label('delete'))) : rcube_label('delete'); +    $del_button = $attrib['deleteicon'] ? html::img(array('src' => $RCMAIL->output->get_skin_file($attrib['deleteicon']), 'alt' => rcube_label('delete'))) : rcube_label('delete');      unset($attrib['deleteicon']);      $out = ''; @@ -695,12 +737,15 @@ function rcmail_contact_form($form, $record, $attrib = null)  function rcmail_contact_photo($attrib)  { -    global $SOURCE_ID, $CONTACTS, $CONTACT_COLTYPES, $RCMAIL, $CONFIG; +    global $SOURCE_ID, $CONTACTS, $CONTACT_COLTYPES, $RCMAIL;      if ($result = $CONTACTS->get_result())          $record = $result->first(); -    $photo_img = $attrib['placeholder'] ? $CONFIG['skin_path'] . $attrib['placeholder'] : 'program/resources/blank.gif'; +    $photo_img = $attrib['placeholder'] ? $RCMAIL->output->get_skin_file($attrib['placeholder']) : 'program/resources/blank.gif'; +    if ($record['_type'] == 'group' && $attrib['placeholdergroup']) +        $photo_img = $RCMAIL->output->get_skin_file($attrib['placeholdergroup']); +      $RCMAIL->output->set_env('photo_placeholder', $photo_img);      unset($attrib['placeholder']); @@ -791,6 +836,7 @@ $OUTPUT->add_handlers(array(      'directorylist' => 'rcmail_directory_list',  //  'groupslist' => 'rcmail_contact_groups',      'addresslist' => 'rcmail_contacts_list', +    'addresslisttitle' => 'rcmail_contacts_list_title',      'addressframe' => 'rcmail_contact_frame',      'recordscountdisplay' => 'rcmail_rowcount_display',      'searchform' => array($OUTPUT, 'search_form') diff --git a/program/steps/addressbook/list.inc b/program/steps/addressbook/list.inc index 1bb28658b..6f3a3e0f3 100644 --- a/program/steps/addressbook/list.inc +++ b/program/steps/addressbook/list.inc @@ -81,6 +81,11 @@ else {          $OUTPUT->show_message('contactsearchonly', 'notice');          $OUTPUT->command('command', 'advanced-search');      } + +    if ($CONTACTS->group_id) { +        $OUTPUT->command('set_group_prop', array('ID' => $CONTACTS->group_id) +            + array_intersect_key((array)$CONTACTS->get_group($CONTACTS->group_id), array('name'=>1,'email'=>1))); +    }  }  // update message count display diff --git a/program/steps/addressbook/save.inc b/program/steps/addressbook/save.inc index 25bfbd48b..e7e5efc63 100644 --- a/program/steps/addressbook/save.inc +++ b/program/steps/addressbook/save.inc @@ -134,11 +134,11 @@ if (!empty($cid))      $record['email'] = reset($CONTACTS->get_col_values('email', $record, true));      $record['name']  = rcube_addressbook::compose_list_name($record); -    foreach (array('name', 'email') as $col) +    foreach (array('name') as $col)        $a_js_cols[] = Q((string)$record[$col]);      // update the changed col in list -    $OUTPUT->command('parent.update_contact_row', $cid, $a_js_cols, $newcid, $source); +    $OUTPUT->command('parent.update_contact_row', $cid, $a_js_cols, $newcid, $source, $record);      // show confirmation      $OUTPUT->show_message('successfullysaved', 'confirmation', null, false); diff --git a/program/steps/addressbook/show.inc b/program/steps/addressbook/show.inc index 1a97c65b1..63abc8c4b 100644 --- a/program/steps/addressbook/show.inc +++ b/program/steps/addressbook/show.inc @@ -213,8 +213,11 @@ function rcmail_contact_record_groups($contact_id)      $checkbox = new html_checkbox(array('name' => '_gid[]',          'class' => 'groupmember', 'disabled' => $CONTACTS->readonly)); -    foreach ($GROUPS as $group) { +    foreach (array_merge($GROUPS, $members) as $group) {          $gid = $group['ID']; +        if ($seen[$gid]++) +            continue; +          $table->add(null, $checkbox->show($members[$gid] ? $gid : null,              array('value' => $gid, 'id' => 'ff_gid' . $gid)));          $table->add(null, html::label('ff_gid' . $gid, Q($group['name']))); diff --git a/program/steps/mail/list_contacts.inc b/program/steps/mail/list_contacts.inc index 7e3b349cd..a48109fed 100644 --- a/program/steps/mail/list_contacts.inc +++ b/program/steps/mail/list_contacts.inc @@ -73,8 +73,11 @@ else {          $CONTACTS->set_pagesize($page_size);          $CONTACTS->set_page($page); +        if ($group_id = get_input_value('_gid', RCUBE_INPUT_GPC)) { +            $CONTACTS->set_group($group_id); +        }          // list groups of this source (on page one) -        if ($CONTACTS->groups && $CONTACTS->list_page == 1) { +        else if ($CONTACTS->groups && $CONTACTS->list_page == 1) {              foreach ($CONTACTS->list_groups() as $group) {                  $CONTACTS->reset();                  $CONTACTS->set_group($group['ID']); @@ -89,6 +92,19 @@ else {                              'contactgroup' => html::span(array('title' => $email), Q($group['name']))), 'group');                      }                  } +                // make virtual groups clickable to list their members +                else if ($group_prop['virtual']) { +                    $row_id = 'G'.$group['ID']; +                    $OUTPUT->command('add_contact_row', $row_id, array( +                        'contactgroup' => html::a(array( +                            'href' => '#list', +                            'rel' => $row['ID'], +                            'title' => rcube_label('listgroup'), +                            'onclick' => sprintf("return %s.command('pushgroup',{'source':'%s','id':'%s'},this,event)", JS_OBJECT_NAME, $source, $group['ID']), +                        ), Q($group['name']) . ' ' . html::span('action', '»'))), +                        'group', +                        array('ID' => $group['ID'], 'name' => $group['name'], 'virtual' => true)); +                }                  // show group with count                  else if (($result = $CONTACTS->count()) && $result->count) {                      $row_id = 'E'.$group['ID']; @@ -97,10 +113,12 @@ else {                          'contactgroup' => Q($group['name'] . ' (' . intval($result->count) . ')')), 'group');                  }              } + +            $CONTACTS->reset(); +            $CONTACTS->set_group(0);          }          // get contacts for this user -        $CONTACTS->set_group(0);          $result = $CONTACTS->list_records($afields);      }  } @@ -118,10 +136,13 @@ else if (!empty($result) && $result->count > 0) {          foreach ($emails as $i => $email) {              $row_id = $row['ID'].$i;              $jsresult[$row_id] = format_email_recipient($email, $name); +            $classname = $row['_type'] == 'group' ? 'group' : 'person'; +            $keyname = $row['_type'] == 'group' ? 'contactgroup' : 'contact'; +              $OUTPUT->command('add_contact_row', $row_id, array( -                'contact' => html::span(array('title' => $email), Q($name ? $name : $email) . +                $keyname => html::span(array('title' => $email), Q($name ? $name : $email) .                      ($name && count($emails) > 1 ? ' ' . html::span('email', Q($email)) : '') -                )), 'person'); +                )), $classname);          }      }  } diff --git a/skins/classic/addressbook.css b/skins/classic/addressbook.css index 5afa4592f..415142e0c 100644 --- a/skins/classic/addressbook.css +++ b/skins/classic/addressbook.css @@ -224,6 +224,37 @@    -o-text-overflow: ellipsis;  } +#contacts-table .contact.readonly td +{ +  font-style: italic; +} + +#contacts-table td.name +{ +  width: 95%; +} + +#contacts-table td.action +{ +  width: 12px; +  padding: 0px 6px 0 4px; +  text-align: right; +} + +#contacts-table td.action a +{ +  font-size: 16px; +  font-weight: bold; +  font-style: normal; +  text-decoration: none; +  color: #333; +} + +#contacts-table .selected td.action a +{ +  color: #fff; +} +  #contacts-box  {    position: absolute; diff --git a/skins/classic/images/contactgroup.png b/skins/classic/images/contactgroup.pngBinary files differ new file mode 100644 index 000000000..c46383255 --- /dev/null +++ b/skins/classic/images/contactgroup.png diff --git a/skins/classic/mail.css b/skins/classic/mail.css index b99c599b4..0193e87ff 100644 --- a/skins/classic/mail.css +++ b/skins/classic/mail.css @@ -1690,6 +1690,14 @@ input.from_address    -o-text-overflow: ellipsis;  } +#contacts-table td span.email +{ +  display: inline; +  color: #ccc; +  font-style: italic; +  margin-left: 0.5em; +} +  #abookcountbar  {    margin-top: 4px; diff --git a/skins/classic/templates/addressbook.html b/skins/classic/templates/addressbook.html index ba119891c..fdcd1847f 100644 --- a/skins/classic/templates/addressbook.html +++ b/skins/classic/templates/addressbook.html @@ -75,7 +75,7 @@  <div id="addressscreen">  <div id="addresslist"> -<div class="boxtitle"><roundcube:label name="contacts" /></div> +<roundcube:object name="addresslisttitle" label="contacts" tag="div" class="boxtitle" />  <div class="boxlistcontent">  <roundcube:object name="addresslist" id="contacts-table" class="records-table" cellspacing="0" summary="Contacts list" noheader="true" />  </div> diff --git a/skins/classic/templates/contact.html b/skins/classic/templates/contact.html index d74a78b27..8be112b49 100644 --- a/skins/classic/templates/contact.html +++ b/skins/classic/templates/contact.html @@ -13,7 +13,7 @@      <div id="sourcename"><roundcube:label name="addressbook" />: <roundcube:var name="env:sourcename" /></div>    <roundcube:endif /> -  <div id="contactphoto"><roundcube:object name="contactphoto" id="contactpic" placeholder="/images/contactpic.png" /></div> +  <div id="contactphoto"><roundcube:object name="contactphoto" id="contactpic" placeholder="/images/contactpic.png" placeholderGroup="/images/contactgroup.png" /></div>    <roundcube:object name="contacthead" id="contacthead" />    <div style="clear:both"></div>    <div id="contacttabs"> diff --git a/skins/larry/addressbook.css b/skins/larry/addressbook.css index ff3951497..090e54c7b 100644 --- a/skins/larry/addressbook.css +++ b/skins/larry/addressbook.css @@ -75,10 +75,6 @@  	text-overflow: ellipsis;  } -#contacts-table .contact.readonly td { -	font-style: italic; -} -  #directorylist li.addressbook a {  	background-position: 6px -766px;  } @@ -131,6 +127,28 @@  	left: 20px;  } +#contacts-table .contact.readonly td { +	font-style: italic; +} + +#contacts-table td.name { +	width: 95%; +} + +#contacts-table td.action { +	width: 24px; +	padding: 4px; +} + +#contacts-table td.action a { +	display: block; +	width: 16px; +	height: 14px; +	text-indent: -5000px; +	overflow: hidden; +	background: url(images/listicons.png) -2px -1180px no-repeat; +} +  #contacts-table .contact td.name {  	background-position: 6px -1603px;  } @@ -141,6 +159,29 @@  	font-weight: bold;  } +#contacts-table .group td.name { +	background-position: 6px -1555px; +} + +#contacts-table .group.selected td.name, +#contacts-table .group.unfocused td.name { +	background-position: 6px -1579px; +	font-weight: bold; +} + +#addresslist .boxtitle { +	padding-right: 95px; +	overflow: hidden; +	text-overflow: ellipsis; +} + +#addresslist .boxtitle a.poplink { +	color: #004458; +	font-size: 14px; +	line-height: 12px; +	text-decoration: none; +} +  #contact-frame {  	position: absolute;  	top: 0; diff --git a/skins/larry/ie7hacks.css b/skins/larry/ie7hacks.css index 6d7af4787..fc4713361 100644 --- a/skins/larry/ie7hacks.css +++ b/skins/larry/ie7hacks.css @@ -41,6 +41,7 @@ a.deletebutton,  .boxfooter .listbutton .inner,  .attachmentslist li a.delete,  .attachmentslist li a.cancelupload, +#contacts-table td.action a,  .previewheader .iconlink,  .minimal #taskbar .button-inner {  	/* workaround for text-indent which also offsets the background image */ diff --git a/skins/larry/images/contactgroup.png b/skins/larry/images/contactgroup.pngBinary files differ new file mode 100644 index 000000000..8303cf02f --- /dev/null +++ b/skins/larry/images/contactgroup.png diff --git a/skins/larry/images/listicons.png b/skins/larry/images/listicons.pngBinary files differ index f4505d4fa..e4ffef660 100644 --- a/skins/larry/images/listicons.png +++ b/skins/larry/images/listicons.png diff --git a/skins/larry/mail.css b/skins/larry/mail.css index fe9e56ea3..d653c7804 100644 --- a/skins/larry/mail.css +++ b/skins/larry/mail.css @@ -1238,6 +1238,19 @@ div.message-partheaders .headers-table td.header {  	text-overflow: ellipsis;  } +#contacts-table td.contactgroup a { +	color: #376572; +	text-decoration: none; +} + +#contacts-table td.contactgroup a span { +	display: inline-block; +	font-size: 16px; +	font-weight: bold; +	line-height: 11px; +	margin-left: 0.3em; +} +  #contacts-table tr:first-child td {  	border-top: 0;  } diff --git a/skins/larry/styles.css b/skins/larry/styles.css index cfbf9ac5f..8ddbb5960 100644 --- a/skins/larry/styles.css +++ b/skins/larry/styles.css @@ -1059,6 +1059,10 @@ table.listing tr.droptarget td {  	background-color: #e8e798;  } +.listbox table.listing { +	background-color: #d9ecf4; +} +  table.listing,  table.layout {  	border: 0; diff --git a/skins/larry/templates/addressbook.html b/skins/larry/templates/addressbook.html index 401640f1f..d9e491f99 100644 --- a/skins/larry/templates/addressbook.html +++ b/skins/larry/templates/addressbook.html @@ -50,7 +50,7 @@  <!-- contacts list -->  <div id="addresslist" class="uibox listbox"> -<h2 class="boxtitle"><roundcube:label name="contacts" /></h2> +<roundcube:object name="addresslisttitle" label="contacts" tag="h2" class="boxtitle" />  <div class="scroller withfooter">  <roundcube:object name="addresslist" id="contacts-table" class="listing" noheader="true" />  </div> diff --git a/skins/larry/templates/contact.html b/skins/larry/templates/contact.html index d252049cd..59fe6f79f 100644 --- a/skins/larry/templates/contact.html +++ b/skins/larry/templates/contact.html @@ -13,7 +13,7 @@  		<div id="sourcename"><roundcube:label name="addressbook" />: <roundcube:var name="env:sourcename" /></div>  	<roundcube:endif /> -	<div id="contactphoto"><roundcube:object name="contactphoto" id="contactpic" placeholder="/images/contactpic.png" /></div> +	<div id="contactphoto"><roundcube:object name="contactphoto" id="contactpic" placeholder="/images/contactpic.png" placeholderGroup="/images/contactgroup.png" /></div>  	<roundcube:object name="contacthead" id="contacthead" />  	<br style="clear:both" /> diff --git a/skins/larry/ui.js b/skins/larry/ui.js index ec4d03d00..38d8539c7 100644 --- a/skins/larry/ui.js +++ b/skins/larry/ui.js @@ -191,6 +191,8 @@ function rcube_mail_ui()      /***  addressbook task  ***/      else if (rcmail.env.task == 'addressbook') {        rcmail.addEventListener('afterupload-photo', show_uploadform); +      rcmail.addEventListener('beforepushgroup', push_contactgroup); +      rcmail.addEventListener('beforepopgroup', pop_contactgroup);        if (rcmail.env.action == '') {          new rcube_splitter({ id:'addressviewsplitterd', p1:'#addressview-left', p2:'#addressview-right', @@ -825,6 +827,35 @@ function rcube_mail_ui()      });    } +  function push_contactgroup(p) +  { +    // lets the contacts list swipe to the left, nice! +    var table = $('#contacts-table'), +      scroller = table.parent().css('overflow', 'hidden'); + +    table.clone() +      .css({ position:'absolute', top:'0', left:'0', width:table.width()+'px', 'z-index':10 }) +      .appendTo(scroller) +      .animate({ left: -(table.width()+5) + 'px' }, 300, 'swing', function(){ +        $(this).remove(); +        scroller.css('overflow', 'auto') +      }); +  } + +  function pop_contactgroup(p) +  { +    // lets the contacts list swipe to the left, nice! +    var table = $('#contacts-table'), +      scroller = table.parent().css('overflow', 'hidden'), +      clone = table.clone().appendTo(scroller); + +      table.css({ position:'absolute', top:'0', left:-(table.width()+5) + 'px', width:table.width()+'px', height:table.height()+'px', 'z-index':10 }) +        .animate({ left:'0' }, 300, 'linear', function(){ +        clone.remove(); +        $(this).css({ position:'relative', left:'0', width:'100%', height:'auto', 'z-index':1 }); +        scroller.css('overflow', 'auto') +      }); +  }    function show_uploadform()    { @@ -835,7 +866,7 @@ function rcube_mail_ui()        $dialog.dialog('close');        return;      } -     +      // add icons to clone file input field      if (rcmail.env.action == 'compose' && !$dialog.data('extended')) {        $('<a>') | 
