PageRenderTime 10ms CodeModel.GetById 45ms app.highlight 66ms RepoModel.GetById 1ms app.codeStats 1ms

/program/lib/Roundcube/rcube_ldap.php

https://github.com/trimbakgopalghare/roundcubemail
PHP | 2019 lines | 1285 code | 304 blank | 430 comment | 337 complexity | 0bf29664ac357a64aed9088f1a143d06 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1<?php
   2
   3/*
   4 +-----------------------------------------------------------------------+
   5 | This file is part of the Roundcube Webmail client                     |
   6 | Copyright (C) 2006-2013, The Roundcube Dev Team                       |
   7 | Copyright (C) 2011-2013, Kolab Systems AG                             |
   8 |                                                                       |
   9 | Licensed under the GNU General Public License version 3 or            |
  10 | any later version with exceptions for skins & plugins.                |
  11 | See the README file for a full license statement.                     |
  12 |                                                                       |
  13 | PURPOSE:                                                              |
  14 |   Interface to an LDAP address directory                              |
  15 +-----------------------------------------------------------------------+
  16 | Author: Thomas Bruederli <roundcube@gmail.com>                        |
  17 |         Andreas Dick <andudi (at) gmx (dot) ch>                       |
  18 |         Aleksander Machniak <machniak@kolabsys.com>                   |
  19 +-----------------------------------------------------------------------+
  20*/
  21
  22/**
  23 * Model class to access an LDAP address directory
  24 *
  25 * @package    Framework
  26 * @subpackage Addressbook
  27 */
  28class rcube_ldap extends rcube_addressbook
  29{
  30    // public properties
  31    public $primary_key = 'ID';
  32    public $groups      = false;
  33    public $readonly    = true;
  34    public $ready       = false;
  35    public $group_id    = 0;
  36    public $coltypes    = array();
  37    public $export_groups = false;
  38
  39    // private properties
  40    protected $ldap;
  41    protected $prop     = array();
  42    protected $fieldmap = array();
  43    protected $filter   = '';
  44    protected $sub_filter;
  45    protected $result;
  46    protected $ldap_result;
  47    protected $mail_domain = '';
  48    protected $debug = false;
  49
  50    /**
  51     * Group objectclass (lowercase) to member attribute mapping
  52     *
  53     * @var array
  54     */
  55    private $group_types = array(
  56        'group'                   => 'member',
  57        'groupofnames'            => 'member',
  58        'kolabgroupofnames'       => 'member',
  59        'groupofuniquenames'      => 'uniqueMember',
  60        'kolabgroupofuniquenames' => 'uniqueMember',
  61        'univentiongroup'         => 'uniqueMember',
  62        'groupofurls'             => null,
  63    );
  64
  65    private $base_dn        = '';
  66    private $groups_base_dn = '';
  67    private $group_url;
  68    private $cache;
  69
  70
  71    /**
  72    * Object constructor
  73    *
  74    * @param array   $p            LDAP connection properties
  75    * @param boolean $debug        Enables debug mode
  76    * @param string  $mail_domain  Current user mail domain name
  77    */
  78    function __construct($p, $debug = false, $mail_domain = null)
  79    {
  80        $this->prop = $p;
  81
  82        $fetch_attributes = array('objectClass');
  83
  84        // check if groups are configured
  85        if (is_array($p['groups']) && count($p['groups'])) {
  86            $this->groups = true;
  87            // set member field
  88            if (!empty($p['groups']['member_attr']))
  89                $this->prop['member_attr'] = strtolower($p['groups']['member_attr']);
  90            else if (empty($p['member_attr']))
  91                $this->prop['member_attr'] = 'member';
  92            // set default name attribute to cn
  93            if (empty($this->prop['groups']['name_attr']))
  94                $this->prop['groups']['name_attr'] = 'cn';
  95            if (empty($this->prop['groups']['scope']))
  96                $this->prop['groups']['scope'] = 'sub';
  97            // extend group objectclass => member attribute mapping
  98            if (!empty($this->prop['groups']['class_member_attr']))
  99                $this->group_types = array_merge($this->group_types, $this->prop['groups']['class_member_attr']);
 100
 101            // add group name attrib to the list of attributes to be fetched
 102            $fetch_attributes[] = $this->prop['groups']['name_attr'];
 103        }
 104        if (is_array($p['group_filters']) && count($p['group_filters'])) {
 105            $this->groups = true;
 106
 107            foreach ($p['group_filters'] as $k => $group_filter) {
 108                // set default name attribute to cn
 109                if (empty($group_filter['name_attr']) && empty($this->prop['groups']['name_attr']))
 110                    $this->prop['group_filters'][$k]['name_attr'] = $group_filter['name_attr'] = 'cn';
 111
 112                if ($group_filter['name_attr'])
 113                    $fetch_attributes[] = $group_filter['name_attr'];
 114            }
 115        }
 116
 117        // fieldmap property is given
 118        if (is_array($p['fieldmap'])) {
 119            foreach ($p['fieldmap'] as $rf => $lf)
 120                $this->fieldmap[$rf] = $this->_attr_name(strtolower($lf));
 121        }
 122        else if (!empty($p)) {
 123            // read deprecated *_field properties to remain backwards compatible
 124            foreach ($p as $prop => $value)
 125                if (preg_match('/^(.+)_field$/', $prop, $matches))
 126                    $this->fieldmap[$matches[1]] = $this->_attr_name(strtolower($value));
 127        }
 128
 129        // use fieldmap to advertise supported coltypes to the application
 130        foreach ($this->fieldmap as $colv => $lfv) {
 131            list($col, $type) = explode(':', $colv);
 132            list($lf, $limit, $delim) = explode(':', $lfv);
 133
 134            if ($limit == '*') $limit = null;
 135            else               $limit = max(1, intval($limit));
 136
 137            if (!is_array($this->coltypes[$col])) {
 138                $subtypes = $type ? array($type) : null;
 139                $this->coltypes[$col] = array('limit' => $limit, 'subtypes' => $subtypes, 'attributes' => array($lf));
 140            }
 141            elseif ($type) {
 142                $this->coltypes[$col]['subtypes'][] = $type;
 143                $this->coltypes[$col]['attributes'][] = $lf;
 144                $this->coltypes[$col]['limit'] += $limit;
 145            }
 146
 147            if ($delim)
 148               $this->coltypes[$col]['serialized'][$type] = $delim;
 149
 150           $this->fieldmap[$colv] = $lf;
 151        }
 152
 153        // support for composite address
 154        if ($this->coltypes['street'] && $this->coltypes['locality']) {
 155            $this->coltypes['address'] = array(
 156               'limit'    => max(1, $this->coltypes['locality']['limit'] + $this->coltypes['address']['limit']),
 157               'subtypes' => array_merge((array)$this->coltypes['address']['subtypes'], (array)$this->coltypes['locality']['subtypes']),
 158               'childs' => array(),
 159               ) + (array)$this->coltypes['address'];
 160
 161            foreach (array('street','locality','zipcode','region','country') as $childcol) {
 162                if ($this->coltypes[$childcol]) {
 163                    $this->coltypes['address']['childs'][$childcol] = array('type' => 'text');
 164                    unset($this->coltypes[$childcol]);  // remove address child col from global coltypes list
 165                }
 166            }
 167
 168            // at least one address type must be specified
 169            if (empty($this->coltypes['address']['subtypes'])) {
 170                $this->coltypes['address']['subtypes'] = array('home');
 171            }
 172        }
 173        else if ($this->coltypes['address']) {
 174            $this->coltypes['address'] += array('type' => 'textarea', 'childs' => null, 'size' => 40);
 175
 176            // 'serialized' means the UI has to present a composite address field
 177            if ($this->coltypes['address']['serialized']) {
 178                $childprop = array('type' => 'text');
 179                $this->coltypes['address']['type'] = 'composite';
 180                $this->coltypes['address']['childs'] = array('street' => $childprop, 'locality' => $childprop, 'zipcode' => $childprop, 'country' => $childprop);
 181            }
 182        }
 183
 184        // make sure 'required_fields' is an array
 185        if (!is_array($this->prop['required_fields'])) {
 186            $this->prop['required_fields'] = (array) $this->prop['required_fields'];
 187        }
 188
 189        // make sure LDAP_rdn field is required
 190        if (!empty($this->prop['LDAP_rdn']) && !in_array($this->prop['LDAP_rdn'], $this->prop['required_fields'])
 191            && !in_array($this->prop['LDAP_rdn'], array_keys((array)$this->prop['autovalues']))) {
 192            $this->prop['required_fields'][] = $this->prop['LDAP_rdn'];
 193        }
 194
 195        foreach ($this->prop['required_fields'] as $key => $val) {
 196            $this->prop['required_fields'][$key] = $this->_attr_name(strtolower($val));
 197        }
 198
 199        // Build sub_fields filter
 200        if (!empty($this->prop['sub_fields']) && is_array($this->prop['sub_fields'])) {
 201            $this->sub_filter = '';
 202            foreach ($this->prop['sub_fields'] as $class) {
 203                if (!empty($class)) {
 204                    $class = is_array($class) ? array_pop($class) : $class;
 205                    $this->sub_filter .= '(objectClass=' . $class . ')';
 206                }
 207            }
 208            if (count($this->prop['sub_fields']) > 1) {
 209                $this->sub_filter = '(|' . $this->sub_filter . ')';
 210            }
 211        }
 212
 213        $this->sort_col    = is_array($p['sort']) ? $p['sort'][0] : $p['sort'];
 214        $this->debug       = $debug;
 215        $this->mail_domain = $mail_domain;
 216
 217        // initialize cache
 218        $rcube = rcube::get_instance();
 219        if ($cache_type = $rcube->config->get('ldap_cache', 'db')) {
 220            $cache_ttl  = $rcube->config->get('ldap_cache_ttl', '10m');
 221            $cache_name = 'LDAP.' . asciiwords($this->prop['name']);
 222
 223            $this->cache = $rcube->get_cache($cache_name, $cache_type, $cache_ttl);
 224        }
 225
 226        // determine which attributes to fetch
 227        $this->prop['list_attributes'] = array_unique($fetch_attributes);
 228        $this->prop['attributes'] = array_merge(array_values($this->fieldmap), $fetch_attributes);
 229        foreach ($rcube->config->get('contactlist_fields') as $col) {
 230            $this->prop['list_attributes'] = array_merge($this->prop['list_attributes'], $this->_map_field($col));
 231        }
 232
 233        // initialize ldap wrapper object
 234        $this->ldap = new rcube_ldap_generic($this->prop);
 235        $this->ldap->set_cache($this->cache);
 236        $this->ldap->set_debug($this->debug);
 237
 238        $this->_connect();
 239    }
 240
 241
 242    /**
 243    * Establish a connection to the LDAP server
 244    */
 245    private function _connect()
 246    {
 247        $rcube = rcube::get_instance();
 248
 249        if ($this->ready)
 250            return true;
 251
 252        if (!is_array($this->prop['hosts']))
 253            $this->prop['hosts'] = array($this->prop['hosts']);
 254
 255        // try to connect + bind for every host configured
 256        // with OpenLDAP 2.x ldap_connect() always succeeds but ldap_bind will fail if host isn't reachable
 257        // see http://www.php.net/manual/en/function.ldap-connect.php
 258        foreach ($this->prop['hosts'] as $host) {
 259            // skip host if connection failed
 260            if (!$this->ldap->connect($host)) {
 261                continue;
 262            }
 263
 264            // See if the directory is writeable.
 265            if ($this->prop['writable']) {
 266                $this->readonly = false;
 267            }
 268
 269            $bind_pass = $this->prop['bind_pass'];
 270            $bind_user = $this->prop['bind_user'];
 271            $bind_dn   = $this->prop['bind_dn'];
 272
 273            $this->base_dn        = $this->prop['base_dn'];
 274            $this->groups_base_dn = ($this->prop['groups']['base_dn']) ?
 275                $this->prop['groups']['base_dn'] : $this->base_dn;
 276
 277            // User specific access, generate the proper values to use.
 278            if ($this->prop['user_specific']) {
 279                // No password set, use the session password
 280                if (empty($bind_pass)) {
 281                    $bind_pass = $rcube->get_user_password();
 282                }
 283
 284                // Get the pieces needed for variable replacement.
 285                if ($fu = $rcube->get_user_email())
 286                    list($u, $d) = explode('@', $fu);
 287                else
 288                    $d = $this->mail_domain;
 289
 290                $dc = 'dc='.strtr($d, array('.' => ',dc=')); // hierarchal domain string
 291
 292                $replaces = array('%dn' => '', '%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u);
 293
 294                // Search for the dn to use to authenticate
 295                if ($this->prop['search_base_dn'] && $this->prop['search_filter']
 296                    && (strstr($bind_dn, '%dn') || strstr($this->base_dn, '%dn') || strstr($this->groups_base_dn, '%dn'))
 297                ) {
 298                    $search_attribs = array('uid');
 299                     if ($search_bind_attrib = (array)$this->prop['search_bind_attrib']) {
 300                         foreach ($search_bind_attrib as $r => $attr) {
 301                             $search_attribs[] = $attr;
 302                             $replaces[$r] = '';
 303                         }
 304                     }
 305
 306                    $search_bind_dn = strtr($this->prop['search_bind_dn'], $replaces);
 307                    $search_base_dn = strtr($this->prop['search_base_dn'], $replaces);
 308                    $search_filter  = strtr($this->prop['search_filter'], $replaces);
 309
 310                    $cache_key = 'DN.' . md5("$host:$search_bind_dn:$search_base_dn:$search_filter:"
 311                        .$this->prop['search_bind_pw']);
 312
 313                    if ($this->cache && ($dn = $this->cache->get($cache_key))) {
 314                        $replaces['%dn'] = $dn;
 315                    }
 316                    else {
 317                        $ldap = $this->ldap;
 318                        if (!empty($search_bind_dn) && !empty($this->prop['search_bind_pw'])) {
 319                            // To protect from "Critical extension is unavailable" error
 320                            // we need to use a separate LDAP connection
 321                            if (!empty($this->prop['vlv'])) {
 322                                $ldap = new rcube_ldap_generic($this->prop);
 323                                $ldap->set_debug($this->debug);
 324                                $ldap->set_cache($this->cache);
 325                                if (!$ldap->connect($host)) {
 326                                    continue;
 327                                }
 328                            }
 329
 330                            if (!$ldap->bind($search_bind_dn, $this->prop['search_bind_pw'])) {
 331                                continue;  // bind failed, try next host
 332                            }
 333                        }
 334
 335                        $res = $ldap->search($search_base_dn, $search_filter, 'sub', $search_attribs);
 336                        if ($res) {
 337                            $res->rewind();
 338                            $replaces['%dn'] = $res->get_dn();
 339
 340                            // add more replacements from 'search_bind_attrib' config
 341                            if ($search_bind_attrib) {
 342                                $res = $res->current();
 343                                foreach ($search_bind_attrib as $r => $attr) {
 344                                    $replaces[$r] = $res[$attr][0];
 345                                }
 346                            }
 347                        }
 348
 349                        if ($ldap != $this->ldap) {
 350                            $ldap->close();
 351                        }
 352                    }
 353
 354                    // DN not found
 355                    if (empty($replaces['%dn'])) {
 356                        if (!empty($this->prop['search_dn_default']))
 357                            $replaces['%dn'] = $this->prop['search_dn_default'];
 358                        else {
 359                            rcube::raise_error(array(
 360                                'code' => 100, 'type' => 'ldap',
 361                                'file' => __FILE__, 'line' => __LINE__,
 362                                'message' => "DN not found using LDAP search."), true);
 363                            continue;
 364                        }
 365                    }
 366
 367                    if ($this->cache && !empty($replaces['%dn'])) {
 368                        $this->cache->set($cache_key, $replaces['%dn']);
 369                    }
 370                }
 371
 372                // Replace the bind_dn and base_dn variables.
 373                $bind_dn              = strtr($bind_dn, $replaces);
 374                $this->base_dn        = strtr($this->base_dn, $replaces);
 375                $this->groups_base_dn = strtr($this->groups_base_dn, $replaces);
 376
 377                // replace placeholders in filter settings
 378                if (!empty($this->prop['filter']))
 379                    $this->prop['filter'] = strtr($this->prop['filter'], $replaces);
 380
 381                foreach (array('base_dn','filter','member_filter') as $k) {
 382                    if (!empty($this->prop['groups'][$k]))
 383                        $this->prop['groups'][$k] = strtr($this->prop['groups'][$k], $replaces);
 384                }
 385
 386                if (!empty($this->prop['group_filters'])) {
 387                    foreach ($this->prop['group_filters'] as $i => $gf) {
 388                        if (!empty($gf['base_dn']))
 389                            $this->prop['group_filters'][$i]['base_dn'] = strtr($gf['base_dn'], $replaces);
 390                        if (!empty($gf['filter']))
 391                            $this->prop['group_filters'][$i]['filter'] = strtr($gf['filter'], $replaces);
 392                    }
 393                }
 394
 395                if (empty($bind_user)) {
 396                    $bind_user = $u;
 397                }
 398            }
 399
 400            if (empty($bind_pass)) {
 401                $this->ready = true;
 402            }
 403            else {
 404                if (!empty($bind_dn)) {
 405                    $this->ready = $this->ldap->bind($bind_dn, $bind_pass);
 406                }
 407                else if (!empty($this->prop['auth_cid'])) {
 408                    $this->ready = $this->ldap->sasl_bind($this->prop['auth_cid'], $bind_pass, $bind_user);
 409                }
 410                else {
 411                    $this->ready = $this->ldap->sasl_bind($bind_user, $bind_pass);
 412                }
 413            }
 414
 415            // connection established, we're done here
 416            if ($this->ready) {
 417                break;
 418            }
 419
 420        }  // end foreach hosts
 421
 422        if (!is_resource($this->ldap->conn)) {
 423            rcube::raise_error(array('code' => 100, 'type' => 'ldap',
 424                'file' => __FILE__, 'line' => __LINE__,
 425                'message' => "Could not connect to any LDAP server, last tried $host"), true);
 426
 427            return false;
 428        }
 429
 430        return $this->ready;
 431    }
 432
 433
 434    /**
 435     * Close connection to LDAP server
 436     */
 437    function close()
 438    {
 439        if ($this->ldap) {
 440            $this->ldap->close();
 441        }
 442    }
 443
 444
 445    /**
 446     * Returns address book name
 447     *
 448     * @return string Address book name
 449     */
 450    function get_name()
 451    {
 452        return $this->prop['name'];
 453    }
 454
 455
 456    /**
 457     * Set internal list page
 458     *
 459     * @param  number  Page number to list
 460     */
 461    function set_page($page)
 462    {
 463        $this->list_page = (int)$page;
 464        $this->ldap->set_vlv_page($this->list_page, $this->page_size);
 465    }
 466
 467    /**
 468     * Set internal page size
 469     *
 470     * @param  number  Number of records to display on one page
 471     */
 472    function set_pagesize($size)
 473    {
 474        $this->page_size = (int)$size;
 475        $this->ldap->set_vlv_page($this->list_page, $this->page_size);
 476    }
 477
 478
 479    /**
 480     * Set internal sort settings
 481     *
 482     * @param string $sort_col Sort column
 483     * @param string $sort_order Sort order
 484     */
 485    function set_sort_order($sort_col, $sort_order = null)
 486    {
 487        if ($this->coltypes[$sort_col]['attributes'])
 488            $this->sort_col = $this->coltypes[$sort_col]['attributes'][0];
 489    }
 490
 491
 492    /**
 493     * Save a search string for future listings
 494     *
 495     * @param string $filter Filter string
 496     */
 497    function set_search_set($filter)
 498    {
 499        $this->filter = $filter;
 500    }
 501
 502
 503    /**
 504     * Getter for saved search properties
 505     *
 506     * @return mixed Search properties used by this class
 507     */
 508    function get_search_set()
 509    {
 510        return $this->filter;
 511    }
 512
 513
 514    /**
 515     * Reset all saved results and search parameters
 516     */
 517    function reset()
 518    {
 519        $this->result = null;
 520        $this->ldap_result = null;
 521        $this->filter = '';
 522    }
 523
 524
 525    /**
 526     * List the current set of contact records
 527     *
 528     * @param  array  List of cols to show
 529     * @param  int    Only return this number of records
 530     *
 531     * @return array  Indexed list of contact records, each a hash array
 532     */
 533    function list_records($cols=null, $subset=0)
 534    {
 535        if ($this->prop['searchonly'] && empty($this->filter) && !$this->group_id) {
 536            $this->result = new rcube_result_set(0);
 537            $this->result->searchonly = true;
 538            return $this->result;
 539        }
 540
 541        // fetch group members recursively
 542        if ($this->group_id && $this->group_data['dn']) {
 543            $entries = $this->list_group_members($this->group_data['dn']);
 544
 545            // make list of entries unique and sort it
 546            $seen = array();
 547            foreach ($entries as $i => $rec) {
 548                if ($seen[$rec['dn']]++)
 549                    unset($entries[$i]);
 550            }
 551            usort($entries, array($this, '_entry_sort_cmp'));
 552
 553            $entries['count'] = count($entries);
 554            $this->result = new rcube_result_set($entries['count'], ($this->list_page-1) * $this->page_size);
 555        }
 556        else {
 557            $prop    = $this->group_id ? $this->group_data : $this->prop;
 558            $base_dn = $this->group_id ? $prop['base_dn'] : $this->base_dn;
 559
 560            // use global search filter
 561            if (!empty($this->filter))
 562                $prop['filter'] = $this->filter;
 563
 564            // exec LDAP search if no result resource is stored
 565            if ($this->ready && !$this->ldap_result)
 566                $this->ldap_result = $this->ldap->search($base_dn, $prop['filter'], $prop['scope'], $this->prop['attributes'], $prop);
 567
 568            // count contacts for this user
 569            $this->result = $this->count();
 570
 571            // we have a search result resource
 572            if ($this->ldap_result && $this->result->count > 0) {
 573                // sorting still on the ldap server
 574                if ($this->sort_col && $prop['scope'] !== 'base' && !$this->ldap->vlv_active)
 575                    $this->ldap_result->sort($this->sort_col);
 576
 577                // get all entries from the ldap server
 578                $entries = $this->ldap_result->entries();
 579            }
 580
 581        }  // end else
 582
 583        // start and end of the page
 584        $start_row = $this->ldap->vlv_active ? 0 : $this->result->first;
 585        $start_row = $subset < 0 ? $start_row + $this->page_size + $subset : $start_row;
 586        $last_row = $this->result->first + $this->page_size;
 587        $last_row = $subset != 0 ? $start_row + abs($subset) : $last_row;
 588
 589        // filter entries for this page
 590        for ($i = $start_row; $i < min($entries['count'], $last_row); $i++)
 591            $this->result->add($this->_ldap2result($entries[$i]));
 592
 593        return $this->result;
 594    }
 595
 596    /**
 597     * Get all members of the given group
 598     *
 599     * @param string  Group DN
 600     * @param boolean Count only
 601     * @param array   Group entries (if called recursively)
 602     * @return array  Accumulated group members
 603     */
 604    function list_group_members($dn, $count = false, $entries = null)
 605    {
 606        $group_members = array();
 607
 608        // fetch group object
 609        if (empty($entries)) {
 610            $attribs = array_merge(array('dn','objectClass','memberURL'), array_values($this->group_types));
 611            $entries = $this->ldap->read_entries($dn, '(objectClass=*)', $attribs);
 612            if ($entries === false) {
 613                return $group_members;
 614            }
 615        }
 616
 617        for ($i=0; $i < $entries['count']; $i++) {
 618            $entry = $entries[$i];
 619            $attrs = array();
 620
 621            foreach ((array)$entry['objectclass'] as $objectclass) {
 622                if (($member_attr = $this->get_group_member_attr(array($objectclass), ''))
 623                    && ($member_attr = strtolower($member_attr)) && !in_array($member_attr, $attrs)
 624                ) {
 625                    $members       = $this->_list_group_members($dn, $entry, $member_attr, $count);
 626                    $group_members = array_merge($group_members, $members);
 627                    $attrs[]       = $member_attr;
 628                }
 629                else if (!empty($entry['memberurl'])) {
 630                    $members       = $this->_list_group_memberurl($dn, $entry, $count);
 631                    $group_members = array_merge($group_members, $members);
 632                }
 633
 634                if ($this->prop['sizelimit'] && count($group_members) > $this->prop['sizelimit']) {
 635                    break 2;
 636                }
 637            }
 638        }
 639
 640        return array_filter($group_members);
 641    }
 642
 643    /**
 644     * Fetch members of the given group entry from server
 645     *
 646     * @param string Group DN
 647     * @param array  Group entry
 648     * @param string Member attribute to use
 649     * @param boolean Count only
 650     * @return array Accumulated group members
 651     */
 652    private function _list_group_members($dn, $entry, $attr, $count)
 653    {
 654        // Use the member attributes to return an array of member ldap objects
 655        // NOTE that the member attribute is supposed to contain a DN
 656        $group_members = array();
 657        if (empty($entry[$attr])) {
 658            return $group_members;
 659        }
 660
 661        // read these attributes for all members
 662        $attrib = $count ? array('dn','objectClass') : $this->prop['list_attributes'];
 663        $attrib = array_merge($attrib, array_values($this->group_types));
 664        $attrib[] = 'memberURL';
 665
 666        $filter = $this->prop['groups']['member_filter'] ? $this->prop['groups']['member_filter'] : '(objectclass=*)';
 667
 668        for ($i=0; $i < $entry[$attr]['count']; $i++) {
 669            if (empty($entry[$attr][$i]))
 670                continue;
 671
 672            $members = $this->ldap->read_entries($entry[$attr][$i], $filter, $attrib);
 673            if ($members == false) {
 674                $members = array();
 675            }
 676
 677            // for nested groups, call recursively
 678            $nested_group_members = $this->list_group_members($entry[$attr][$i], $count, $members);
 679
 680            unset($members['count']);
 681            $group_members = array_merge($group_members, array_filter($members), $nested_group_members);
 682        }
 683
 684        return $group_members;
 685    }
 686
 687    /**
 688     * List members of group class groupOfUrls
 689     *
 690     * @param string Group DN
 691     * @param array  Group entry
 692     * @param boolean True if only used for counting
 693     * @return array Accumulated group members
 694     */
 695    private function _list_group_memberurl($dn, $entry, $count)
 696    {
 697        $group_members = array();
 698
 699        for ($i=0; $i < $entry['memberurl']['count']; $i++) {
 700            // extract components from url
 701            if (!preg_match('!ldap:///([^\?]+)\?\?(\w+)\?(.*)$!', $entry['memberurl'][$i], $m))
 702                continue;
 703
 704            // add search filter if any
 705            $filter = $this->filter ? '(&(' . $m[3] . ')(' . $this->filter . '))' : $m[3];
 706            $attrs = $count ? array('dn','objectClass') : $this->prop['list_attributes'];
 707            if ($result = $this->ldap->search($m[1], $filter, $m[2], $attrs, $this->group_data)) {
 708                $entries = $result->entries();
 709                for ($j = 0; $j < $entries['count']; $j++) {
 710                    if ($this->is_group_entry($entries[$j]) && ($nested_group_members = $this->list_group_members($entries[$j]['dn'], $count)))
 711                        $group_members = array_merge($group_members, $nested_group_members);
 712                    else
 713                        $group_members[] = $entries[$j];
 714                }
 715            }
 716        }
 717
 718        return $group_members;
 719    }
 720
 721    /**
 722     * Callback for sorting entries
 723     */
 724    function _entry_sort_cmp($a, $b)
 725    {
 726        return strcmp($a[$this->sort_col][0], $b[$this->sort_col][0]);
 727    }
 728
 729
 730    /**
 731     * Search contacts
 732     *
 733     * @param mixed   $fields   The field name of array of field names to search in
 734     * @param mixed   $value    Search value (or array of values when $fields is array)
 735     * @param int     $mode     Matching mode:
 736     *                          0 - partial (*abc*),
 737     *                          1 - strict (=),
 738     *                          2 - prefix (abc*)
 739     * @param boolean $select   True if results are requested, False if count only
 740     * @param boolean $nocount  (Not used)
 741     * @param array   $required List of fields that cannot be empty
 742     *
 743     * @return array  Indexed list of contact records and 'count' value
 744     */
 745    function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array())
 746    {
 747        $mode = intval($mode);
 748
 749        // special treatment for ID-based search
 750        if ($fields == 'ID' || $fields == $this->primary_key) {
 751            $ids = !is_array($value) ? explode(',', $value) : $value;
 752            $result = new rcube_result_set();
 753            foreach ($ids as $id) {
 754                if ($rec = $this->get_record($id, true)) {
 755                    $result->add($rec);
 756                    $result->count++;
 757                }
 758            }
 759            return $result;
 760        }
 761
 762        // use VLV pseudo-search for autocompletion
 763        $rcube = rcube::get_instance();
 764        $list_fields = $rcube->config->get('contactlist_fields');
 765
 766        if ($this->prop['vlv_search'] && $this->ready && join(',', (array)$fields) == join(',', $list_fields)) {
 767            $this->result = new rcube_result_set(0);
 768
 769            $search_suffix = $this->prop['fuzzy_search'] && $mode != 1 ? '*' : '';
 770            $ldap_data = $this->ldap->search($this->base_dn, $this->prop['filter'], $this->prop['scope'], $this->prop['attributes'],
 771                array('search' => $value . $search_suffix /*, 'sort' => $this->prop['sort'] */));
 772            if ($ldap_data === false) {
 773                return $this->result;
 774            }
 775
 776            // get all entries of this page and post-filter those that really match the query
 777            $search = mb_strtolower($value);
 778            foreach ($ldap_data as $i => $entry) {
 779                $rec = $this->_ldap2result($entry);
 780                foreach ($fields as $f) {
 781                    foreach ((array)$rec[$f] as $val) {
 782                        if ($this->compare_search_value($f, $val, $search, $mode)) {
 783                            $this->result->add($rec);
 784                            $this->result->count++;
 785                            break 2;
 786                        }
 787                    }
 788                }
 789            }
 790
 791            return $this->result;
 792        }
 793
 794        // use AND operator for advanced searches
 795        $filter = is_array($value) ? '(&' : '(|';
 796        // set wildcards
 797        $wp = $ws = '';
 798        if (!empty($this->prop['fuzzy_search']) && $mode != 1) {
 799            $ws = '*';
 800            if (!$mode) {
 801                $wp = '*';
 802            }
 803        }
 804
 805        if ($fields == '*') {
 806            // search_fields are required for fulltext search
 807            if (empty($this->prop['search_fields'])) {
 808                $this->set_error(self::ERROR_SEARCH, 'nofulltextsearch');
 809                $this->result = new rcube_result_set();
 810                return $this->result;
 811            }
 812            if (is_array($this->prop['search_fields'])) {
 813                foreach ($this->prop['search_fields'] as $field) {
 814                    $filter .= "($field=$wp" . rcube_ldap_generic::quote_string($value) . "$ws)";
 815                }
 816            }
 817        }
 818        else {
 819            foreach ((array)$fields as $idx => $field) {
 820                $val = is_array($value) ? $value[$idx] : $value;
 821                if ($attrs = $this->_map_field($field)) {
 822                    if (count($attrs) > 1)
 823                        $filter .= '(|';
 824                    foreach ($attrs as $f)
 825                        $filter .= "($f=$wp" . rcube_ldap_generic::quote_string($val) . "$ws)";
 826                    if (count($attrs) > 1)
 827                        $filter .= ')';
 828                }
 829            }
 830        }
 831        $filter .= ')';
 832
 833        // add required (non empty) fields filter
 834        $req_filter = '';
 835        foreach ((array)$required as $field) {
 836            if (in_array($field, (array)$fields))  // required field is already in search filter
 837                continue;
 838            if ($attrs = $this->_map_field($field)) {
 839                if (count($attrs) > 1)
 840                    $req_filter .= '(|';
 841                foreach ($attrs as $f)
 842                    $req_filter .= "($f=*)";
 843                if (count($attrs) > 1)
 844                    $req_filter .= ')';
 845            }
 846        }
 847
 848        if (!empty($req_filter))
 849            $filter = '(&' . $req_filter . $filter . ')';
 850
 851        // avoid double-wildcard if $value is empty
 852        $filter = preg_replace('/\*+/', '*', $filter);
 853
 854        // add general filter to query
 855        if (!empty($this->prop['filter']))
 856            $filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->prop['filter']) . ')' . $filter . ')';
 857
 858        // set filter string and execute search
 859        $this->set_search_set($filter);
 860
 861        if ($select)
 862            $this->list_records();
 863        else
 864            $this->result = $this->count();
 865
 866        return $this->result;
 867    }
 868
 869
 870    /**
 871     * Count number of available contacts in database
 872     *
 873     * @return object rcube_result_set Resultset with values for 'count' and 'first'
 874     */
 875    function count()
 876    {
 877        $count = 0;
 878        if ($this->ldap_result) {
 879            $count = $this->ldap_result->count();
 880        }
 881        else if ($this->group_id && $this->group_data['dn']) {
 882            $count = count($this->list_group_members($this->group_data['dn'], true));
 883        }
 884        // We have a connection but no result set, attempt to get one.
 885        else if ($this->ready) {
 886            $prop    = $this->group_id ? $this->group_data : $this->prop;
 887            $base_dn = $this->group_id ? $this->group_base_dn : $this->base_dn;
 888
 889            if (!empty($this->filter)) {  // Use global search filter
 890                $prop['filter'] = $this->filter;
 891            }
 892            $count = $this->ldap->search($base_dn, $prop['filter'], $prop['scope'], array('dn'), $prop, true);
 893        }
 894
 895        return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
 896    }
 897
 898
 899    /**
 900     * Return the last result set
 901     *
 902     * @return object rcube_result_set Current resultset or NULL if nothing selected yet
 903     */
 904    function get_result()
 905    {
 906        return $this->result;
 907    }
 908
 909
 910    /**
 911     * Get a specific contact record
 912     *
 913     * @param mixed   Record identifier
 914     * @param boolean Return as associative array
 915     *
 916     * @return mixed  Hash array or rcube_result_set with all record fields
 917     */
 918    function get_record($dn, $assoc=false)
 919    {
 920        $res = $this->result = null;
 921
 922        if ($this->ready && $dn) {
 923            $dn = self::dn_decode($dn);
 924
 925            if ($rec = $this->ldap->get_entry($dn)) {
 926                $rec = array_change_key_case($rec, CASE_LOWER);
 927            }
 928
 929            // Use ldap_list to get subentries like country (c) attribute (#1488123)
 930            if (!empty($rec) && $this->sub_filter) {
 931                if ($entries = $this->ldap->list_entries($dn, $this->sub_filter, array_keys($this->prop['sub_fields']))) {
 932                    foreach ($entries as $entry) {
 933                        $lrec = array_change_key_case($entry, CASE_LOWER);
 934                        $rec  = array_merge($lrec, $rec);
 935                    }
 936                }
 937            }
 938
 939            if (!empty($rec)) {
 940                // Add in the dn for the entry.
 941                $rec['dn'] = $dn;
 942                $res = $this->_ldap2result($rec);
 943                $this->result = new rcube_result_set(1);
 944                $this->result->add($res);
 945            }
 946        }
 947
 948        return $assoc ? $res : $this->result;
 949    }
 950
 951
 952    /**
 953     * Check the given data before saving.
 954     * If input not valid, the message to display can be fetched using get_error()
 955     *
 956     * @param array Assoziative array with data to save
 957     * @param boolean Try to fix/complete record automatically
 958     * @return boolean True if input is valid, False if not.
 959     */
 960    public function validate(&$save_data, $autofix = false)
 961    {
 962        // validate e-mail addresses
 963        if (!parent::validate($save_data, $autofix)) {
 964            return false;
 965        }
 966
 967        // check for name input
 968        if (empty($save_data['name'])) {
 969            $this->set_error(self::ERROR_VALIDATE, 'nonamewarning');
 970            return false;
 971        }
 972
 973        // Verify that the required fields are set.
 974        $missing = null;
 975        $ldap_data = $this->_map_data($save_data);
 976        foreach ($this->prop['required_fields'] as $fld) {
 977            if (!isset($ldap_data[$fld]) || $ldap_data[$fld] === '') {
 978                $missing[$fld] = 1;
 979            }
 980        }
 981
 982        if ($missing) {
 983            // try to complete record automatically
 984            if ($autofix) {
 985                $sn_field    = $this->fieldmap['surname'];
 986                $fn_field    = $this->fieldmap['firstname'];
 987                $mail_field  = $this->fieldmap['email'];
 988
 989                // try to extract surname and firstname from displayname
 990                $name_parts  = preg_split('/[\s,.]+/', $save_data['name']);
 991
 992                if ($sn_field && $missing[$sn_field]) {
 993                    $save_data['surname'] = array_pop($name_parts);
 994                    unset($missing[$sn_field]);
 995                }
 996
 997                if ($fn_field && $missing[$fn_field]) {
 998                    $save_data['firstname'] = array_shift($name_parts);
 999                    unset($missing[$fn_field]);
1000                }
1001
1002                // try to fix missing e-mail, very often on import
1003                // from vCard we have email:other only defined
1004                if ($mail_field && $missing[$mail_field]) {
1005                    $emails = $this->get_col_values('email', $save_data, true);
1006                    if (!empty($emails) && ($email = array_shift($emails))) {
1007                        $save_data['email'] = $email;
1008                        unset($missing[$mail_field]);
1009                    }
1010                }
1011            }
1012
1013            // TODO: generate message saying which fields are missing
1014            if (!empty($missing)) {
1015                $this->set_error(self::ERROR_VALIDATE, 'formincomplete');
1016                return false;
1017            }
1018        }
1019
1020        return true;
1021    }
1022
1023
1024    /**
1025     * Create a new contact record
1026     *
1027     * @param array    Hash array with save data
1028     *
1029     * @return encoded record ID on success, False on error
1030     */
1031    function insert($save_cols)
1032    {
1033        // Map out the column names to their LDAP ones to build the new entry.
1034        $newentry = $this->_map_data($save_cols);
1035        $newentry['objectClass'] = $this->prop['LDAP_Object_Classes'];
1036
1037        // add automatically generated attributes
1038        $this->add_autovalues($newentry);
1039
1040        // Verify that the required fields are set.
1041        $missing = null;
1042        foreach ($this->prop['required_fields'] as $fld) {
1043            if (!isset($newentry[$fld])) {
1044                $missing[] = $fld;
1045            }
1046        }
1047
1048        // abort process if requiered fields are missing
1049        // TODO: generate message saying which fields are missing
1050        if ($missing) {
1051            $this->set_error(self::ERROR_VALIDATE, 'formincomplete');
1052            return false;
1053        }
1054
1055        // Build the new entries DN.
1056        $dn = $this->prop['LDAP_rdn'].'='.rcube_ldap_generic::quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->base_dn;
1057
1058        // Remove attributes that need to be added separately (child objects)
1059        $xfields = array();
1060        if (!empty($this->prop['sub_fields']) && is_array($this->prop['sub_fields'])) {
1061            foreach (array_keys($this->prop['sub_fields']) as $xf) {
1062                if (!empty($newentry[$xf])) {
1063                    $xfields[$xf] = $newentry[$xf];
1064                    unset($newentry[$xf]);
1065                }
1066            }
1067        }
1068
1069        if (!$this->ldap->add($dn, $newentry)) {
1070            $this->set_error(self::ERROR_SAVING, 'errorsaving');
1071            return false;
1072        }
1073
1074        foreach ($xfields as $xidx => $xf) {
1075            $xdn = $xidx.'='.rcube_ldap_generic::quote_string($xf).','.$dn;
1076            $xf = array(
1077                $xidx => $xf,
1078                'objectClass' => (array) $this->prop['sub_fields'][$xidx],
1079            );
1080
1081            $this->ldap->add($xdn, $xf);
1082        }
1083
1084        $dn = self::dn_encode($dn);
1085
1086        // add new contact to the selected group
1087        if ($this->group_id)
1088            $this->add_to_group($this->group_id, $dn);
1089
1090        return $dn;
1091    }
1092
1093
1094    /**
1095     * Update a specific contact record
1096     *
1097     * @param mixed Record identifier
1098     * @param array Hash array with save data
1099     *
1100     * @return boolean True on success, False on error
1101     */
1102    function update($id, $save_cols)
1103    {
1104        $record = $this->get_record($id, true);
1105
1106        $newdata     = array();
1107        $replacedata = array();
1108        $deletedata  = array();
1109        $subdata     = array();
1110        $subdeldata  = array();
1111        $subnewdata  = array();
1112
1113        $ldap_data = $this->_map_data($save_cols);
1114        $old_data  = $record['_raw_attrib'];
1115
1116        // special handling of photo col
1117        if ($photo_fld = $this->fieldmap['photo']) {
1118            // undefined means keep old photo
1119            if (!array_key_exists('photo', $save_cols)) {
1120                $ldap_data[$photo_fld] = $record['photo'];
1121            }
1122        }
1123
1124        foreach ($this->fieldmap as $fld) {
1125            if ($fld) {
1126                $val = $ldap_data[$fld];
1127                $old = $old_data[$fld];
1128                // remove empty array values
1129                if (is_array($val))
1130                    $val = array_filter($val);
1131                // $this->_map_data() result and _raw_attrib use different format
1132                // make sure comparing array with one element with a string works as expected
1133                if (is_array($old) && count($old) == 1 && !is_array($val)) {
1134                    $old = array_pop($old);
1135                }
1136                if (is_array($val) && count($val) == 1 && !is_array($old)) {
1137                    $val = array_pop($val);
1138                }
1139                // Subentries must be handled separately
1140                if (!empty($this->prop['sub_fields']) && isset($this->prop['sub_fields'][$fld])) {
1141                    if ($old != $val) {
1142                        if ($old !== null) {
1143                            $subdeldata[$fld] = $old;
1144                        }
1145                        if ($val) {
1146                            $subnewdata[$fld] = $val;
1147                        }
1148                    }
1149                    else if ($old !== null) {
1150                        $subdata[$fld] = $old;
1151                    }
1152                    continue;
1153                }
1154
1155                // The field does exist compare it to the ldap record.
1156                if ($old != $val) {
1157                    // Changed, but find out how.
1158                    if ($old === null) {
1159                        // Field was not set prior, need to add it.
1160                        $newdata[$fld] = $val;
1161                    }
1162                    else if ($val == '') {
1163                        // Field supplied is empty, verify that it is not required.
1164                        if (!in_array($fld, $this->prop['required_fields'])) {
1165                            // ...It is not, safe to clear.
1166                            // #1488420: Workaround "ldap_mod_del(): Modify: Inappropriate matching in..."
1167                            // jpegPhoto attribute require an array() here. It looks to me that it works for other attribs too
1168                            $deletedata[$fld] = array();
1169                            //$deletedata[$fld] = $old_data[$fld];
1170                        }
1171                    }
1172                    else {
1173                        // The data was modified, save it out.
1174                        $replacedata[$fld] = $val;
1175                    }
1176                } // end if
1177            } // end if
1178        } // end foreach
1179
1180        // console($old_data, $ldap_data, '----', $newdata, $replacedata, $deletedata, '----', $subdata, $subnewdata, $subdeldata);
1181
1182        $dn = self::dn_decode($id);
1183
1184        // Update the entry as required.
1185        if (!empty($deletedata)) {
1186            // Delete the fields.
1187            if (!$this->ldap->mod_del($dn, $deletedata)) {
1188                $this->set_error(self::ERROR_SAVING, 'errorsaving');
1189                return false;
1190            }
1191        } // end if
1192
1193        if (!empty($replacedata)) {
1194            // Handle RDN change
1195            if ($replacedata[$this->prop['LDAP_rdn']]) {
1196                $newdn = $this->prop['LDAP_rdn'].'='
1197                    .rcube_ldap_generic::quote_string($replacedata[$this->prop['LDAP_rdn']], true)
1198                    .','.$this->base_dn;
1199                if ($dn != $newdn) {
1200                    $newrdn = $this->prop['LDAP_rdn'].'='
1201                    .rcube_ldap_generic::quote_string($replacedata[$this->prop['LDAP_rdn']], true);
1202                    unset($replacedata[$this->prop['LDAP_rdn']]);
1203                }
1204            }
1205            // Replace the fields.
1206            if (!empty($replacedata)) {
1207                if (!$this->ldap->mod_replace($dn, $replacedata)) {
1208                    $this->set_error(self::ERROR_SAVING, 'errorsaving');
1209                    return false;
1210                }
1211            }
1212        } // end if
1213
1214        // RDN change, we need to remove all sub-entries
1215        if (!empty($newrdn)) {
1216            $subdeldata = array_merge($subdeldata, $subdata);
1217            $subnewdata = array_merge($subnewdata, $subdata);
1218        }
1219
1220        // remove sub-entries
1221        if (!empty($subdeldata)) {
1222            foreach ($subdeldata as $fld => $val) {
1223                $subdn = $fld.'='.rcube_ldap_generic::quote_string($val).','.$dn;
1224                if (!$this->ldap->delete($subdn)) {
1225                    return false;
1226                }
1227            }
1228        }
1229
1230        if (!empty($newdata)) {
1231            // Add the fields.
1232            if (!$this->ldap->mod_add($dn, $newdata)) {
1233                $this->set_error(self::ERROR_SAVING, 'errorsaving');
1234                return false;
1235            }
1236        } // end if
1237
1238        // Handle RDN change
1239        if (!empty($newrdn)) {
1240            if (!$this->ldap->rename($dn, $newrdn, null, true)) {
1241                $this->set_error(self::ERROR_SAVING, 'errorsaving');
1242                return false;
1243            }
1244
1245            $dn    = self::dn_encode($dn);
1246            $newdn = self::dn_encode($newdn);
1247
1248            // change the group membership of the contact
1249            if ($this->groups) {
1250                $group_ids = $this->get_record_groups($dn);
1251                foreach (array_keys($group_ids) as $group_id) {
1252                    $this->remove_from_group($group_id, $dn);
1253                    $this->add_to_group($group_id, $newdn);
1254                }
1255            }
1256
1257            $dn = self::dn_decode($newdn);
1258        }
1259
1260        // add sub-entries
1261        if (!empty($subnewdata)) {
1262            foreach ($subnewdata as $fld => $val) {
1263                $subdn = $fld.'='.rcube_ldap_generic::quote_string($val).','.$dn;
1264                $xf = array(
1265                    $fld => $val,
1266                    'objectClass' => (array) $this->prop['sub_fields'][$fld],
1267                );
1268                $this->ldap->add($subdn, $xf);
1269            }
1270        }
1271
1272        return $newdn ? $newdn : true;
1273    }
1274
1275
1276    /**
1277     * Mark one or more contact records as deleted
1278     *
1279     * @param array   Record identifiers
1280     * @param boolean Remove record(s) irreversible (unsupported)
1281     *
1282     * @return boolean True on success, False on error
1283     */
1284    function delete($ids, $force=true)
1285    {
1286        if (!is_array($ids)) {
1287            // Not an array, break apart the encoded DNs.
1288            $ids = explode(',', $ids);
1289        } // end if
1290
1291        foreach ($ids as $id) {
1292            $dn = self::dn_decode($id);
1293
1294            // Need to delete all sub-entries first
1295            if ($this->sub_filter) {
1296                if ($entries = $this->ldap->list_entries($dn, $this->sub_filter)) {
1297                    foreach ($entries as $entry) {
1298                        if (!$this->ldap->delete($entry['dn'])) {
1299                            $this->set_error(self::ERROR_SAVING, 'errorsaving');
1300                            return false;
1301                        }
1302                    }
1303                }
1304            }
1305
1306            // Delete the record.
1307            if (!$this->ldap->delete($dn)) {
1308                $this->set_error(self::ERROR_SAVING, 'errorsaving');
1309                return false;
1310            }
1311
1312            // remove contact from all groups where he was member
1313            if ($this->groups) {
1314                $dn = self::dn_encode($dn);
1315                $group_ids = $this->get_record_groups($dn);
1316                foreach (array_keys($group_ids) as $group_id) {
1317                    $this->remove_from_group($group_id, $dn);
1318                }
1319            }
1320        } // end foreach
1321
1322        return count($ids);
1323    }
1324
1325
1326    /**
1327     * Remove all contact records
1328     *
1329     * @param bool $with_groups Delete also groups if enabled
1330     */
1331    function delete_all($with_groups = false)
1332    {
1333        // searching for contact entries
1334        $dn_list = $this->ldap->list_entries($this->base_dn, $this->prop['filter'] ? $this->prop['filter'] : '(objectclass=*)');
1335
1336        if (!empty($dn_list)) {
1337            foreach ($dn_list as $idx =

Large files files are truncated, but you can click here to view the full file