PageRenderTime 153ms CodeModel.GetById 67ms app.highlight 67ms RepoModel.GetById 1ms app.codeStats 1ms

/etc/apps/webmail/program/lib/Roundcube/rcube_ldap.php

https://github.com/raiman264/zpanelx
PHP | 2008 lines | 1277 code | 302 blank | 429 comment | 336 complexity | f892cfb79f72c1d8e01fa62fa9eb2601 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                if (!empty($this->prop['groups']['filter']))
 381                    $this->prop['groups']['filter'] = strtr($this->prop['groups']['filter'], $replaces);
 382                if (!empty($this->prop['groups']['member_filter']))
 383                    $this->prop['groups']['member_filter'] = strtr($this->prop['groups']['member_filter'], $replaces);
 384
 385                if (!empty($this->prop['group_filters'])) {
 386                    foreach ($this->prop['group_filters'] as $i => $gf) {
 387                        if (!empty($gf['base_dn']))
 388                            $this->prop['group_filters'][$i]['base_dn'] = strtr($gf['base_dn'], $replaces);
 389                        if (!empty($gf['filter']))
 390                            $this->prop['group_filters'][$i]['filter'] = strtr($gf['filter'], $replaces);
 391                    }
 392                }
 393
 394                if (empty($bind_user)) {
 395                    $bind_user = $u;
 396                }
 397            }
 398
 399            if (empty($bind_pass)) {
 400                $this->ready = true;
 401            }
 402            else {
 403                if (!empty($bind_dn)) {
 404                    $this->ready = $this->ldap->bind($bind_dn, $bind_pass);
 405                }
 406                else if (!empty($this->prop['auth_cid'])) {
 407                    $this->ready = $this->ldap->sasl_bind($this->prop['auth_cid'], $bind_pass, $bind_user);
 408                }
 409                else {
 410                    $this->ready = $this->ldap->sasl_bind($bind_user, $bind_pass);
 411                }
 412            }
 413
 414            // connection established, we're done here
 415            if ($this->ready) {
 416                break;
 417            }
 418
 419        }  // end foreach hosts
 420
 421        if (!is_resource($this->ldap->conn)) {
 422            rcube::raise_error(array('code' => 100, 'type' => 'ldap',
 423                'file' => __FILE__, 'line' => __LINE__,
 424                'message' => "Could not connect to any LDAP server, last tried $host"), true);
 425
 426            return false;
 427        }
 428
 429        return $this->ready;
 430    }
 431
 432
 433    /**
 434     * Close connection to LDAP server
 435     */
 436    function close()
 437    {
 438        if ($this->ldap) {
 439            $this->ldap->close();
 440        }
 441    }
 442
 443
 444    /**
 445     * Returns address book name
 446     *
 447     * @return string Address book name
 448     */
 449    function get_name()
 450    {
 451        return $this->prop['name'];
 452    }
 453
 454
 455    /**
 456     * Set internal list page
 457     *
 458     * @param  number  Page number to list
 459     */
 460    function set_page($page)
 461    {
 462        $this->list_page = (int)$page;
 463        $this->ldap->set_vlv_page($this->list_page, $this->page_size);
 464    }
 465
 466    /**
 467     * Set internal page size
 468     *
 469     * @param  number  Number of records to display on one page
 470     */
 471    function set_pagesize($size)
 472    {
 473        $this->page_size = (int)$size;
 474        $this->ldap->set_vlv_page($this->list_page, $this->page_size);
 475    }
 476
 477
 478    /**
 479     * Set internal sort settings
 480     *
 481     * @param string $sort_col Sort column
 482     * @param string $sort_order Sort order
 483     */
 484    function set_sort_order($sort_col, $sort_order = null)
 485    {
 486        if ($this->coltypes[$sort_col]['attributes'])
 487            $this->sort_col = $this->coltypes[$sort_col]['attributes'][0];
 488    }
 489
 490
 491    /**
 492     * Save a search string for future listings
 493     *
 494     * @param string $filter Filter string
 495     */
 496    function set_search_set($filter)
 497    {
 498        $this->filter = $filter;
 499    }
 500
 501
 502    /**
 503     * Getter for saved search properties
 504     *
 505     * @return mixed Search properties used by this class
 506     */
 507    function get_search_set()
 508    {
 509        return $this->filter;
 510    }
 511
 512
 513    /**
 514     * Reset all saved results and search parameters
 515     */
 516    function reset()
 517    {
 518        $this->result = null;
 519        $this->ldap_result = null;
 520        $this->filter = '';
 521    }
 522
 523
 524    /**
 525     * List the current set of contact records
 526     *
 527     * @param  array  List of cols to show
 528     * @param  int    Only return this number of records
 529     *
 530     * @return array  Indexed list of contact records, each a hash array
 531     */
 532    function list_records($cols=null, $subset=0)
 533    {
 534        if ($this->prop['searchonly'] && empty($this->filter) && !$this->group_id) {
 535            $this->result = new rcube_result_set(0);
 536            $this->result->searchonly = true;
 537            return $this->result;
 538        }
 539
 540        // fetch group members recursively
 541        if ($this->group_id && $this->group_data['dn']) {
 542            $entries = $this->list_group_members($this->group_data['dn']);
 543
 544            // make list of entries unique and sort it
 545            $seen = array();
 546            foreach ($entries as $i => $rec) {
 547                if ($seen[$rec['dn']]++)
 548                    unset($entries[$i]);
 549            }
 550            usort($entries, array($this, '_entry_sort_cmp'));
 551
 552            $entries['count'] = count($entries);
 553            $this->result = new rcube_result_set($entries['count'], ($this->list_page-1) * $this->page_size);
 554        }
 555        else {
 556            $prop    = $this->group_id ? $this->group_data : $this->prop;
 557            $base_dn = $this->group_id ? $prop['base_dn'] : $this->base_dn;
 558
 559            // use global search filter
 560            if (!empty($this->filter))
 561                $prop['filter'] = $this->filter;
 562
 563            // exec LDAP search if no result resource is stored
 564            if ($this->ready && !$this->ldap_result)
 565                $this->ldap_result = $this->ldap->search($base_dn, $prop['filter'], $prop['scope'], $this->prop['attributes'], $prop);
 566
 567            // count contacts for this user
 568            $this->result = $this->count();
 569
 570            // we have a search result resource
 571            if ($this->ldap_result && $this->result->count > 0) {
 572                // sorting still on the ldap server
 573                if ($this->sort_col && $prop['scope'] !== 'base' && !$this->ldap->vlv_active)
 574                    $this->ldap_result->sort($this->sort_col);
 575
 576                // get all entries from the ldap server
 577                $entries = $this->ldap_result->entries();
 578            }
 579
 580        }  // end else
 581
 582        // start and end of the page
 583        $start_row = $this->ldap->vlv_active ? 0 : $this->result->first;
 584        $start_row = $subset < 0 ? $start_row + $this->page_size + $subset : $start_row;
 585        $last_row = $this->result->first + $this->page_size;
 586        $last_row = $subset != 0 ? $start_row + abs($subset) : $last_row;
 587
 588        // filter entries for this page
 589        for ($i = $start_row; $i < min($entries['count'], $last_row); $i++)
 590            $this->result->add($this->_ldap2result($entries[$i]));
 591
 592        return $this->result;
 593    }
 594
 595    /**
 596     * Get all members of the given group
 597     *
 598     * @param string  Group DN
 599     * @param boolean Count only
 600     * @param array   Group entries (if called recursively)
 601     * @return array  Accumulated group members
 602     */
 603    function list_group_members($dn, $count = false, $entries = null)
 604    {
 605        $group_members = array();
 606
 607        // fetch group object
 608        if (empty($entries)) {
 609            $attribs = array_merge(array('dn','objectClass','memberURL'), array_values($this->group_types));
 610            $entries = $this->ldap->read_entries($dn, '(objectClass=*)', $attribs);
 611            if ($entries === false) {
 612                return $group_members;
 613            }
 614        }
 615
 616        for ($i=0; $i < $entries['count']; $i++) {
 617            $entry = $entries[$i];
 618            $attrs = array();
 619
 620            foreach ((array)$entry['objectclass'] as $objectclass) {
 621                if (($member_attr = $this->get_group_member_attr(array($objectclass), ''))
 622                    && ($member_attr = strtolower($member_attr)) && !in_array($member_attr, $attrs)
 623                ) {
 624                    $members       = $this->_list_group_members($dn, $entry, $member_attr, $count);
 625                    $group_members = array_merge($group_members, $members);
 626                    $attrs[]       = $member_attr;
 627                }
 628                else if (!empty($entry['memberurl'])) {
 629                    $members       = $this->_list_group_memberurl($dn, $entry, $count);
 630                    $group_members = array_merge($group_members, $members);
 631                }
 632
 633                if ($this->prop['sizelimit'] && count($group_members) > $this->prop['sizelimit']) {
 634                    break 2;
 635                }
 636            }
 637        }
 638
 639        return array_filter($group_members);
 640    }
 641
 642    /**
 643     * Fetch members of the given group entry from server
 644     *
 645     * @param string Group DN
 646     * @param array  Group entry
 647     * @param string Member attribute to use
 648     * @param boolean Count only
 649     * @return array Accumulated group members
 650     */
 651    private function _list_group_members($dn, $entry, $attr, $count)
 652    {
 653        // Use the member attributes to return an array of member ldap objects
 654        // NOTE that the member attribute is supposed to contain a DN
 655        $group_members = array();
 656        if (empty($entry[$attr])) {
 657            return $group_members;
 658        }
 659
 660        // read these attributes for all members
 661        $attrib = $count ? array('dn','objectClass') : $this->prop['list_attributes'];
 662        $attrib = array_merge($attrib, array_values($this->group_types));
 663        $attrib[] = 'memberURL';
 664
 665        $filter = $this->prop['groups']['member_filter'] ? $this->prop['groups']['member_filter'] : '(objectclass=*)';
 666
 667        for ($i=0; $i < $entry[$attr]['count']; $i++) {
 668            if (empty($entry[$attr][$i]))
 669                continue;
 670
 671            $members = $this->ldap->read_entries($entry[$attr][$i], $filter, $attrib);
 672            if ($members == false) {
 673                $members = array();
 674            }
 675
 676            // for nested groups, call recursively
 677            $nested_group_members = $this->list_group_members($entry[$attr][$i], $count, $members);
 678
 679            unset($members['count']);
 680            $group_members = array_merge($group_members, array_filter($members), $nested_group_members);
 681        }
 682
 683        return $group_members;
 684    }
 685
 686    /**
 687     * List members of group class groupOfUrls
 688     *
 689     * @param string Group DN
 690     * @param array  Group entry
 691     * @param boolean True if only used for counting
 692     * @return array Accumulated group members
 693     */
 694    private function _list_group_memberurl($dn, $entry, $count)
 695    {
 696        $group_members = array();
 697
 698        for ($i=0; $i < $entry['memberurl']['count']; $i++) {
 699            // extract components from url
 700            if (!preg_match('!ldap:///([^\?]+)\?\?(\w+)\?(.*)$!', $entry['memberurl'][$i], $m))
 701                continue;
 702
 703            // add search filter if any
 704            $filter = $this->filter ? '(&(' . $m[3] . ')(' . $this->filter . '))' : $m[3];
 705            $attrs = $count ? array('dn','objectClass') : $this->prop['list_attributes'];
 706            if ($result = $this->ldap->search($m[1], $filter, $m[2], $attrs, $this->group_data)) {
 707                $entries = $result->entries();
 708                for ($j = 0; $j < $entries['count']; $j++) {
 709                    if ($this->is_group_entry($entries[$j]) && ($nested_group_members = $this->list_group_members($entries[$j]['dn'], $count)))
 710                        $group_members = array_merge($group_members, $nested_group_members);
 711                    else
 712                        $group_members[] = $entries[$j];
 713                }
 714            }
 715        }
 716
 717        return $group_members;
 718    }
 719
 720    /**
 721     * Callback for sorting entries
 722     */
 723    function _entry_sort_cmp($a, $b)
 724    {
 725        return strcmp($a[$this->sort_col][0], $b[$this->sort_col][0]);
 726    }
 727
 728
 729    /**
 730     * Search contacts
 731     *
 732     * @param mixed   $fields   The field name of array of field names to search in
 733     * @param mixed   $value    Search value (or array of values when $fields is array)
 734     * @param int     $mode     Matching mode:
 735     *                          0 - partial (*abc*),
 736     *                          1 - strict (=),
 737     *                          2 - prefix (abc*)
 738     * @param boolean $select   True if results are requested, False if count only
 739     * @param boolean $nocount  (Not used)
 740     * @param array   $required List of fields that cannot be empty
 741     *
 742     * @return array  Indexed list of contact records and 'count' value
 743     */
 744    function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array())
 745    {
 746        $mode = intval($mode);
 747
 748        // special treatment for ID-based search
 749        if ($fields == 'ID' || $fields == $this->primary_key) {
 750            $ids = !is_array($value) ? explode(',', $value) : $value;
 751            $result = new rcube_result_set();
 752            foreach ($ids as $id) {
 753                if ($rec = $this->get_record($id, true)) {
 754                    $result->add($rec);
 755                    $result->count++;
 756                }
 757            }
 758            return $result;
 759        }
 760
 761        // use VLV pseudo-search for autocompletion
 762        $rcube = rcube::get_instance();
 763        $list_fields = $rcube->config->get('contactlist_fields');
 764
 765        if ($this->prop['vlv_search'] && $this->ready && join(',', (array)$fields) == join(',', $list_fields)) {
 766            $this->result = new rcube_result_set(0);
 767
 768            $search_suffix = $this->prop['fuzzy_search'] && $mode != 1 ? '*' : '';
 769            $ldap_data = $this->ldap->search($this->base_dn, $this->prop['filter'], $this->prop['scope'], $this->prop['attributes'],
 770                array('search' => $value . $search_suffix /*, 'sort' => $this->prop['sort'] */));
 771            if ($ldap_data === false) {
 772                return $this->result;
 773            }
 774
 775            // get all entries of this page and post-filter those that really match the query
 776            $search = mb_strtolower($value);
 777            foreach ($ldap_data as $i => $entry) {
 778                $rec = $this->_ldap2result($entry);
 779                foreach ($fields as $f) {
 780                    foreach ((array)$rec[$f] as $val) {
 781                        if ($this->compare_search_value($f, $val, $search, $mode)) {
 782                            $this->result->add($rec);
 783                            $this->result->count++;
 784                            break 2;
 785                        }
 786                    }
 787                }
 788            }
 789
 790            return $this->result;
 791        }
 792
 793        // use AND operator for advanced searches
 794        $filter = is_array($value) ? '(&' : '(|';
 795        // set wildcards
 796        $wp = $ws = '';
 797        if (!empty($this->prop['fuzzy_search']) && $mode != 1) {
 798            $ws = '*';
 799            if (!$mode) {
 800                $wp = '*';
 801            }
 802        }
 803
 804        if ($fields == '*') {
 805            // search_fields are required for fulltext search
 806            if (empty($this->prop['search_fields'])) {
 807                $this->set_error(self::ERROR_SEARCH, 'nofulltextsearch');
 808                $this->result = new rcube_result_set();
 809                return $this->result;
 810            }
 811            if (is_array($this->prop['search_fields'])) {
 812                foreach ($this->prop['search_fields'] as $field) {
 813                    $filter .= "($field=$wp" . rcube_ldap_generic::quote_string($value) . "$ws)";
 814                }
 815            }
 816        }
 817        else {
 818            foreach ((array)$fields as $idx => $field) {
 819                $val = is_array($value) ? $value[$idx] : $value;
 820                if ($attrs = $this->_map_field($field)) {
 821                    if (count($attrs) > 1)
 822                        $filter .= '(|';
 823                    foreach ($attrs as $f)
 824                        $filter .= "($f=$wp" . rcube_ldap_generic::quote_string($val) . "$ws)";
 825                    if (count($attrs) > 1)
 826                        $filter .= ')';
 827                }
 828            }
 829        }
 830        $filter .= ')';
 831
 832        // add required (non empty) fields filter
 833        $req_filter = '';
 834        foreach ((array)$required as $field) {
 835            if (in_array($field, (array)$fields))  // required field is already in search filter
 836                continue;
 837            if ($attrs = $this->_map_field($field)) {
 838                if (count($attrs) > 1)
 839                    $req_filter .= '(|';
 840                foreach ($attrs as $f)
 841                    $req_filter .= "($f=*)";
 842                if (count($attrs) > 1)
 843                    $req_filter .= ')';
 844            }
 845        }
 846
 847        if (!empty($req_filter))
 848            $filter = '(&' . $req_filter . $filter . ')';
 849
 850        // avoid double-wildcard if $value is empty
 851        $filter = preg_replace('/\*+/', '*', $filter);
 852
 853        // add general filter to query
 854        if (!empty($this->prop['filter']))
 855            $filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->prop['filter']) . ')' . $filter . ')';
 856
 857        // set filter string and execute search
 858        $this->set_search_set($filter);
 859
 860        if ($select)
 861            $this->list_records();
 862        else
 863            $this->result = $this->count();
 864
 865        return $this->result;
 866    }
 867
 868
 869    /**
 870     * Count number of available contacts in database
 871     *
 872     * @return object rcube_result_set Resultset with values for 'count' and 'first'
 873     */
 874    function count()
 875    {
 876        $count = 0;
 877        if ($this->ldap_result) {
 878            $count = $this->ldap_result->count();
 879        }
 880        else if ($this->group_id && $this->group_data['dn']) {
 881            $count = count($this->list_group_members($this->group_data['dn'], true));
 882        }
 883        // We have a connection but no result set, attempt to get one.
 884        else if ($this->ready) {
 885            $prop    = $this->group_id ? $this->group_data : $this->prop;
 886            $base_dn = $this->group_id ? $this->group_base_dn : $this->base_dn;
 887
 888            if (!empty($this->filter)) {  // Use global search filter
 889                $prop['filter'] = $this->filter;
 890            }
 891            $count = $this->ldap->search($base_dn, $prop['filter'], $prop['scope'], array('dn'), $prop, true);
 892        }
 893
 894        return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
 895    }
 896
 897
 898    /**
 899     * Return the last result set
 900     *
 901     * @return object rcube_result_set Current resultset or NULL if nothing selected yet
 902     */
 903    function get_result()
 904    {
 905        return $this->result;
 906    }
 907
 908
 909    /**
 910     * Get a specific contact record
 911     *
 912     * @param mixed   Record identifier
 913     * @param boolean Return as associative array
 914     *
 915     * @return mixed  Hash array or rcube_result_set with all record fields
 916     */
 917    function get_record($dn, $assoc=false)
 918    {
 919        $res = $this->result = null;
 920
 921        if ($this->ready && $dn) {
 922            $dn = self::dn_decode($dn);
 923
 924            if ($rec = $this->ldap->get_entry($dn)) {
 925                $rec = array_change_key_case($rec, CASE_LOWER);
 926            }
 927
 928            // Use ldap_list to get subentries like country (c) attribute (#1488123)
 929            if (!empty($rec) && $this->sub_filter) {
 930                if ($entries = $this->ldap->list_entries($dn, $this->sub_filter, array_keys($this->prop['sub_fields']))) {
 931                    foreach ($entries as $entry) {
 932                        $lrec = array_change_key_case($entry, CASE_LOWER);
 933                        $rec  = array_merge($lrec, $rec);
 934                    }
 935                }
 936            }
 937
 938            if (!empty($rec)) {
 939                // Add in the dn for the entry.
 940                $rec['dn'] = $dn;
 941                $res = $this->_ldap2result($rec);
 942                $this->result = new rcube_result_set(1);
 943                $this->result->add($res);
 944            }
 945        }
 946
 947        return $assoc ? $res : $this->result;
 948    }
 949
 950
 951    /**
 952     * Check the given data before saving.
 953     * If input not valid, the message to display can be fetched using get_error()
 954     *
 955     * @param array Assoziative array with data to save
 956     * @param boolean Try to fix/complete record automatically
 957     * @return boolean True if input is valid, False if not.
 958     */
 959    public function validate(&$save_data, $autofix = false)
 960    {
 961        // validate e-mail addresses
 962        if (!parent::validate($save_data, $autofix)) {
 963            return false;
 964        }
 965
 966        // check for name input
 967        if (empty($save_data['name'])) {
 968            $this->set_error(self::ERROR_VALIDATE, 'nonamewarning');
 969            return false;
 970        }
 971
 972        // Verify that the required fields are set.
 973        $missing = null;
 974        $ldap_data = $this->_map_data($save_data);
 975        foreach ($this->prop['required_fields'] as $fld) {
 976            if (!isset($ldap_data[$fld]) || $ldap_data[$fld] === '') {
 977                $missing[$fld] = 1;
 978            }
 979        }
 980
 981        if ($missing) {
 982            // try to complete record automatically
 983            if ($autofix) {
 984                $sn_field    = $this->fieldmap['surname'];
 985                $fn_field    = $this->fieldmap['firstname'];
 986                $mail_field  = $this->fieldmap['email'];
 987
 988                // try to extract surname and firstname from displayname
 989                $name_parts  = preg_split('/[\s,.]+/', $save_data['name']);
 990
 991                if ($sn_field && $missing[$sn_field]) {
 992                    $save_data['surname'] = array_pop($name_parts);
 993                    unset($missing[$sn_field]);
 994                }
 995
 996                if ($fn_field && $missing[$fn_field]) {
 997                    $save_data['firstname'] = array_shift($name_parts);
 998                    unset($missing[$fn_field]);
 999                }
1000
1001                // try to fix missing e-mail, very often on import
1002                // from vCard we have email:other only defined
1003                if ($mail_field && $missing[$mail_field]) {
1004                    $emails = $this->get_col_values('email', $save_data, true);
1005                    if (!empty($emails) && ($email = array_shift($emails))) {
1006                        $save_data['email'] = $email;
1007                        unset($missing[$mail_field]);
1008                    }
1009                }
1010            }
1011
1012            // TODO: generate message saying which fields are missing
1013            if (!empty($missing)) {
1014                $this->set_error(self::ERROR_VALIDATE, 'formincomplete');
1015                return false;
1016            }
1017        }
1018
1019        return true;
1020    }
1021
1022
1023    /**
1024     * Create a new contact record
1025     *
1026     * @param array    Hash array with save data
1027     *
1028     * @return encoded record ID on success, False on error
1029     */
1030    function insert($save_cols)
1031    {
1032        // Map out the column names to their LDAP ones to build the new entry.
1033        $newentry = $this->_map_data($save_cols);
1034        $newentry['objectClass'] = $this->prop['LDAP_Object_Classes'];
1035
1036        // add automatically generated attributes
1037        $this->add_autovalues($newentry);
1038
1039        // Verify that the required fields are set.
1040        $missing = null;
1041        foreach ($this->prop['required_fields'] as $fld) {
1042            if (!isset($newentry[$fld])) {
1043                $missing[] = $fld;
1044            }
1045        }
1046
1047        // abort process if requiered fields are missing
1048        // TODO: generate message saying which fields are missing
1049        if ($missing) {
1050            $this->set_error(self::ERROR_VALIDATE, 'formincomplete');
1051            return false;
1052        }
1053
1054        // Build the new entries DN.
1055        $dn = $this->prop['LDAP_rdn'].'='.rcube_ldap_generic::quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->base_dn;
1056
1057        // Remove attributes that need to be added separately (child objects)
1058        $xfields = array();
1059        if (!empty($this->prop['sub_fields']) && is_array($this->prop['sub_fields'])) {
1060            foreach (array_keys($this->prop['sub_fields']) as $xf) {
1061                if (!empty($newentry[$xf])) {
1062                    $xfields[$xf] = $newentry[$xf];
1063                    unset($newentry[$xf]);
1064                }
1065            }
1066        }
1067
1068        if (!$this->ldap->add($dn, $newentry)) {
1069            $this->set_error(self::ERROR_SAVING, 'errorsaving');
1070            return false;
1071        }
1072
1073        foreach ($xfields as $xidx => $xf) {
1074            $xdn = $xidx.'='.rcube_ldap_generic::quote_string($xf).','.$dn;
1075            $xf = array(
1076                $xidx => $xf,
1077                'objectClass' => (array) $this->prop['sub_fields'][$xidx],
1078            );
1079
1080            $this->ldap->add($xdn, $xf);
1081        }
1082
1083        $dn = self::dn_encode($dn);
1084
1085        // add new contact to the selected group
1086        if ($this->group_id)
1087            $this->add_to_group($this->group_id, $dn);
1088
1089        return $dn;
1090    }
1091
1092
1093    /**
1094     * Update a specific contact record
1095     *
1096     * @param mixed Record identifier
1097     * @param array Hash array with save data
1098     *
1099     * @return boolean True on success, False on error
1100     */
1101    function update($id, $save_cols)
1102    {
1103        $record = $this->get_record($id, true);
1104
1105        $newdata     = array();
1106        $replacedata = array();
1107        $deletedata  = array();
1108        $subdata     = array();
1109        $subdeldata  = array();
1110        $subnewdata  = array();
1111
1112        $ldap_data = $this->_map_data($save_cols);
1113        $old_data  = $record['_raw_attrib'];
1114
1115        // special handling of photo col
1116        if ($photo_fld = $this->fieldmap['photo']) {
1117            // undefined means keep old photo
1118            if (!array_key_exists('photo', $save_cols)) {
1119                $ldap_data[$photo_fld] = $record['photo'];
1120            }
1121        }
1122
1123        foreach ($this->fieldmap as $fld) {
1124            if ($fld) {
1125                $val = $ldap_data[$fld];
1126                $old = $old_data[$fld];
1127                // remove empty array values
1128                if (is_array($val))
1129                    $val = array_filter($val);
1130                // $this->_map_data() result and _raw_attrib use different format
1131                // make sure comparing array with one element with a string works as expected
1132                if (is_array($old) && count($old) == 1 && !is_array($val)) {
1133                    $old = array_pop($old);
1134                }
1135                if (is_array($val) && count($val) == 1 && !is_array($old)) {
1136                    $val = array_pop($val);
1137                }
1138                // Subentries must be handled separately
1139                if (!empty($this->prop['sub_fields']) && isset($this->prop['sub_fields'][$fld])) {
1140                    if ($old != $val) {
1141                        if ($old !== null) {
1142                            $subdeldata[$fld] = $old;
1143                        }
1144                        if ($val) {
1145                            $subnewdata[$fld] = $val;
1146                        }
1147                    }
1148                    else if ($old !== null) {
1149                        $subdata[$fld] = $old;
1150                    }
1151                    continue;
1152                }
1153
1154                // The field does exist compare it to the ldap record.
1155                if ($old != $val) {
1156                    // Changed, but find out how.
1157                    if ($old === null) {
1158                        // Field was not set prior, need to add it.
1159                        $newdata[$fld] = $val;
1160                    }
1161                    else if ($val == '') {
1162                        // Field supplied is empty, verify that it is not required.
1163                        if (!in_array($fld, $this->prop['required_fields'])) {
1164                            // ...It is not, safe to clear.
1165                            // #1488420: Workaround "ldap_mod_del(): Modify: Inappropriate matching in..."
1166                            // jpegPhoto attribute require an array() here. It looks to me that it works for other attribs too
1167                            $deletedata[$fld] = array();
1168                            //$deletedata[$fld] = $old_data[$fld];
1169                        }
1170                    }
1171                    else {
1172                        // The data was modified, save it out.
1173                        $replacedata[$fld] = $val;
1174                    }
1175                } // end if
1176            } // end if
1177        } // end foreach
1178
1179        // console($old_data, $ldap_data, '----', $newdata, $replacedata, $deletedata, '----', $subdata, $subnewdata, $subdeldata);
1180
1181        $dn = self::dn_decode($id);
1182
1183        // Update the entry as required.
1184        if (!empty($deletedata)) {
1185            // Delete the fields.
1186            if (!$this->ldap->mod_del($dn, $deletedata)) {
1187                $this->set_error(self::ERROR_SAVING, 'errorsaving');
1188                return false;
1189            }
1190        } // end if
1191
1192        if (!empty($replacedata)) {
1193            // Handle RDN change
1194            if ($replacedata[$this->prop['LDAP_rdn']]) {
1195                $newdn = $this->prop['LDAP_rdn'].'='
1196                    .rcube_ldap_generic::quote_string($replacedata[$this->prop['LDAP_rdn']], true)
1197                    .','.$this->base_dn;
1198                if ($dn != $newdn) {
1199                    $newrdn = $this->prop['LDAP_rdn'].'='
1200                    .rcube_ldap_generic::quote_string($replacedata[$this->prop['LDAP_rdn']], true);
1201                    unset($replacedata[$this->prop['LDAP_rdn']]);
1202                }
1203            }
1204            // Replace the fields.
1205            if (!empty($replacedata)) {
1206                if (!$this->ldap->mod_replace($dn, $replacedata)) {
1207                    $this->set_error(self::ERROR_SAVING, 'errorsaving');
1208                    return false;
1209                }
1210            }
1211        } // end if
1212
1213        // RDN change, we need to remove all sub-entries
1214        if (!empty($newrdn)) {
1215            $subdeldata = array_merge($subdeldata, $subdata);
1216            $subnewdata = array_merge($subnewdata, $subdata);
1217        }
1218
1219        // remove sub-entries
1220        if (!empty($subdeldata)) {
1221            foreach ($subdeldata as $fld => $val) {
1222                $subdn = $fld.'='.rcube_ldap_generic::quote_string($val).','.$dn;
1223                if (!$this->ldap->delete($subdn)) {
1224                    return false;
1225                }
1226            }
1227        }
1228
1229        if (!empty($newdata)) {
1230            // Add the fields.
1231            if (!$this->ldap->mod_add($dn, $newdata)) {
1232                $this->set_error(self::ERROR_SAVING, 'errorsaving');
1233                return false;
1234            }
1235        } // end if
1236
1237        // Handle RDN change
1238        if (!empty($newrdn)) {
1239            if (!$this->ldap->rename($dn, $newrdn, null, true)) {
1240                $this->set_error(self::ERROR_SAVING, 'errorsaving');
1241                return false;
1242            }
1243
1244            $dn    = self::dn_encode($dn);
1245            $newdn = self::dn_encode($newdn);
1246
1247            // change the group membership of the contact
1248            if ($this->groups) {
1249                $group_ids = $this->get_record_groups($dn);
1250                foreach (array_keys($group_ids) as $group_id) {
1251                    $this->remove_from_group($group_id, $dn);
1252                    $this->add_to_group($group_id, $newdn);
1253                }
1254            }
1255
1256            $dn = self::dn_decode($newdn);
1257        }
1258
1259        // add sub-entries
1260        if (!empty($subnewdata)) {
1261            foreach ($subnewdata as $fld => $val) {
1262                $subdn = $fld.'='.rcube_ldap_generic::quote_string($val).','.$dn;
1263                $xf = array(
1264                    $fld => $val,
1265                    'objectClass' => (array) $this->prop['sub_fields'][$fld],
1266                );
1267                $this->ldap->add($subdn, $xf);
1268            }
1269        }
1270
1271        return $newdn ? $newdn : true;
1272    }
1273
1274
1275    /**
1276     * Mark one or more contact records as deleted
1277     *
1278     * @param array   Record identifiers
1279     * @param boolean Remove record(s) irreversible (unsupported)
1280     *
1281     * @return boolean True on success, False on error
1282     */
1283    function delete($ids, $force=true)
1284    {
1285        if (!is_array($ids)) {
1286            // Not an array, break apart the encoded DNs.
1287            $ids = explode(',', $ids);
1288        } // end if
1289
1290        foreach ($ids as $id) {
1291            $dn = self::dn_decode($id);
1292
1293            // Need to delete all sub-entries first
1294            if ($this->sub_filter) {
1295                if ($entries = $this->ldap->list_entries($dn, $this->sub_filter)) {
1296                    foreach ($entries as $entry) {
1297                        if (!$this->ldap->delete($entry['dn'])) {
1298                            $this->set_error(self::ERROR_SAVING, 'errorsaving');
1299                            return false;
1300                        }
1301                    }
1302                }
1303            }
1304
1305            // Delete the record.
1306            if (!$this->ldap->delete($dn)) {
1307                $this->set_error(self::ERROR_SAVING, 'errorsaving');
1308                return false;
1309            }
1310
1311            // remove contact from all groups where he was member
1312            if ($this->groups) {
1313                $dn = self::dn_encode($dn);
1314                $group_ids = $this->get_record_groups($dn);
1315                foreach (array_keys($group_ids) as $group_id) {
1316                    $this->remove_from_group($group_id, $dn);
1317                }
1318            }
1319        } // end foreach
1320
1321        return count($ids);
1322    }
1323
1324
1325    /**
1326     * Remove all contact records
1327     *
1328     * @param bool $with_groups Delete also groups if enabled
1329     */
1330    function delete_all($with_groups = false)
1331    {
1332        // searching for contact entries
1333        $dn_list = $this->ldap->list_entries($this->base_dn, $this->prop['filter'] ? $this->prop['f…

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