Thomas Bruederli
2013-06-18 4500b2f79bbd0985ba2e13ff870e6d474370f9ae
commit | author | age
d1d2c4 1 <?php
041c93 2
d1d2c4 3 /*
S 4  +-----------------------------------------------------------------------+
e019f2 5  | This file is part of the Roundcube Webmail client                     |
203323 6  | Copyright (C) 2006-2013, The Roundcube Dev Team                       |
TB 7  | Copyright (C) 2011-2013, Kolab Systems AG                             |
7fe381 8  |                                                                       |
T 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.                     |
d1d2c4 12  |                                                                       |
S 13  | PURPOSE:                                                              |
f11541 14  |   Interface to an LDAP address directory                              |
d1d2c4 15  +-----------------------------------------------------------------------+
f11541 16  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
6039aa 17  |         Andreas Dick <andudi (at) gmx (dot) ch>                       |
c3ba0e 18  |         Aleksander Machniak <machniak@kolabsys.com>                   |
d1d2c4 19  +-----------------------------------------------------------------------+
S 20 */
6d969b 21
T 22 /**
23  * Model class to access an LDAP address directory
24  *
9ab346 25  * @package    Framework
AM 26  * @subpackage Addressbook
6d969b 27  */
cc97ea 28 class rcube_ldap extends rcube_addressbook
f11541 29 {
a97937 30     /** public properties */
T 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();
1148c6 37
a97937 38     /** private properties */
203323 39     protected $ldap;
a97937 40     protected $prop = array();
T 41     protected $fieldmap = array();
13db9e 42     protected $sub_filter;
a97937 43     protected $filter = '';
T 44     protected $result = null;
45     protected $ldap_result = null;
46     protected $mail_domain = '';
47     protected $debug = false;
6039aa 48
d1e08f 49     private $base_dn = '';
T 50     private $groups_base_dn = '';
8fb04b 51     private $group_url = null;
T 52     private $cache;
1148c6 53
A 54
a97937 55     /**
T 56     * Object constructor
57     *
0c2596 58     * @param array      $p            LDAP connection properties
A 59     * @param boolean $debug        Enables debug mode
60     * @param string  $mail_domain  Current user mail domain name
a97937 61     */
0c2596 62     function __construct($p, $debug = false, $mail_domain = null)
f11541 63     {
a97937 64         $this->prop = $p;
b1f084 65
49cb69 66         $fetch_attributes = array('objectClass');
2d3e2b 67
T 68         if (isset($p['searchonly']))
69             $this->searchonly = $p['searchonly'];
010274 70
a97937 71         // check if groups are configured
f763fb 72         if (is_array($p['groups']) && count($p['groups'])) {
a97937 73             $this->groups = true;
f763fb 74             // set member field
A 75             if (!empty($p['groups']['member_attr']))
015dec 76                 $this->prop['member_attr'] = strtolower($p['groups']['member_attr']);
f763fb 77             else if (empty($p['member_attr']))
A 78                 $this->prop['member_attr'] = 'member';
448f81 79             // set default name attribute to cn
T 80             if (empty($this->prop['groups']['name_attr']))
81                 $this->prop['groups']['name_attr'] = 'cn';
fb6cc8 82             if (empty($this->prop['groups']['scope']))
T 83                 $this->prop['groups']['scope'] = 'sub';
ec2185 84
49cb69 85             // add group name attrib to the list of attributes to be fetched
TB 86             $fetch_attributes[] = $this->prop['groups']['name_attr'];
ec2185 87         }
TB 88         else if (is_array($p['group_filters']) && count($p['group_filters'])) {
89             $this->groups = true;
f763fb 90         }
cd6749 91
a97937 92         // fieldmap property is given
T 93         if (is_array($p['fieldmap'])) {
94             foreach ($p['fieldmap'] as $rf => $lf)
95                 $this->fieldmap[$rf] = $this->_attr_name(strtolower($lf));
96         }
38dc51 97         else if (!empty($p)) {
a97937 98             // read deprecated *_field properties to remain backwards compatible
T 99             foreach ($p as $prop => $value)
100                 if (preg_match('/^(.+)_field$/', $prop, $matches))
101                     $this->fieldmap[$matches[1]] = $this->_attr_name(strtolower($value));
b9f9f1 102         }
4f9c83 103
a97937 104         // use fieldmap to advertise supported coltypes to the application
a605b2 105         foreach ($this->fieldmap as $colv => $lfv) {
T 106             list($col, $type) = explode(':', $colv);
107             list($lf, $limit, $delim) = explode(':', $lfv);
108
109             if ($limit == '*') $limit = null;
110             else               $limit = max(1, intval($limit));
111
a97937 112             if (!is_array($this->coltypes[$col])) {
T 113                 $subtypes = $type ? array($type) : null;
1078a6 114                 $this->coltypes[$col] = array('limit' => $limit, 'subtypes' => $subtypes, 'attributes' => array($lf));
1148c6 115             }
a97937 116             elseif ($type) {
T 117                 $this->coltypes[$col]['subtypes'][] = $type;
1078a6 118                 $this->coltypes[$col]['attributes'][] = $lf;
a605b2 119                 $this->coltypes[$col]['limit'] += $limit;
a97937 120             }
a605b2 121
T 122             if ($delim)
123                $this->coltypes[$col]['serialized'][$type] = $delim;
124
1078a6 125            $this->fieldmap[$colv] = $lf;
1148c6 126         }
f76765 127
1937f4 128         // support for composite address
1078a6 129         if ($this->coltypes['street'] && $this->coltypes['locality']) {
a605b2 130             $this->coltypes['address'] = array(
T 131                'limit'    => max(1, $this->coltypes['locality']['limit'] + $this->coltypes['address']['limit']),
5b0b03 132                'subtypes' => array_merge((array)$this->coltypes['address']['subtypes'], (array)$this->coltypes['locality']['subtypes']),
a605b2 133                'childs' => array(),
T 134                ) + (array)$this->coltypes['address'];
135
3ac5cd 136             foreach (array('street','locality','zipcode','region','country') as $childcol) {
1078a6 137                 if ($this->coltypes[$childcol]) {
3ac5cd 138                     $this->coltypes['address']['childs'][$childcol] = array('type' => 'text');
T 139                     unset($this->coltypes[$childcol]);  // remove address child col from global coltypes list
140                 }
141             }
fb001f 142
AM 143             // at least one address type must be specified
144             if (empty($this->coltypes['address']['subtypes'])) {
145                 $this->coltypes['address']['subtypes'] = array('home');
146             }
1937f4 147         }
19fccd 148         else if ($this->coltypes['address']) {
24f1bf 149             $this->coltypes['address'] += array('type' => 'textarea', 'childs' => null, 'size' => 40);
T 150
151             // 'serialized' means the UI has to present a composite address field
152             if ($this->coltypes['address']['serialized']) {
153                 $childprop = array('type' => 'text');
154                 $this->coltypes['address']['type'] = 'composite';
155                 $this->coltypes['address']['childs'] = array('street' => $childprop, 'locality' => $childprop, 'zipcode' => $childprop, 'country' => $childprop);
156             }
19fccd 157         }
4f9c83 158
a97937 159         // make sure 'required_fields' is an array
19fccd 160         if (!is_array($this->prop['required_fields'])) {
a97937 161             $this->prop['required_fields'] = (array) $this->prop['required_fields'];
19fccd 162         }
4f9c83 163
19fccd 164         // make sure LDAP_rdn field is required
540e13 165         if (!empty($this->prop['LDAP_rdn']) && !in_array($this->prop['LDAP_rdn'], $this->prop['required_fields'])
AM 166             && !in_array($this->prop['LDAP_rdn'], array_keys((array)$this->prop['autovalues']))) {
19fccd 167             $this->prop['required_fields'][] = $this->prop['LDAP_rdn'];
A 168         }
169
170         foreach ($this->prop['required_fields'] as $key => $val) {
a97937 171             $this->prop['required_fields'][$key] = $this->_attr_name(strtolower($val));
19fccd 172         }
13db9e 173
A 174         // Build sub_fields filter
175         if (!empty($this->prop['sub_fields']) && is_array($this->prop['sub_fields'])) {
176             $this->sub_filter = '';
3725cf 177             foreach ($this->prop['sub_fields'] as $class) {
13db9e 178                 if (!empty($class)) {
A 179                     $class = is_array($class) ? array_pop($class) : $class;
180                     $this->sub_filter .= '(objectClass=' . $class . ')';
181                 }
182             }
183             if (count($this->prop['sub_fields']) > 1) {
184                 $this->sub_filter = '(|' . $this->sub_filter . ')';
185             }
186         }
f11541 187
f763fb 188         $this->sort_col    = is_array($p['sort']) ? $p['sort'][0] : $p['sort'];
A 189         $this->debug       = $debug;
a97937 190         $this->mail_domain = $mail_domain;
8fb04b 191
T 192         // initialize cache
b07426 193         $rcube      = rcube::get_instance();
AM 194         $cache_type = $rcube->config->get('ldap_cache', 'db');
195         $cache_ttl  = $rcube->config->get('ldap_cache_ttl', '10m');
196         $cache_name = 'LDAP.' . asciiwords($this->prop['name']);
197
198         $this->cache = $rcube->get_cache($cache_name, $cache_type, $cache_ttl);
f11541 199
807c3d 200         // determine which attributes to fetch
f924f5 201         $this->prop['attributes'] = array_merge(array_values($this->fieldmap), $fetch_attributes);
807c3d 202         $this->prop['list_attributes'] = $fetch_attributes;
TB 203         foreach ($rcube->config->get('contactlist_fields') as $col) {
204             $this->prop['list_attributes'] = array_merge($this->prop['list_attributes'], $this->_map_field($col));
205         }
206
207         // initialize ldap wrapper object
203323 208         $this->ldap = new rcube_ldap_generic($this->prop, true);
004f86 209         $this->ldap->set_cache($this->cache);
203323 210
a97937 211         $this->_connect();
736307 212     }
010274 213
736307 214
a97937 215     /**
T 216     * Establish a connection to the LDAP server
217     */
218     private function _connect()
6b603d 219     {
be98df 220         $rcube = rcube::get_instance();
f11541 221
203323 222         if ($this->ready)
a97937 223             return true;
f11541 224
a97937 225         if (!is_array($this->prop['hosts']))
T 226             $this->prop['hosts'] = array($this->prop['hosts']);
f11541 227
e114a6 228         // try to connect + bind for every host configured
TB 229         // with OpenLDAP 2.x ldap_connect() always succeeds but ldap_bind will fail if host isn't reachable
230         // see http://www.php.net/manual/en/function.ldap-connect.php
231         foreach ($this->prop['hosts'] as $host) {
203323 232             // skip host if connection failed
TB 233             if (!$this->ldap->connect($host)) {
e114a6 234                 continue;
TB 235             }
236
237             // See if the directory is writeable.
238             if ($this->prop['writable']) {
239                 $this->readonly = false;
240             }
241
242             $bind_pass = $this->prop['bind_pass'];
243             $bind_user = $this->prop['bind_user'];
244             $bind_dn   = $this->prop['bind_dn'];
245
246             $this->base_dn        = $this->prop['base_dn'];
247             $this->groups_base_dn = ($this->prop['groups']['base_dn']) ?
203323 248                 $this->prop['groups']['base_dn'] : $this->base_dn;
e114a6 249
TB 250             // User specific access, generate the proper values to use.
251             if ($this->prop['user_specific']) {
252                 // No password set, use the session password
253                 if (empty($bind_pass)) {
254                     $bind_pass = $rcube->get_user_password();
255                 }
256
257                 // Get the pieces needed for variable replacement.
258                 if ($fu = $rcube->get_user_email())
259                     list($u, $d) = explode('@', $fu);
260                 else
261                     $d = $this->mail_domain;
262
263                 $dc = 'dc='.strtr($d, array('.' => ',dc=')); // hierarchal domain string
264
265                 $replaces = array('%dn' => '', '%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u);
266
267                 if ($this->prop['search_base_dn'] && $this->prop['search_filter']) {
268                     if (!empty($this->prop['search_bind_dn']) && !empty($this->prop['search_bind_pw'])) {
203323 269                         if (!$this->ldap->bind($this->prop['search_bind_dn'], $this->prop['search_bind_pw']))
TB 270                             continue;  // bind failed, try neyt host
e114a6 271                     }
TB 272
273                     // Search for the dn to use to authenticate
274                     $this->prop['search_base_dn'] = strtr($this->prop['search_base_dn'], $replaces);
275                     $this->prop['search_filter'] = strtr($this->prop['search_filter'], $replaces);
276
277                     $this->_debug("S: searching with base {$this->prop['search_base_dn']} for {$this->prop['search_filter']}");
278
203323 279                     // TODO: use $this->ldap->search() here
TB 280                     $res = @ldap_search($this->ldap->conn, $this->prop['search_base_dn'], $this->prop['search_filter'], array('uid'));
e114a6 281                     if ($res) {
203323 282                         if (($entry = ldap_first_entry($this->ldap->conn, $res))
TB 283                             && ($bind_dn = ldap_get_dn($this->ldap->conn, $entry))
e114a6 284                         ) {
TB 285                             $this->_debug("S: search returned dn: $bind_dn");
286                             $dn = ldap_explode_dn($bind_dn, 1);
287                             $replaces['%dn'] = $dn[0];
288                         }
289                     }
290                     else {
203323 291                         $this->_debug("S: ".ldap_error($this->ldap->conn));
e114a6 292                     }
TB 293
294                     // DN not found
295                     if (empty($replaces['%dn'])) {
296                         if (!empty($this->prop['search_dn_default']))
297                             $replaces['%dn'] = $this->prop['search_dn_default'];
298                         else {
299                             rcube::raise_error(array(
300                                 'code' => 100, 'type' => 'ldap',
301                                 'file' => __FILE__, 'line' => __LINE__,
302                                 'message' => "DN not found using LDAP search."), true);
303                             return false;
304                         }
305                     }
306                 }
307
308                 // Replace the bind_dn and base_dn variables.
309                 $bind_dn              = strtr($bind_dn, $replaces);
310                 $this->base_dn        = strtr($this->base_dn, $replaces);
311                 $this->groups_base_dn = strtr($this->groups_base_dn, $replaces);
312
313                 if (empty($bind_user)) {
314                     $bind_user = $u;
315                 }
316             }
317
318             if (empty($bind_pass)) {
319                 $this->ready = true;
320             }
321             else {
322                 if (!empty($bind_dn)) {
203323 323                     $this->ready = $this->ldap->bind($bind_dn, $bind_pass);
e114a6 324                 }
TB 325                 else if (!empty($this->prop['auth_cid'])) {
203323 326                     $this->ready = $this->ldap->sasl_bind($this->prop['auth_cid'], $bind_pass, $bind_user);
e114a6 327                 }
TB 328                 else {
203323 329                     $this->ready = $this->ldap->sasl_bind($bind_user, $bind_pass);
e114a6 330                 }
TB 331             }
332
333             // connection established, we're done here
334             if ($this->ready) {
a97937 335                 break;
T 336             }
337
e114a6 338         }  // end foreach hosts
c041d5 339
203323 340         if (!is_resource($this->ldap->conn)) {
0c2596 341             rcube::raise_error(array('code' => 100, 'type' => 'ldap',
c041d5 342                 'file' => __FILE__, 'line' => __LINE__,
203323 343                 'message' => "Could not connect to any LDAP server, last tried $host"), true);
c041d5 344
A 345             return false;
346         }
347
348         return $this->ready;
a97937 349     }
T 350
351
352     /**
cc90ed 353      * Close connection to LDAP server
A 354      */
a97937 355     function close()
T 356     {
203323 357         if ($this->ldap) {
TB 358             $this->ldap->close();
a97937 359         }
T 360     }
361
362
363     /**
cc90ed 364      * Returns address book name
A 365      *
366      * @return string Address book name
367      */
368     function get_name()
369     {
370         return $this->prop['name'];
203323 371     }
TB 372
373
374     /**
375      * Set internal list page
376      *
377      * @param  number  Page number to list
378      */
379     function set_page($page)
380     {
381         $this->list_page = (int)$page;
382         $this->ldap->set_vlv_page($this->list_page, $this->page_size);
383     }
384
385     /**
386      * Set internal page size
387      *
388      * @param  number  Number of records to display on one page
389      */
390     function set_pagesize($size)
391     {
392         $this->page_size = (int)$size;
393         $this->ldap->set_vlv_page($this->list_page, $this->page_size);
cc90ed 394     }
A 395
396
397     /**
438753 398      * Set internal sort settings
cc90ed 399      *
438753 400      * @param string $sort_col Sort column
T 401      * @param string $sort_order Sort order
cc90ed 402      */
438753 403     function set_sort_order($sort_col, $sort_order = null)
a97937 404     {
1078a6 405         if ($this->coltypes[$sort_col]['attributes'])
TB 406             $this->sort_col = $this->coltypes[$sort_col]['attributes'][0];
a97937 407     }
T 408
409
410     /**
cc90ed 411      * Save a search string for future listings
A 412      *
413      * @param string $filter Filter string
414      */
a97937 415     function set_search_set($filter)
T 416     {
417         $this->filter = $filter;
418     }
419
420
421     /**
cc90ed 422      * Getter for saved search properties
A 423      *
424      * @return mixed Search properties used by this class
425      */
a97937 426     function get_search_set()
T 427     {
428         return $this->filter;
429     }
430
431
432     /**
cc90ed 433      * Reset all saved results and search parameters
A 434      */
a97937 435     function reset()
T 436     {
437         $this->result = null;
438         $this->ldap_result = null;
439         $this->filter = '';
440     }
441
442
443     /**
cc90ed 444      * List the current set of contact records
A 445      *
446      * @param  array  List of cols to show
447      * @param  int    Only return this number of records
448      *
449      * @return array  Indexed list of contact records, each a hash array
450      */
a97937 451     function list_records($cols=null, $subset=0)
T 452     {
203323 453         if ($this->prop['searchonly'] && empty($this->filter) && !$this->group_id) {
2d3e2b 454             $this->result = new rcube_result_set(0);
T 455             $this->result->searchonly = true;
456             return $this->result;
457         }
b1f084 458
a31482 459         // fetch group members recursively
203323 460         if ($this->group_id && $this->group_data['dn']) {
a31482 461             $entries = $this->list_group_members($this->group_data['dn']);
T 462
463             // make list of entries unique and sort it
464             $seen = array();
465             foreach ($entries as $i => $rec) {
466                 if ($seen[$rec['dn']]++)
467                     unset($entries[$i]);
468             }
469             usort($entries, array($this, '_entry_sort_cmp'));
470
471             $entries['count'] = count($entries);
472             $this->result = new rcube_result_set($entries['count'], ($this->list_page-1) * $this->page_size);
473         }
203323 474         else {
ec2185 475             $prop = $this->group_id ? $this->group_data : $this->prop;
TB 476
203323 477             // use global search filter
TB 478             if (!empty($this->filter))
479                 $prop['filter'] = $this->filter;
a31482 480
T 481             // exec LDAP search if no result resource is stored
203323 482             if ($this->ready && !$this->ldap_result)
TB 483                 $this->ldap_result = $this->ldap->search($prop['base_dn'], $prop['filter'], $prop['scope'], $this->prop['attributes'], $prop);
a31482 484
T 485             // count contacts for this user
486             $this->result = $this->count();
487
488             // we have a search result resource
203323 489             if ($this->ldap_result && $this->result->count > 0) {
a31482 490                 // sorting still on the ldap server
203323 491                 if ($this->sort_col && $prop['scope'] !== 'base' && !$this->ldap->vlv_active)
TB 492                     $this->ldap_result->sort($this->sort_col);
a31482 493
T 494                 // get all entries from the ldap server
203323 495                 $entries = $this->ldap_result->entries();
a31482 496             }
T 497
498         }  // end else
499
500         // start and end of the page
203323 501         $start_row = $this->ldap->vlv_active ? 0 : $this->result->first;
a31482 502         $start_row = $subset < 0 ? $start_row + $this->page_size + $subset : $start_row;
T 503         $last_row = $this->result->first + $this->page_size;
504         $last_row = $subset != 0 ? $start_row + abs($subset) : $last_row;
505
506         // filter entries for this page
507         for ($i = $start_row; $i < min($entries['count'], $last_row); $i++)
508             $this->result->add($this->_ldap2result($entries[$i]));
509
510         return $this->result;
511     }
512
513     /**
9b3311 514      * Get all members of the given group
A 515      *
516      * @param string Group DN
517      * @param array  Group entries (if called recursively)
518      * @return array Accumulated group members
a31482 519      */
b35a0f 520     function list_group_members($dn, $count = false, $entries = null)
a31482 521     {
T 522         $group_members = array();
523
524         // fetch group object
525         if (empty($entries)) {
ec2185 526             $this->_debug("C: Read Group [dn: $dn]");
203323 527             $entries = $this->ldap->read_entries($dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL'));
TB 528             if ($entries === false) {
a31482 529                 return $group_members;
T 530             }
531         }
532
203323 533         for ($i=0; $i < $entries['count']; $i++) {
a31482 534             $entry = $entries[$i];
T 535
536             if (empty($entry['objectclass']))
537                 continue;
538
203323 539             foreach ((array)$entry['objectclass'] as $objectclass) {
a31482 540                 switch (strtolower($objectclass)) {
337dc5 541                     case "group":
a31482 542                     case "groupofnames":
T 543                     case "kolabgroupofnames":
b35a0f 544                         $group_members = array_merge($group_members, $this->_list_group_members($dn, $entry, 'member', $count));
a31482 545                         break;
T 546                     case "groupofuniquenames":
547                     case "kolabgroupofuniquenames":
b35a0f 548                         $group_members = array_merge($group_members, $this->_list_group_members($dn, $entry, 'uniquemember', $count));
a31482 549                         break;
T 550                     case "groupofurls":
b35a0f 551                         $group_members = array_merge($group_members, $this->_list_group_memberurl($dn, $entry, $count));
a31482 552                         break;
T 553                 }
554             }
337dc5 555
fb6cc8 556             if ($this->prop['sizelimit'] && count($group_members) > $this->prop['sizelimit'])
T 557               break;
a31482 558         }
T 559
560         return array_filter($group_members);
561     }
562
563     /**
564      * Fetch members of the given group entry from server
565      *
566      * @param string Group DN
567      * @param array  Group entry
568      * @param string Member attribute to use
569      * @return array Accumulated group members
570      */
b35a0f 571     private function _list_group_members($dn, $entry, $attr, $count)
a31482 572     {
T 573         // Use the member attributes to return an array of member ldap objects
574         // NOTE that the member attribute is supposed to contain a DN
575         $group_members = array();
576         if (empty($entry[$attr]))
577             return $group_members;
578
b35a0f 579         // read these attributes for all members
807c3d 580         $attrib = $count ? array('dn','objectClass') : $this->prop['list_attributes'];
b35a0f 581         $attrib[] = 'member';
T 582         $attrib[] = 'uniqueMember';
583         $attrib[] = 'memberURL';
584
c5a5f9 585         $filter = $this->prop['groups']['member_filter'] ? $this->prop['groups']['member_filter'] : '(objectclass=*)';
TB 586
203323 587         for ($i=0; $i < $entry[$attr]['count']; $i++) {
3ed9e8 588             if (empty($entry[$attr][$i]))
T 589                 continue;
590
203323 591             $members = $this->ldap->read_entries($entry[$attr][$i], $filter, $attrib);
TB 592             if ($members == false) {
a31482 593                 $members = array();
T 594             }
595
596             // for nested groups, call recursively
b35a0f 597             $nested_group_members = $this->list_group_members($entry[$attr][$i], $count, $members);
a31482 598
T 599             unset($members['count']);
600             $group_members = array_merge($group_members, array_filter($members), $nested_group_members);
601         }
602
603         return $group_members;
604     }
605
606     /**
607      * List members of group class groupOfUrls
608      *
609      * @param string Group DN
610      * @param array  Group entry
b35a0f 611      * @param boolean True if only used for counting
a31482 612      * @return array Accumulated group members
T 613      */
b35a0f 614     private function _list_group_memberurl($dn, $entry, $count)
a31482 615     {
T 616         $group_members = array();
617
807c3d 618         for ($i=0; $i < $entry['memberurl']['count']; $i++) {
8fb04b 619             // extract components from url
a31482 620             if (!preg_match('!ldap:///([^\?]+)\?\?(\w+)\?(.*)$!', $entry['memberurl'][$i], $m))
T 621                 continue;
622
623             // add search filter if any
624             $filter = $this->filter ? '(&(' . $m[3] . ')(' . $this->filter . '))' : $m[3];
807c3d 625             $attrs = $count ? array('dn','objectClass') : $this->prop['list_attributes'];
203323 626             if ($result = $this->ldap->search($m[1], $filter, $m[2], $attrs, $this->group_data)) {
TB 627                 $entries = $result->entries();
ec2185 628                 for ($j = 0; $j < $entries['count']; $j++) {
f924f5 629                     if (self::is_group_entry($entries[$j]) && ($nested_group_members = $this->list_group_members($entries[$j]['dn'], $count)))
ec2185 630                         $group_members = array_merge($group_members, $nested_group_members);
TB 631                     else
632                         $group_members[] = $entries[$j];
633                 }
8fb04b 634             }
a97937 635         }
39cafa 636
a31482 637         return $group_members;
T 638     }
a97937 639
a31482 640     /**
T 641      * Callback for sorting entries
642      */
643     function _entry_sort_cmp($a, $b)
644     {
645         return strcmp($a[$this->sort_col][0], $b[$this->sort_col][0]);
a97937 646     }
T 647
648
649     /**
cc90ed 650      * Search contacts
A 651      *
652      * @param mixed   $fields   The field name of array of field names to search in
653      * @param mixed   $value    Search value (or array of values when $fields is array)
f21a04 654      * @param int     $mode     Matching mode:
A 655      *                          0 - partial (*abc*),
656      *                          1 - strict (=),
657      *                          2 - prefix (abc*)
cc90ed 658      * @param boolean $select   True if results are requested, False if count only
A 659      * @param boolean $nocount  (Not used)
660      * @param array   $required List of fields that cannot be empty
661      *
662      * @return array  Indexed list of contact records and 'count' value
663      */
f21a04 664     function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array())
a97937 665     {
f21a04 666         $mode = intval($mode);
A 667
a97937 668         // special treatment for ID-based search
203323 669         if ($fields == 'ID' || $fields == $this->primary_key) {
648674 670             $ids = !is_array($value) ? explode(',', $value) : $value;
a97937 671             $result = new rcube_result_set();
203323 672             foreach ($ids as $id) {
TB 673                 if ($rec = $this->get_record($id, true)) {
a97937 674                     $result->add($rec);
T 675                     $result->count++;
676                 }
677             }
678             return $result;
679         }
680
fc91c1 681         // use VLV pseudo-search for autocompletion
e1cfb0 682         $rcube = rcube::get_instance();
699cb1 683         $list_fields = $rcube->config->get('contactlist_fields');
baecd8 684
203323 685         if ($this->prop['vlv_search'] && $this->ready && join(',', (array)$fields) == join(',', $list_fields)) {
9b3311 686             $this->result = new rcube_result_set(0);
A 687
772b73 688             $search_suffix = $this->prop['fuzzy_search'] && $mode != 1 ? '*' : '';
TB 689             $ldap_data = $this->ldap->search($this->base_dn, $this->prop['filter'], $this->prop['scope'], $this->prop['attributes'],
690                 array('search' => $value . $search_suffix /*, 'sort' => $this->prop['sort'] */));
691             if ($ldap_data === false) {
6bddd9 692                 return $this->result;
A 693             }
9b3311 694
fc91c1 695             // get all entries of this page and post-filter those that really match the query
772b73 696             $search = mb_strtolower($value);
TB 697             foreach ($ldap_data as $i => $entry) {
698                 $rec = $this->_ldap2result($entry);
699cb1 699                 foreach ($fields as $f) {
AM 700                     foreach ((array)$rec[$f] as $val) {
83f707 701                         if ($this->compare_search_value($f, $val, $search, $mode)) {
699cb1 702                             $this->result->add($rec);
AM 703                             $this->result->count++;
704                             break 2;
705                         }
f21a04 706                     }
fc91c1 707                 }
T 708             }
709
710             return $this->result;
711         }
712
e9a9f2 713         // use AND operator for advanced searches
A 714         $filter = is_array($value) ? '(&' : '(|';
f21a04 715         // set wildcards
A 716         $wp = $ws = '';
717         if (!empty($this->prop['fuzzy_search']) && $mode != 1) {
718             $ws = '*';
719             if (!$mode) {
720                 $wp = '*';
721             }
722         }
e9a9f2 723
203323 724         if ($fields == '*') {
3e2637 725             // search_fields are required for fulltext search
203323 726             if (empty($this->prop['search_fields'])) {
3e2637 727                 $this->set_error(self::ERROR_SEARCH, 'nofulltextsearch');
T 728                 $this->result = new rcube_result_set();
729                 return $this->result;
730             }
203323 731             if (is_array($this->prop['search_fields'])) {
e9a9f2 732                 foreach ($this->prop['search_fields'] as $field) {
4500b2 733                     $filter .= "($field=$wp" . rcube_ldap_generic::quote_string($value) . "$ws)";
e9a9f2 734                 }
3cacf9 735             }
a97937 736         }
203323 737         else {
e9a9f2 738             foreach ((array)$fields as $idx => $field) {
A 739                 $val = is_array($value) ? $value[$idx] : $value;
1078a6 740                 if ($attrs = $this->_map_field($field)) {
TB 741                     if (count($attrs) > 1)
742                         $filter .= '(|';
743                     foreach ($attrs as $f)
4500b2 744                         $filter .= "($f=$wp" . rcube_ldap_generic::quote_string($val) . "$ws)";
1078a6 745                     if (count($attrs) > 1)
TB 746                         $filter .= ')';
e9a9f2 747                 }
A 748             }
a97937 749         }
T 750         $filter .= ')';
751
752         // add required (non empty) fields filter
753         $req_filter = '';
1078a6 754         foreach ((array)$required as $field) {
4500b2 755             if (in_array($field, (array)$fields))  // required field is already in search filter
TB 756                 continue;
1078a6 757             if ($attrs = $this->_map_field($field)) {
TB 758                 if (count($attrs) > 1)
759                     $req_filter .= '(|';
760                 foreach ($attrs as $f)
761                     $req_filter .= "($f=*)";
762                 if (count($attrs) > 1)
763                     $req_filter .= ')';
764             }
765         }
a97937 766
T 767         if (!empty($req_filter))
768             $filter = '(&' . $req_filter . $filter . ')';
769
770         // avoid double-wildcard if $value is empty
771         $filter = preg_replace('/\*+/', '*', $filter);
772
773         // add general filter to query
774         if (!empty($this->prop['filter']))
775             $filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->prop['filter']) . ')' . $filter . ')';
776
777         // set filter string and execute search
778         $this->set_search_set($filter);
779
780         if ($select)
781             $this->list_records();
782         else
783             $this->result = $this->count();
784
785         return $this->result;
786     }
787
788
789     /**
cc90ed 790      * Count number of available contacts in database
A 791      *
792      * @return object rcube_result_set Resultset with values for 'count' and 'first'
793      */
a97937 794     function count()
T 795     {
796         $count = 0;
203323 797         if ($this->ldap_result) {
TB 798             $count = $this->ldap_result->count();
a31482 799         }
T 800         else if ($this->group_id && $this->group_data['dn']) {
b35a0f 801             $count = count($this->list_group_members($this->group_data['dn'], true));
a31482 802         }
203323 803         // We have a connection but no result set, attempt to get one.
TB 804         else if ($this->ready) {
ec2185 805             $prop = $this->group_id ? $this->group_data : $this->prop;
TB 806
203323 807             if (!empty($this->filter)) {  // Use global search filter
TB 808                 $prop['filter'] = $this->filter;
a31482 809             }
122446 810
203323 811             $count = $this->ldap->search($prop['base_dn'], $prop['filter'], $prop['scope'], array('dn'), $prop, true);
a31482 812         }
a97937 813
T 814         return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
815     }
816
817
818     /**
cc90ed 819      * Return the last result set
A 820      *
821      * @return object rcube_result_set Current resultset or NULL if nothing selected yet
822      */
a97937 823     function get_result()
T 824     {
825         return $this->result;
826     }
827
828
829     /**
cc90ed 830      * Get a specific contact record
A 831      *
832      * @param mixed   Record identifier
833      * @param boolean Return as associative array
834      *
835      * @return mixed  Hash array or rcube_result_set with all record fields
836      */
a97937 837     function get_record($dn, $assoc=false)
T 838     {
13db9e 839         $res = $this->result = null;
A 840
203323 841         if ($this->ready && $dn) {
c3ba0e 842             $dn = self::dn_decode($dn);
a97937 843
203323 844             if ($rec = $this->ldap->get_entry($dn)) {
TB 845                 $rec = array_change_key_case($rec, CASE_LOWER);
13db9e 846             }
a97937 847
13db9e 848             // Use ldap_list to get subentries like country (c) attribute (#1488123)
A 849             if (!empty($rec) && $this->sub_filter) {
203323 850                 if ($entries = $this->ldap->list_entries($dn, $this->sub_filter, array_keys($this->prop['sub_fields']))) {
13db9e 851                     foreach ($entries as $entry) {
A 852                         $lrec = array_change_key_case($entry, CASE_LOWER);
853                         $rec  = array_merge($lrec, $rec);
854                     }
855                 }
856             }
a97937 857
13db9e 858             if (!empty($rec)) {
a97937 859                 // Add in the dn for the entry.
T 860                 $rec['dn'] = $dn;
861                 $res = $this->_ldap2result($rec);
203323 862                 $this->result = new rcube_result_set(1);
a97937 863                 $this->result->add($res);
6039aa 864             }
T 865         }
a97937 866
T 867         return $assoc ? $res : $this->result;
f11541 868     }
T 869
870
a97937 871     /**
e84818 872      * Check the given data before saving.
T 873      * If input not valid, the message to display can be fetched using get_error()
874      *
875      * @param array Assoziative array with data to save
39cafa 876      * @param boolean Try to fix/complete record automatically
e84818 877      * @return boolean True if input is valid, False if not.
T 878      */
39cafa 879     public function validate(&$save_data, $autofix = false)
e84818 880     {
19fccd 881         // validate e-mail addresses
A 882         if (!parent::validate($save_data, $autofix)) {
883             return false;
884         }
885
e84818 886         // check for name input
T 887         if (empty($save_data['name'])) {
39cafa 888             $this->set_error(self::ERROR_VALIDATE, 'nonamewarning');
T 889             return false;
890         }
891
892         // Verify that the required fields are set.
893         $missing = null;
894         $ldap_data = $this->_map_data($save_data);
895         foreach ($this->prop['required_fields'] as $fld) {
19fccd 896             if (!isset($ldap_data[$fld]) || $ldap_data[$fld] === '') {
39cafa 897                 $missing[$fld] = 1;
T 898             }
899         }
900
901         if ($missing) {
902             // try to complete record automatically
903             if ($autofix) {
19fccd 904                 $sn_field    = $this->fieldmap['surname'];
A 905                 $fn_field    = $this->fieldmap['firstname'];
9336ba 906                 $mail_field  = $this->fieldmap['email'];
A 907
908                 // try to extract surname and firstname from displayname
909                 $name_parts  = preg_split('/[\s,.]+/', $save_data['name']);
19fccd 910
A 911                 if ($sn_field && $missing[$sn_field]) {
912                     $save_data['surname'] = array_pop($name_parts);
913                     unset($missing[$sn_field]);
39cafa 914                 }
T 915
19fccd 916                 if ($fn_field && $missing[$fn_field]) {
A 917                     $save_data['firstname'] = array_shift($name_parts);
918                     unset($missing[$fn_field]);
919                 }
9336ba 920
A 921                 // try to fix missing e-mail, very often on import
922                 // from vCard we have email:other only defined
923                 if ($mail_field && $missing[$mail_field]) {
924                     $emails = $this->get_col_values('email', $save_data, true);
925                     if (!empty($emails) && ($email = array_shift($emails))) {
926                         $save_data['email'] = $email;
927                         unset($missing[$mail_field]);
928                     }
929                 }
39cafa 930             }
T 931
932             // TODO: generate message saying which fields are missing
19fccd 933             if (!empty($missing)) {
A 934                 $this->set_error(self::ERROR_VALIDATE, 'formincomplete');
935                 return false;
936             }
e84818 937         }
cc90ed 938
19fccd 939         return true;
e84818 940     }
T 941
942
943     /**
cc90ed 944      * Create a new contact record
A 945      *
946      * @param array    Hash array with save data
947      *
948      * @return encoded record ID on success, False on error
949      */
a97937 950     function insert($save_cols)
f11541 951     {
a97937 952         // Map out the column names to their LDAP ones to build the new entry.
39cafa 953         $newentry = $this->_map_data($save_cols);
a97937 954         $newentry['objectClass'] = $this->prop['LDAP_Object_Classes'];
T 955
3f250a 956         // add automatically generated attributes
TB 957         $this->add_autovalues($newentry);
958
a97937 959         // Verify that the required fields are set.
6f45fa 960         $missing = null;
a97937 961         foreach ($this->prop['required_fields'] as $fld) {
T 962             if (!isset($newentry[$fld])) {
963                 $missing[] = $fld;
964             }
965         }
966
967         // abort process if requiered fields are missing
968         // TODO: generate message saying which fields are missing
969         if ($missing) {
39cafa 970             $this->set_error(self::ERROR_VALIDATE, 'formincomplete');
a97937 971             return false;
T 972         }
973
974         // Build the new entries DN.
4500b2 975         $dn = $this->prop['LDAP_rdn'].'='.rcube_ldap_generic::quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->base_dn;
a97937 976
13db9e 977         // Remove attributes that need to be added separately (child objects)
A 978         $xfields = array();
979         if (!empty($this->prop['sub_fields']) && is_array($this->prop['sub_fields'])) {
3725cf 980             foreach (array_keys($this->prop['sub_fields']) as $xf) {
13db9e 981                 if (!empty($newentry[$xf])) {
A 982                     $xfields[$xf] = $newentry[$xf];
983                     unset($newentry[$xf]);
984                 }
985             }
986         }
a97937 987
203323 988         if (!$this->ldap->add($dn, $newentry)) {
a97937 989             $this->set_error(self::ERROR_SAVING, 'errorsaving');
T 990             return false;
13db9e 991         }
4f9c83 992
13db9e 993         foreach ($xfields as $xidx => $xf) {
4500b2 994             $xdn = $xidx.'='.rcube_ldap_generic::quote_string($xf).','.$dn;
13db9e 995             $xf = array(
A 996                 $xidx => $xf,
997                 'objectClass' => (array) $this->prop['sub_fields'][$xidx],
998             );
999
203323 1000             $this->ldap->add($xdn, $xf);
13db9e 1001         }
4f9c83 1002
c3ba0e 1003         $dn = self::dn_encode($dn);
A 1004
a97937 1005         // add new contact to the selected group
5d692b 1006         if ($this->group_id)
c3ba0e 1007             $this->add_to_group($this->group_id, $dn);
4f9c83 1008
c3ba0e 1009         return $dn;
e83f03 1010     }
A 1011
4f9c83 1012
a97937 1013     /**
cc90ed 1014      * Update a specific contact record
A 1015      *
1016      * @param mixed Record identifier
1017      * @param array Hash array with save data
1018      *
1019      * @return boolean True on success, False on error
1020      */
a97937 1021     function update($id, $save_cols)
f11541 1022     {
a97937 1023         $record = $this->get_record($id, true);
4aaecb 1024
13db9e 1025         $newdata     = array();
a97937 1026         $replacedata = array();
13db9e 1027         $deletedata  = array();
A 1028         $subdata     = array();
1029         $subdeldata  = array();
1030         $subnewdata  = array();
c3ba0e 1031
d6aafd 1032         $ldap_data = $this->_map_data($save_cols);
13db9e 1033         $old_data  = $record['_raw_attrib'];
1803f8 1034
21a0d9 1035         // special handling of photo col
A 1036         if ($photo_fld = $this->fieldmap['photo']) {
1037             // undefined means keep old photo
1038             if (!array_key_exists('photo', $save_cols)) {
1039                 $ldap_data[$photo_fld] = $record['photo'];
1040             }
1041         }
1042
3725cf 1043         foreach ($this->fieldmap as $fld) {
a97937 1044             if ($fld) {
13db9e 1045                 $val = $ldap_data[$fld];
A 1046                 $old = $old_data[$fld];
a97937 1047                 // remove empty array values
T 1048                 if (is_array($val))
1049                     $val = array_filter($val);
13db9e 1050                 // $this->_map_data() result and _raw_attrib use different format
A 1051                 // make sure comparing array with one element with a string works as expected
1052                 if (is_array($old) && count($old) == 1 && !is_array($val)) {
1053                     $old = array_pop($old);
21a0d9 1054                 }
A 1055                 if (is_array($val) && count($val) == 1 && !is_array($old)) {
1056                     $val = array_pop($val);
13db9e 1057                 }
A 1058                 // Subentries must be handled separately
1059                 if (!empty($this->prop['sub_fields']) && isset($this->prop['sub_fields'][$fld])) {
1060                     if ($old != $val) {
1061                         if ($old !== null) {
1062                             $subdeldata[$fld] = $old;
1063                         }
1064                         if ($val) {
1065                             $subnewdata[$fld] = $val;
1066                         }
1067                     }
1068                     else if ($old !== null) {
1069                         $subdata[$fld] = $old;
1070                     }
1071                     continue;
1072                 }
21a0d9 1073
a97937 1074                 // The field does exist compare it to the ldap record.
13db9e 1075                 if ($old != $val) {
a97937 1076                     // Changed, but find out how.
13db9e 1077                     if ($old === null) {
a97937 1078                         // Field was not set prior, need to add it.
T 1079                         $newdata[$fld] = $val;
1803f8 1080                     }
T 1081                     else if ($val == '') {
a97937 1082                         // Field supplied is empty, verify that it is not required.
T 1083                         if (!in_array($fld, $this->prop['required_fields'])) {
afaccf 1084                             // ...It is not, safe to clear.
AM 1085                             // #1488420: Workaround "ldap_mod_del(): Modify: Inappropriate matching in..."
1086                             // jpegPhoto attribute require an array() here. It looks to me that it works for other attribs too
1087                             $deletedata[$fld] = array();
1088                             //$deletedata[$fld] = $old_data[$fld];
1803f8 1089                         }
13db9e 1090                     }
a97937 1091                     else {
T 1092                         // The data was modified, save it out.
1093                         $replacedata[$fld] = $val;
1803f8 1094                     }
a97937 1095                 } // end if
T 1096             } // end if
1097         } // end foreach
a605b2 1098
T 1099         // console($old_data, $ldap_data, '----', $newdata, $replacedata, $deletedata, '----', $subdata, $subnewdata, $subdeldata);
1100
c3ba0e 1101         $dn = self::dn_decode($id);
a97937 1102
T 1103         // Update the entry as required.
1104         if (!empty($deletedata)) {
1105             // Delete the fields.
203323 1106             if (!$this->ldap->mod_del($dn, $deletedata)) {
a97937 1107                 $this->set_error(self::ERROR_SAVING, 'errorsaving');
T 1108                 return false;
1109             }
1110         } // end if
1111
1112         if (!empty($replacedata)) {
1113             // Handle RDN change
1114             if ($replacedata[$this->prop['LDAP_rdn']]) {
1115                 $newdn = $this->prop['LDAP_rdn'].'='
4500b2 1116                     .rcube_ldap_generic::quote_string($replacedata[$this->prop['LDAP_rdn']], true)
d1e08f 1117                     .','.$this->base_dn;
a97937 1118                 if ($dn != $newdn) {
T 1119                     $newrdn = $this->prop['LDAP_rdn'].'='
4500b2 1120                     .rcube_ldap_generic::quote_string($replacedata[$this->prop['LDAP_rdn']], true);
a97937 1121                     unset($replacedata[$this->prop['LDAP_rdn']]);
T 1122                 }
1123             }
1124             // Replace the fields.
1125             if (!empty($replacedata)) {
203323 1126                 if (!$this->ldap->mod_replace($dn, $replacedata)) {
13db9e 1127                     $this->set_error(self::ERROR_SAVING, 'errorsaving');
a97937 1128                     return false;
T 1129                 }
13db9e 1130             }
a97937 1131         } // end if
13db9e 1132
A 1133         // RDN change, we need to remove all sub-entries
1134         if (!empty($newrdn)) {
1135             $subdeldata = array_merge($subdeldata, $subdata);
1136             $subnewdata = array_merge($subnewdata, $subdata);
1137         }
1138
1139         // remove sub-entries
1140         if (!empty($subdeldata)) {
1141             foreach ($subdeldata as $fld => $val) {
4500b2 1142                 $subdn = $fld.'='.rcube_ldap_generic::quote_string($val).','.$dn;
203323 1143                 if (!$this->ldap->delete($subdn)) {
13db9e 1144                     return false;
A 1145                 }
1146             }
1147         }
a97937 1148
T 1149         if (!empty($newdata)) {
1150             // Add the fields.
203323 1151             if (!$this->ldap->mod_add($dn, $newdata)) {
a97937 1152                 $this->set_error(self::ERROR_SAVING, 'errorsaving');
T 1153                 return false;
1154             }
1155         } // end if
1156
1157         // Handle RDN change
1158         if (!empty($newrdn)) {
203323 1159             if (!$this->ldap->rename($dn, $newrdn, null, true)) {
13db9e 1160                 $this->set_error(self::ERROR_SAVING, 'errorsaving');
a97937 1161                 return false;
T 1162             }
1163
c3ba0e 1164             $dn    = self::dn_encode($dn);
A 1165             $newdn = self::dn_encode($newdn);
1166
a97937 1167             // change the group membership of the contact
13db9e 1168             if ($this->groups) {
c3ba0e 1169                 $group_ids = $this->get_record_groups($dn);
86552f 1170                 foreach ($group_ids as $group_id => $group_prop)
a97937 1171                 {
c3ba0e 1172                     $this->remove_from_group($group_id, $dn);
A 1173                     $this->add_to_group($group_id, $newdn);
a97937 1174                 }
T 1175             }
c3ba0e 1176
13db9e 1177             $dn = self::dn_decode($newdn);
a97937 1178         }
T 1179
13db9e 1180         // add sub-entries
A 1181         if (!empty($subnewdata)) {
1182             foreach ($subnewdata as $fld => $val) {
4500b2 1183                 $subdn = $fld.'='.rcube_ldap_generic::quote_string($val).','.$dn;
13db9e 1184                 $xf = array(
A 1185                     $fld => $val,
1186                     'objectClass' => (array) $this->prop['sub_fields'][$fld],
1187                 );
203323 1188                 $this->ldap->add($subdn, $xf);
13db9e 1189             }
A 1190         }
1191
1192         return $newdn ? $newdn : true;
f11541 1193     }
a97937 1194
T 1195
1196     /**
cc90ed 1197      * Mark one or more contact records as deleted
A 1198      *
63fda8 1199      * @param array   Record identifiers
A 1200      * @param boolean Remove record(s) irreversible (unsupported)
cc90ed 1201      *
A 1202      * @return boolean True on success, False on error
1203      */
63fda8 1204     function delete($ids, $force=true)
f11541 1205     {
a97937 1206         if (!is_array($ids)) {
T 1207             // Not an array, break apart the encoded DNs.
0f1fae 1208             $ids = explode(',', $ids);
a97937 1209         } // end if
T 1210
0f1fae 1211         foreach ($ids as $id) {
c3ba0e 1212             $dn = self::dn_decode($id);
13db9e 1213
A 1214             // Need to delete all sub-entries first
1215             if ($this->sub_filter) {
203323 1216                 if ($entries = $this->ldap->list_entries($dn, $this->sub_filter)) {
13db9e 1217                     foreach ($entries as $entry) {
203323 1218                         if (!$this->ldap->delete($entry['dn'])) {
13db9e 1219                             $this->set_error(self::ERROR_SAVING, 'errorsaving');
A 1220                             return false;
1221                         }
1222                     }
1223                 }
1224             }
1225
a97937 1226             // Delete the record.
203323 1227             if (!$this->ldap->delete($dn)) {
a97937 1228                 $this->set_error(self::ERROR_SAVING, 'errorsaving');
T 1229                 return false;
13db9e 1230             }
a97937 1231
T 1232             // remove contact from all groups where he was member
c3ba0e 1233             if ($this->groups) {
A 1234                 $dn = self::dn_encode($dn);
1235                 $group_ids = $this->get_record_groups($dn);
86552f 1236                 foreach ($group_ids as $group_id => $group_prop) {
c3ba0e 1237                     $this->remove_from_group($group_id, $dn);
a97937 1238                 }
T 1239             }
1240         } // end foreach
1241
0f1fae 1242         return count($ids);
d6eb7c 1243     }
A 1244
1245
1246     /**
1247      * Remove all contact records
1248      */
1249     function delete_all()
1250     {
203323 1251         // searching for contact entries
TB 1252         $dn_list = $this->ldap->list_entries($this->base_dn, $this->prop['filter'] ? $this->prop['filter'] : '(objectclass=*)');
d6eb7c 1253
A 1254         if (!empty($dn_list)) {
1255             foreach ($dn_list as $idx => $entry) {
1256                 $dn_list[$idx] = self::dn_encode($entry['dn']);
1257             }
1258             $this->delete($dn_list);
1259         }
f11541 1260     }
4368a0 1261
3f250a 1262     /**
TB 1263      * Generate missing attributes as configured
1264      *
1265      * @param array LDAP record attributes
1266      */
1267     protected function add_autovalues(&$attrs)
1268     {
4741d1 1269         if (empty($this->prop['autovalues'])) {
AM 1270             return;
1271         }
1272
3f250a 1273         $attrvals = array();
TB 1274         foreach ($attrs as $k => $v) {
1275             $attrvals['{'.$k.'}'] = is_array($v) ? $v[0] : $v;
1276         }
1277
1278         foreach ((array)$this->prop['autovalues'] as $lf => $templ) {
1279             if (empty($attrs[$lf])) {
c2e1ab 1280                 if (strpos($templ, '(') !== false) {
TB 1281                     // replace {attr} placeholders with (escaped!) attribute values to be safely eval'd
1282                     $code = preg_replace('/\{\w+\}/', '', strtr($templ, array_map('addslashes', $attrvals)));
4741d1 1283                     $fn   = create_function('', "return ($code);");
AM 1284                     if (!$fn) {
1285                         rcube::raise_error(array(
1286                             'code' => 505, 'type' => 'php',
1287                             'file' => __FILE__, 'line' => __LINE__,
1288                             'message' => "Expression parse error on: ($code)"), true, false);
1289                         continue;
1290                     }
1291
1292                     $attrs[$lf] = $fn();
c2e1ab 1293                 }
TB 1294                 else {
1295                     // replace {attr} placeholders with concrete attribute values
1296                     $attrs[$lf] = preg_replace('/\{\w+\}/', '', strtr($templ, $attrvals));
1297                 }
3f250a 1298             }
TB 1299         }
1300     }
4368a0 1301
a97937 1302
T 1303     /**
cc90ed 1304      * Converts LDAP entry into an array
A 1305      */
a97937 1306     private function _ldap2result($rec)
T 1307     {
c5a5f9 1308         $out = array('_type' => 'person');
ec2185 1309         $fieldmap = $this->fieldmap;
a97937 1310
T 1311         if ($rec['dn'])
c3ba0e 1312             $out[$this->primary_key] = self::dn_encode($rec['dn']);
a97937 1313
c5a5f9 1314         // determine record type
f924f5 1315         if (self::is_group_entry($rec)) {
c5a5f9 1316             $out['_type'] = 'group';
TB 1317             $out['readonly'] = true;
49cb69 1318             $fieldmap['name'] = $this->prop['groups']['name_attr'];
c5a5f9 1319         }
a97937 1320
ec2185 1321         foreach ($fieldmap as $rf => $lf)
a97937 1322         {
T 1323             for ($i=0; $i < $rec[$lf]['count']; $i++) {
1324                 if (!($value = $rec[$lf][$i]))
1325                     continue;
1803f8 1326
ad8c9d 1327                 list($col, $subtype) = explode(':', $rf);
1803f8 1328                 $out['_raw_attrib'][$lf][$i] = $value;
T 1329
1078a6 1330                 if ($col == 'email' && $this->mail_domain && !strpos($value, '@'))
a97937 1331                     $out[$rf][] = sprintf('%s@%s', $value, $this->mail_domain);
ad8c9d 1332                 else if (in_array($col, array('street','zipcode','locality','country','region')))
T 1333                     $out['address'.($subtype?':':'').$subtype][$i][$col] = $value;
a605b2 1334                 else if ($col == 'address' && strpos($value, '$') !== false)  // address data is represented as string separated with $
T 1335                     list($out[$rf][$i]['street'], $out[$rf][$i]['locality'], $out[$rf][$i]['zipcode'], $out[$rf][$i]['country']) = explode('$', $value);
a97937 1336                 else if ($rec[$lf]['count'] > 1)
T 1337                     $out[$rf][] = $value;
1338                 else
1339                     $out[$rf] = $value;
1340             }
b1f084 1341
A 1342             // Make sure name fields aren't arrays (#1488108)
1343             if (is_array($out[$rf]) && in_array($rf, array('name', 'surname', 'firstname', 'middlename', 'nickname'))) {
1803f8 1344                 $out[$rf] = $out['_raw_attrib'][$lf] = $out[$rf][0];
b1f084 1345             }
a97937 1346         }
T 1347
1348         return $out;
1349     }
1350
1351
1352     /**
1078a6 1353      * Return LDAP attribute(s) for the given field
cc90ed 1354      */
a97937 1355     private function _map_field($field)
T 1356     {
1078a6 1357         return (array)$this->coltypes[$field]['attributes'];
a97937 1358     }
T 1359
1360
1361     /**
39cafa 1362      * Convert a record data set into LDAP field attributes
T 1363      */
1364     private function _map_data($save_cols)
1365     {
d6aafd 1366         // flatten composite fields first
T 1367         foreach ($this->coltypes as $col => $colprop) {
1368             if (is_array($colprop['childs']) && ($values = $this->get_col_values($col, $save_cols, false))) {
1369                 foreach ($values as $subtype => $childs) {
1370                     $subtype = $subtype ? ':'.$subtype : '';
1371                     foreach ($childs as $i => $child_values) {
1372                         foreach ((array)$child_values as $childcol => $value) {
1373                             $save_cols[$childcol.$subtype][$i] = $value;
1374                         }
1375                     }
1376                 }
1377             }
a605b2 1378
T 1379             // if addresses are to be saved as serialized string, do so
1380             if (is_array($colprop['serialized'])) {
1381                foreach ($colprop['serialized'] as $subtype => $delim) {
1382                   $key = $col.':'.$subtype;
49cb69 1383                   foreach ((array)$save_cols[$key] as $i => $val) {
TB 1384                      $values = array($val['street'], $val['locality'], $val['zipcode'], $val['country']);
1385                      $save_cols[$key][$i] = count(array_filter($values)) ? join($delim, $values) : null;
1386                  }
a605b2 1387                }
T 1388             }
d6aafd 1389         }
T 1390
39cafa 1391         $ldap_data = array();
1078a6 1392         foreach ($this->fieldmap as $rf => $fld) {
TB 1393             $val = $save_cols[$rf];
1394
1395             // check for value in base field (eg.g email instead of email:foo)
1396             list($col, $subtype) = explode(':', $rf);
1397             if (!$val && !empty($save_cols[$col])) {
1398                 $val = $save_cols[$col];
1399                 unset($save_cols[$col]);  // only use this value once
1400             }
1401             else if (!$val && !$subtype) { // extract values from subtype cols
1402                 $val = $this->get_col_values($col, $save_cols, true);
1403             }
1404
39cafa 1405             if (is_array($val))
T 1406                 $val = array_filter($val);  // remove empty entries
1407             if ($fld && $val) {
1408                 // The field does exist, add it to the entry.
1409                 $ldap_data[$fld] = $val;
1410             }
1411         }
13db9e 1412
39cafa 1413         return $ldap_data;
T 1414     }
1415
1416
1417     /**
cc90ed 1418      * Returns unified attribute name (resolving aliases)
A 1419      */
a605b2 1420     private static function _attr_name($namev)
a97937 1421     {
T 1422         // list of known attribute aliases
a605b2 1423         static $aliases = array(
a97937 1424             'gn' => 'givenname',
T 1425             'rfc822mailbox' => 'email',
1426             'userid' => 'uid',
1427             'emailaddress' => 'email',
1428             'pkcs9email' => 'email',
1429         );
a605b2 1430
T 1431         list($name, $limit) = explode(':', $namev, 2);
1432         $suffix = $limit ? ':'.$limit : '';
1433
1434         return (isset($aliases[$name]) ? $aliases[$name] : $name) . $suffix;
a97937 1435     }
T 1436
f924f5 1437     /**
TB 1438      * Determines whether the given LDAP entry is a group record
1439      */
1440     private static function is_group_entry($entry)
1441     {
1442         return array_intersect(
1443             array('group', 'groupofnames', 'kolabgroupofnames', 'groupofuniquenames','kolabgroupofuniquenames','groupofurls'),
1444             array_map('strtolower', (array)$entry['objectclass'])
1445         );
1446     }
a97937 1447
T 1448     /**
cc90ed 1449      * Prints debug info to the log
A 1450      */
a97937 1451     private function _debug($str)
T 1452     {
0c2596 1453         if ($this->debug) {
be98df 1454             rcube::write_log('ldap', $str);
0c2596 1455         }
a97937 1456     }
T 1457
1458
1459     /**
06744d 1460      * Activate/deactivate debug mode
T 1461      *
1462      * @param boolean $dbg True if LDAP commands should be logged
1463      * @access public
1464      */
1465     function set_debug($dbg = true)
1466     {
1467         $this->debug = $dbg;
1468     }
1469
1470
1471     /**
6039aa 1472      * Setter for the current group
T 1473      */
1474     function set_group($group_id)
1475     {
ec2185 1476         if ($group_id) {
6039aa 1477             $this->group_id = $group_id;
ec2185 1478             $this->group_data = $this->get_group_entry($group_id);
6039aa 1479         }
ec2185 1480         else {
a97937 1481             $this->group_id = 0;
a31482 1482             $this->group_data = null;
8fb04b 1483         }
6039aa 1484     }
T 1485
1486     /**
1487      * List all active contact groups of this source
1488      *
1489      * @param string  Optional search string to match group name
ec4331 1490      * @param int     Matching mode:
AM 1491      *                0 - partial (*abc*),
1492      *                1 - strict (=),
1493      *                2 - prefix (abc*)
1494      *
6039aa 1495      * @return array  Indexed list of contact groups, each a hash array
T 1496      */
ec4331 1497     function list_groups($search = null, $mode = 0)
6039aa 1498     {
a97937 1499         if (!$this->groups)
T 1500             return array();
6039aa 1501
a31482 1502         // use cached list for searching
T 1503         $this->cache->expunge();
1504         if (!$search || ($group_cache = $this->cache->get('groups')) === null)
1505             $group_cache = $this->_fetch_groups();
1506
1507         $groups = array();
1508         if ($search) {
1509             foreach ($group_cache as $group) {
ec4331 1510                 if ($this->compare_search_value('name', $group['name'], $search, $mode)) {
a31482 1511                     $groups[] = $group;
ec4331 1512                 }
a31482 1513             }
T 1514         }
1515         else
1516             $groups = $group_cache;
1517
1518         return array_values($groups);
1519     }
1520
1521     /**
1522      * Fetch groups from server
1523      */
fb6cc8 1524     private function _fetch_groups($vlv_page = 0)
a31482 1525     {
ec2185 1526         // special case: list groups from 'group_filters' config
TB 1527         if (!empty($this->prop['group_filters'])) {
1528             $groups = array();
1529
1530             // list regular groups configuration as special filter
1531             if (!empty($this->prop['groups']['filter'])) {
1532                 $id = '__groups__';
86552f 1533                 $groups[$id] = array('ID' => $id, 'name' => rcube_label('groups'), 'virtual' => true) + $this->prop['groups'];
ec2185 1534             }
TB 1535
1536             foreach ($this->prop['group_filters'] as $id => $prop) {
203323 1537                 $groups[$id] = $prop + array('ID' => $id, 'name' => ucfirst($id), 'virtual' => true, 'base_dn' => $this->base_dn);
ec2185 1538             }
TB 1539
1540             return $groups;
1541         }
1542
d1e08f 1543         $base_dn = $this->groups_base_dn;
T 1544         $filter = $this->prop['groups']['filter'];
448f81 1545         $name_attr = $this->prop['groups']['name_attr'];
a31482 1546         $email_attr = $this->prop['groups']['email_attr'] ? $this->prop['groups']['email_attr'] : 'mail';
fb6cc8 1547         $sort_attrs = $this->prop['groups']['sort'] ? (array)$this->prop['groups']['sort'] : array($name_attr);
T 1548         $sort_attr = $sort_attrs[0];
6039aa 1549
203323 1550         $ldap = $this->ldap;
755189 1551
fb6cc8 1552         // use vlv to list groups
T 1553         if ($this->prop['groups']['vlv']) {
1554             $page_size = 200;
1555             if (!$this->prop['groups']['sort'])
1556                 $this->prop['groups']['sort'] = $sort_attrs;
203323 1557
TB 1558             $ldap = clone $this->ldap;
1559             $ldap->set_config($this->prop['groups']);
1560             $ldap->set_vlv_page($vlv_page+1, $page_size);
fb6cc8 1561         }
T 1562
203323 1563         $attrs = array_unique(array('dn', 'objectClass', $name_attr, $email_attr, $sort_attr));
TB 1564         $ldap_data = $ldap->search($base_dn, $filter, $this->prop['groups']['scope'], $attrs, $this->prop['groups']);
1565         if ($ldap_data === false) {
6039aa 1566             return array();
T 1567         }
755189 1568
6039aa 1569         $groups = array();
T 1570         $group_sortnames = array();
203323 1571         $group_count = $ldap_data->count();
TB 1572         foreach ($ldap_data as $entry) {
1573             if (!$entry['dn'])  // DN is mandatory
1574                 $entry['dn'] = $ldap_data->get_dn();
1575
1576             $group_name = is_array($entry[$name_attr]) ? $entry[$name_attr][0] : $entry[$name_attr];
1577             $group_id = self::dn_encode($entry['dn']);
a31482 1578             $groups[$group_id]['ID'] = $group_id;
203323 1579             $groups[$group_id]['dn'] = $entry['dn'];
a31482 1580             $groups[$group_id]['name'] = $group_name;
203323 1581             $groups[$group_id]['member_attr'] = $this->get_group_member_attr($entry['objectclass']);
a31482 1582
T 1583             // list email attributes of a group
203323 1584             for ($j=0; $entry[$email_attr] && $j < $entry[$email_attr]['count']; $j++) {
TB 1585                 if (strpos($entry[$email_attr][$j], '@') > 0)
1586                     $groups[$group_id]['email'][] = $entry[$email_attr][$j];
a31482 1587             }
T 1588
203323 1589             $group_sortnames[] = mb_strtolower($entry[$sort_attr][0]);
6039aa 1590         }
fb6cc8 1591
T 1592         // recursive call can exit here
1593         if ($vlv_page > 0)
1594             return $groups;
1595
1596         // call recursively until we have fetched all groups
203323 1597         while ($this->prop['groups']['vlv'] && $group_count == $page_size) {
fb6cc8 1598             $next_page = $this->_fetch_groups(++$vlv_page);
T 1599             $groups = array_merge($groups, $next_page);
1600             $group_count = count($next_page);
1601         }
1602
1603         // when using VLV the list of groups is already sorted
1604         if (!$this->prop['groups']['vlv'])
1605             array_multisort($group_sortnames, SORT_ASC, SORT_STRING, $groups);
8fb04b 1606
T 1607         // cache this
1608         $this->cache->set('groups', $groups);
a97937 1609
6039aa 1610         return $groups;
a31482 1611     }
T 1612
1613     /**
ec2185 1614      * Fetch a group entry from LDAP and save in local cache
TB 1615      */
1616     private function get_group_entry($group_id)
1617     {
1618         if (($group_cache = $this->cache->get('groups')) === null)
1619             $group_cache = $this->_fetch_groups();
1620
1621         // add group record to cache if it isn't yet there
1622         if (!isset($group_cache[$group_id])) {
1623             $name_attr = $this->prop['groups']['name_attr'];
1624             $dn = self::dn_decode($group_id);
1625
1626             $this->_debug("C: Read Group [dn: $dn]");
203323 1627             if ($list = $this->ldap->read_entries($dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL',$name_attr,$this->fieldmap['email']))) {
ec2185 1628                 $entry = $list[0];
TB 1629                 $group_name = is_array($entry[$name_attr]) ? $entry[$name_attr][0] : $entry[$name_attr];
1630                 $group_cache[$group_id]['ID'] = $group_id;
1631                 $group_cache[$group_id]['dn'] = $dn;
1632                 $group_cache[$group_id]['name'] = $group_name;
1633                 $group_cache[$group_id]['member_attr'] = $this->get_group_member_attr($entry['objectclass']);
1634             }
1635             else {
1636                 $group_cache[$group_id] = false;
1637             }
1638
1639             $this->cache->set('groups', $group_cache);
1640         }
1641
1642         return $group_cache[$group_id];
a31482 1643     }
T 1644
1645     /**
1646      * Get group properties such as name and email address(es)
1647      *
1648      * @param string Group identifier
1649      * @return array Group properties as hash array
1650      */
1651     function get_group($group_id)
1652     {
86552f 1653         $group_data = $this->get_group_entry($group_id);
a31482 1654         unset($group_data['dn'], $group_data['member_attr']);
T 1655
1656         return $group_data;
6039aa 1657     }
T 1658
1659     /**
1660      * Create a contact group with the given name
1661      *
1662      * @param string The group name
1663      * @return mixed False on error, array with record props in success
1664      */
1665     function create_group($group_name)
1666     {
4500b2 1667         $new_dn = 'cn=' . rcube_ldap_generic::quote_string($group_name, true) . ',' . $this->groups_base_dn;
c8a714 1668         $new_gid = self::dn_encode($new_dn);
097dbc 1669         $member_attr = $this->get_group_member_attr();
A 1670         $name_attr = $this->prop['groups']['name_attr'] ? $this->prop['groups']['name_attr'] : 'cn';
6039aa 1671
T 1672         $new_entry = array(
d1e08f 1673             'objectClass' => $this->prop['groups']['object_classes'],
448f81 1674             $name_attr => $group_name,
b4b377 1675             $member_attr => '',
6039aa 1676         );
T 1677
203323 1678         if (!$this->ldap->add($new_dn, $new_entry)) {
6039aa 1679             $this->set_error(self::ERROR_SAVING, 'errorsaving');
T 1680             return false;
1681         }
755189 1682
8fb04b 1683         $this->cache->remove('groups');
755189 1684
6039aa 1685         return array('id' => $new_gid, 'name' => $group_name);
T 1686     }
1687
1688     /**
1689      * Delete the given group and all linked group members
1690      *
1691      * @param string Group identifier
1692      * @return boolean True on success, false if no data was changed
1693      */
1694     function delete_group($group_id)
1695     {
a31482 1696         if (($group_cache = $this->cache->get('groups')) === null)
T 1697             $group_cache = $this->_fetch_groups();
6039aa 1698
c8a714 1699         $del_dn = $group_cache[$group_id]['dn'];
755189 1700
203323 1701         if (!$this->ldap->delete($del_dn)) {
6039aa 1702             $this->set_error(self::ERROR_SAVING, 'errorsaving');
T 1703             return false;
1704         }
755189 1705
c8a714 1706         unset($group_cache[$group_id]);
TB 1707         $this->cache->set('groups', $group_cache);
755189 1708
6039aa 1709         return true;
T 1710     }
1711
1712     /**
1713      * Rename a specific contact group
1714      *
1715      * @param string Group identifier
1716      * @param string New name to set for this group
360bd3 1717      * @param string New group identifier (if changed, otherwise don't set)
6039aa 1718      * @return boolean New name on success, false if no data was changed
T 1719      */
15e944 1720     function rename_group($group_id, $new_name, &$new_gid)
6039aa 1721     {
a31482 1722         if (($group_cache = $this->cache->get('groups')) === null)
T 1723             $group_cache = $this->_fetch_groups();
6039aa 1724
c8a714 1725         $old_dn = $group_cache[$group_id]['dn'];
4500b2 1726         $new_rdn = "cn=" . rcube_ldap_generic::quote_string($new_name, true);
c8a714 1727         $new_gid = self::dn_encode($new_rdn . ',' . $this->groups_base_dn);
6039aa 1728
203323 1729         if (!$this->ldap->rename($old_dn, $new_rdn, null, true)) {
6039aa 1730             $this->set_error(self::ERROR_SAVING, 'errorsaving');
T 1731             return false;
1732         }
755189 1733
8fb04b 1734         $this->cache->remove('groups');
755189 1735
6039aa 1736         return $new_name;
T 1737     }
1738
1739     /**
1740      * Add the given contact records the a certain group
1741      *
40d419 1742      * @param string       Group identifier
AM 1743      * @param array|string List of contact identifiers to be added
1744      *
1745      * @return int Number of contacts added
6039aa 1746      */
T 1747     function add_to_group($group_id, $contact_ids)
1748     {
a31482 1749         if (($group_cache = $this->cache->get('groups')) === null)
T 1750             $group_cache = $this->_fetch_groups();
6039aa 1751
5d692b 1752         if (!is_array($contact_ids))
T 1753             $contact_ids = explode(',', $contact_ids);
1754
8fb04b 1755         $member_attr = $group_cache[$group_id]['member_attr'];
c8a714 1756         $group_dn    = $group_cache[$group_id]['dn'];
40d419 1757         $new_attrs   = array();
6039aa 1758
5d692b 1759         foreach ($contact_ids as $id)
f763fb 1760             $new_attrs[$member_attr][] = self::dn_decode($id);
6039aa 1761
203323 1762         if (!$this->ldap->mod_add($group_dn, $new_attrs)) {
6039aa 1763             $this->set_error(self::ERROR_SAVING, 'errorsaving');
T 1764             return 0;
1765         }
755189 1766
8fb04b 1767         $this->cache->remove('groups');
755189 1768
40d419 1769         return count($new_attrs[$member_attr]);
6039aa 1770     }
T 1771
1772     /**
1773      * Remove the given contact records from a certain group
1774      *
40d419 1775      * @param string       Group identifier
AM 1776      * @param array|string List of contact identifiers to be removed
1777      *
1778      * @return int Number of deleted group members
6039aa 1779      */
T 1780     function remove_from_group($group_id, $contact_ids)
1781     {
a31482 1782         if (($group_cache = $this->cache->get('groups')) === null)
T 1783             $group_cache = $this->_fetch_groups();
6039aa 1784
40d419 1785         if (!is_array($contact_ids))
AM 1786             $contact_ids = explode(',', $contact_ids);
1787
8fb04b 1788         $member_attr = $group_cache[$group_id]['member_attr'];
c8a714 1789         $group_dn    = $group_cache[$group_id]['dn'];
6039aa 1790         $del_attrs = array();
T 1791
40d419 1792         foreach ($contact_ids as $id)
f763fb 1793             $del_attrs[$member_attr][] = self::dn_decode($id);
6039aa 1794
203323 1795         if (!$this->ldap->mod_del($group_dn, $del_attrs)) {
6039aa 1796             $this->set_error(self::ERROR_SAVING, 'errorsaving');
T 1797             return 0;
1798         }
755189 1799
8fb04b 1800         $this->cache->remove('groups');
755189 1801
40d419 1802         return count($del_attrs[$member_attr]);
6039aa 1803     }
T 1804
1805     /**
1806      * Get group assignments of a specific contact record
1807      *
1808      * @param mixed Record identifier
1809      *
1810      * @return array List of assigned groups as ID=>Name pairs
1811      * @since 0.5-beta
1812      */
1813     function get_record_groups($contact_id)
1814     {
a97937 1815         if (!$this->groups)
6039aa 1816             return array();
T 1817
f763fb 1818         $base_dn     = $this->groups_base_dn;
A 1819         $contact_dn  = self::dn_decode($contact_id);
097dbc 1820         $name_attr   = $this->prop['groups']['name_attr'] ? $this->prop['groups']['name_attr'] : 'cn';
A 1821         $member_attr = $this->get_group_member_attr();
8fb04b 1822         $add_filter  = '';
T 1823         if ($member_attr != 'member' && $member_attr != 'uniqueMember')
1824             $add_filter = "($member_attr=$contact_dn)";
1825         $filter = strtr("(|(member=$contact_dn)(uniqueMember=$contact_dn)$add_filter)", array('\\' => '\\\\'));
6039aa 1826
203323 1827         $ldap_data = $this->ldap->search($base_dn, $filter, 'sub', array('dn', $name_attr));
TB 1828         if ($res === false) {
6039aa 1829             return array();
T 1830         }
1831
1832         $groups = array();
203323 1833         foreach ($ldap_data as $entry) {
TB 1834             if (!$entry['dn'])
1835                 $entry['dn'] = $ldap_data->get_dn();
1836             $group_name = $entry[$name_attr][0];
1837             $group_id = self::dn_encode($entry['dn']);
1838             $groups[$group_id] = array('ID' => $group_id, 'name' => $group_name, 'dn' => $entry['dn']);
6039aa 1839         }
T 1840         return $groups;
1841     }
69ea3a 1842
097dbc 1843     /**
A 1844      * Detects group member attribute name
1845      */
1846     private function get_group_member_attr($object_classes = array())
1847     {
1848         if (empty($object_classes)) {
1849             $object_classes = $this->prop['groups']['object_classes'];
1850         }
1851         if (!empty($object_classes)) {
1852             foreach ((array)$object_classes as $oc) {
1853                 switch (strtolower($oc)) {
1854                     case 'group':
1855                     case 'groupofnames':
1856                     case 'kolabgroupofnames':
1857                         $member_attr = 'member';
1858                         break;
1859
1860                     case 'groupofuniquenames':
1861                     case 'kolabgroupofuniquenames':
1862                         $member_attr = 'uniqueMember';
1863                         break;
1864                 }
1865             }
1866         }
1867
1868         if (!empty($member_attr)) {
1869             return $member_attr;
1870         }
1871
1872         if (!empty($this->prop['groups']['member_attr'])) {
1873             return $this->prop['groups']['member_attr'];
1874         }
1875
1876         return 'member';
1877     }
1878
69ea3a 1879
T 1880     /**
c3ba0e 1881      * HTML-safe DN string encoding
A 1882      *
1883      * @param string $str DN string
1884      *
1885      * @return string Encoded HTML identifier string
1886      */
1887     static function dn_encode($str)
1888     {
1889         // @TODO: to make output string shorter we could probably
1890         //        remove dc=* items from it
1891         return rtrim(strtr(base64_encode($str), '+/', '-_'), '=');
1892     }
1893
1894     /**
1895      * Decodes DN string encoded with _dn_encode()
1896      *
1897      * @param string $str Encoded HTML identifier string
1898      *
1899      * @return string DN string
1900      */
1901     static function dn_decode($str)
1902     {
1903         $str = str_pad(strtr($str, '-_', '+/'), strlen($str) % 4, '=', STR_PAD_RIGHT);
1904         return base64_decode($str);
13db9e 1905     }
A 1906
f11541 1907 }