alecpl
2011-09-13 c3ba0e54afa9088783eaafa94b13ac1eae03916c
commit | author | age
d1d2c4 1 <?php
S 2 /*
3  +-----------------------------------------------------------------------+
47124c 4  | program/include/rcube_ldap.php                                        |
d1d2c4 5  |                                                                       |
e019f2 6  | This file is part of the Roundcube Webmail client                     |
6039aa 7  | Copyright (C) 2006-2011, The Roundcube Dev Team                       |
c3ba0e 8  | Copyright (C) 2011, Kolab Systems AG                                  |
d1d2c4 9  | Licensed under the GNU GPL                                            |
S 10  |                                                                       |
11  | PURPOSE:                                                              |
f11541 12  |   Interface to an LDAP address directory                              |
d1d2c4 13  |                                                                       |
S 14  +-----------------------------------------------------------------------+
f11541 15  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
6039aa 16  |         Andreas Dick <andudi (at) gmx (dot) ch>                       |
c3ba0e 17  |         Aleksander Machniak <machniak@kolabsys.com>                   |
d1d2c4 18  +-----------------------------------------------------------------------+
S 19
20  $Id$
21
22 */
23
6d969b 24
T 25 /**
26  * Model class to access an LDAP address directory
27  *
28  * @package Addressbook
29  */
cc97ea 30 class rcube_ldap extends rcube_addressbook
f11541 31 {
a97937 32     /** public properties */
T 33     public $primary_key = 'ID';
34     public $groups = false;
35     public $readonly = true;
36     public $ready = false;
37     public $group_id = 0;
38     public $list_page = 1;
39     public $page_size = 10;
40     public $coltypes = array();
1148c6 41
a97937 42     /** private properties */
T 43     protected $conn;
44     protected $prop = array();
45     protected $fieldmap = array();
1148c6 46
a97937 47     protected $filter = '';
T 48     protected $result = null;
49     protected $ldap_result = null;
50     protected $sort_col = '';
51     protected $mail_domain = '';
52     protected $debug = false;
6039aa 53
d1e08f 54     private $base_dn = '';
T 55     private $groups_base_dn = '';
a97937 56     private $group_cache = array();
T 57     private $group_members = array();
1148c6 58
69ea3a 59     private $vlv_active = false;
T 60     private $vlv_count = 0;
61
1148c6 62
a97937 63     /**
T 64     * Object constructor
65     *
66     * @param array     LDAP connection properties
67     * @param boolean     Enables debug mode
68     * @param string     Current user mail domain name
69     * @param integer User-ID
70     */
71     function __construct($p, $debug=false, $mail_domain=NULL)
f11541 72     {
a97937 73         $this->prop = $p;
010274 74
a97937 75         // check if groups are configured
d1e08f 76         if (is_array($p['groups']) and count($p['groups']))
a97937 77             $this->groups = true;
cd6749 78
a97937 79         // fieldmap property is given
T 80         if (is_array($p['fieldmap'])) {
81             foreach ($p['fieldmap'] as $rf => $lf)
82                 $this->fieldmap[$rf] = $this->_attr_name(strtolower($lf));
83         }
84         else {
85             // read deprecated *_field properties to remain backwards compatible
86             foreach ($p as $prop => $value)
87                 if (preg_match('/^(.+)_field$/', $prop, $matches))
88                     $this->fieldmap[$matches[1]] = $this->_attr_name(strtolower($value));
b9f9f1 89         }
4f9c83 90
a97937 91         // use fieldmap to advertise supported coltypes to the application
T 92         foreach ($this->fieldmap as $col => $lf) {
93             list($col, $type) = explode(':', $col);
94             if (!is_array($this->coltypes[$col])) {
95                 $subtypes = $type ? array($type) : null;
96                 $this->coltypes[$col] = array('limit' => 2, 'subtypes' => $subtypes);
1148c6 97             }
a97937 98             elseif ($type) {
T 99                 $this->coltypes[$col]['subtypes'][] = $type;
100                 $this->coltypes[$col]['limit']++;
101             }
102             if ($type && !$this->fieldmap[$col])
103                 $this->fieldmap[$col] = $lf;
1148c6 104         }
f76765 105
a97937 106         if ($this->fieldmap['street'] && $this->fieldmap['locality'])
T 107             $this->coltypes['address'] = array('limit' => 1);
108         else if ($this->coltypes['address'])
109             $this->coltypes['address'] = array('type' => 'textarea', 'childs' => null, 'limit' => 1, 'size' => 40);
4f9c83 110
a97937 111         // make sure 'required_fields' is an array
T 112         if (!is_array($this->prop['required_fields']))
113             $this->prop['required_fields'] = (array) $this->prop['required_fields'];
4f9c83 114
a97937 115         foreach ($this->prop['required_fields'] as $key => $val)
T 116             $this->prop['required_fields'][$key] = $this->_attr_name(strtolower($val));
f11541 117
fd8975 118         $this->sort_col = is_array($p['sort']) ? $p['sort'][0] : $p['sort'];
a97937 119         $this->debug = $debug;
T 120         $this->mail_domain = $mail_domain;
f11541 121
a97937 122         $this->_connect();
736307 123     }
010274 124
736307 125
a97937 126     /**
T 127     * Establish a connection to the LDAP server
128     */
129     private function _connect()
6b603d 130     {
a97937 131         global $RCMAIL;
f11541 132
a97937 133         if (!function_exists('ldap_connect'))
T 134             raise_error(array('code' => 100, 'type' => 'ldap',
56651c 135                 'file' => __FILE__, 'line' => __LINE__,
A 136                 'message' => "No ldap support in this installation of PHP"),
137                 true, true);
f11541 138
a97937 139         if (is_resource($this->conn))
T 140             return true;
f11541 141
a97937 142         if (!is_array($this->prop['hosts']))
T 143             $this->prop['hosts'] = array($this->prop['hosts']);
f11541 144
a97937 145         if (empty($this->prop['ldap_version']))
T 146             $this->prop['ldap_version'] = 3;
f11541 147
a97937 148         foreach ($this->prop['hosts'] as $host)
6039aa 149         {
a97937 150             $host = idn_to_ascii(rcube_parse_host($host));
T 151             $this->_debug("C: Connect [$host".($this->prop['port'] ? ':'.$this->prop['port'] : '')."]");
152
153             if ($lc = @ldap_connect($host, $this->prop['port']))
6039aa 154             {
a97937 155                 if ($this->prop['use_tls']===true)
T 156                     if (!ldap_start_tls($lc))
157                         continue;
158
159                 $this->_debug("S: OK");
160
161                 ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->prop['ldap_version']);
162                 $this->prop['host'] = $host;
163                 $this->conn = $lc;
164                 break;
165             }
166             $this->_debug("S: NOT OK");
167         }
168
169         if (is_resource($this->conn))
170         {
171             $this->ready = true;
172
4d982d 173             $bind_pass = $this->prop['bind_pass'];
A 174             $bind_user = $this->prop['bind_user'];
175             $bind_dn   = $this->prop['bind_dn'];
d476d3 176             $this->base_dn   = $this->prop['base_dn'];
4d982d 177
a97937 178             // User specific access, generate the proper values to use.
T 179             if ($this->prop['user_specific']) {
180                 // No password set, use the session password
4d982d 181                 if (empty($bind_pass)) {
A 182                     $bind_pass = $RCMAIL->decrypt($_SESSION['password']);
a97937 183                 }
T 184
185                 // Get the pieces needed for variable replacement.
94a5a2 186                 if ($fu = $RCMAIL->user->get_username())
T 187                   list($u, $d) = explode('@', $fu);
188                 else
189                   $d = $this->mail_domain;
c3ba0e 190
a97937 191                 $dc = 'dc='.strtr($d, array('.' => ',dc=')); // hierarchal domain string
T 192
193                 $replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u);
194
195                 if ($this->prop['search_base_dn'] && $this->prop['search_filter']) {
196                     // Search for the dn to use to authenticate
197                     $this->prop['search_base_dn'] = strtr($this->prop['search_base_dn'], $replaces);
198                     $this->prop['search_filter'] = strtr($this->prop['search_filter'], $replaces);
199
200                     $this->_debug("S: searching with base {$this->prop['search_base_dn']} for {$this->prop['search_filter']}");
201
202                     $res = ldap_search($this->conn, $this->prop['search_base_dn'], $this->prop['search_filter'], array('uid'));
203                     if ($res && ($entry = ldap_first_entry($this->conn, $res))) {
204                         $bind_dn = ldap_get_dn($this->conn, $entry);
205
206                         $this->_debug("S: search returned dn: $bind_dn");
207
208                         if ($bind_dn) {
209                             $dn = ldap_explode_dn($bind_dn, 1);
210                             $replaces['%dn'] = $dn[0];
211                         }
212                     }
213                 }
214                 // Replace the bind_dn and base_dn variables.
4d982d 215                 $bind_dn   = strtr($bind_dn, $replaces);
d476d3 216                 $this->base_dn   = strtr($this->base_dn, $replaces);
4d982d 217
A 218                 if (empty($bind_user)) {
219                     $bind_user = $u;
220                 }
a97937 221             }
T 222
4d982d 223             if (!empty($bind_pass)) {
A 224                 if (!empty($bind_dn)) {
225                     $this->ready = $this->_bind($bind_dn, $bind_pass);
226                 }
227                 else if (!empty($this->prop['auth_cid'])) {
228                     $this->ready = $this->_sasl_bind($this->prop['auth_cid'], $bind_pass, $bind_user);
229                 }
230                 else {
231                     $this->ready = $this->_sasl_bind($bind_user, $bind_pass);
232                 }
233             }
a97937 234         }
T 235         else
236             raise_error(array('code' => 100, 'type' => 'ldap',
237                 'file' => __FILE__, 'line' => __LINE__,
238                 'message' => "Could not connect to any LDAP server, last tried $host:{$this->prop[port]}"), true);
239
240         // See if the directory is writeable.
241         if ($this->prop['writable']) {
242             $this->readonly = false;
243         } // end if
244     }
245
246
247     /**
4d982d 248      * Bind connection with (SASL-) user and password
A 249      *
250      * @param string $authc Authentication user
251      * @param string $pass  Bind password
252      * @param string $authz Autorization user
253      *
254      * @return boolean True on success, False on error
255      */
256     private function _sasl_bind($authc, $pass, $authz=null)
257     {
258         if (!$this->conn) {
259             return false;
260         }
261
262         if (!function_exists('ldap_sasl_bind')) {
56651c 263             raise_error(array('code' => 100, 'type' => 'ldap',
4d982d 264                 'file' => __FILE__, 'line' => __LINE__,
A 265                 'message' => "Unable to bind: ldap_sasl_bind() not exists"),
56651c 266                 true, true);
4d982d 267         }
A 268
269         if (!empty($authz)) {
270             $authz = 'u:' . $authz;
271         }
272
273         if (!empty($this->prop['auth_method'])) {
274             $method = $this->prop['auth_method'];
275         }
276         else {
277             $method = 'DIGEST-MD5';
278         }
279
280         $this->_debug("C: Bind [mech: $method, authc: $authc, authz: $authz] [pass: $pass]");
281
282         if (ldap_sasl_bind($this->conn, NULL, $pass, $method, NULL, $authc, $authz)) {
283             $this->_debug("S: OK");
284             return true;
285         }
286
287         $this->_debug("S: ".ldap_error($this->conn));
288
289         raise_error(array(
290             'code' => ldap_errno($this->conn), 'type' => 'ldap',
291             'file' => __FILE__, 'line' => __LINE__,
292             'message' => "Bind failed for authcid=$authc ".ldap_error($this->conn)),
293             true);
294
295         return false;
296     }
297
298
299     /**
cc90ed 300      * Bind connection with DN and password
A 301      *
302      * @param string Bind DN
303      * @param string Bind password
304      *
305      * @return boolean True on success, False on error
306      */
a97937 307     private function _bind($dn, $pass)
T 308     {
309         if (!$this->conn) {
310             return false;
311         }
312
313         $this->_debug("C: Bind [dn: $dn] [pass: $pass]");
314
315         if (@ldap_bind($this->conn, $dn, $pass)) {
316             $this->_debug("S: OK");
317             return true;
318         }
319
320         $this->_debug("S: ".ldap_error($this->conn));
321
56651c 322         raise_error(array(
A 323             'code' => ldap_errno($this->conn), 'type' => 'ldap',
324             'file' => __FILE__, 'line' => __LINE__,
325             'message' => "Bind failed for dn=$dn: ".ldap_error($this->conn)),
326             true);
a97937 327
T 328         return false;
329     }
330
331
332     /**
cc90ed 333      * Close connection to LDAP server
A 334      */
a97937 335     function close()
T 336     {
337         if ($this->conn)
338         {
339             $this->_debug("C: Close");
340             ldap_unbind($this->conn);
341             $this->conn = null;
342         }
343     }
344
345
346     /**
cc90ed 347      * Returns address book name
A 348      *
349      * @return string Address book name
350      */
351     function get_name()
352     {
353         return $this->prop['name'];
354     }
355
356
357     /**
358      * Set internal list page
359      *
360      * @param number $page Page number to list
361      */
a97937 362     function set_page($page)
T 363     {
364         $this->list_page = (int)$page;
365     }
366
367
368     /**
cc90ed 369      * Set internal page size
A 370      *
371      * @param number $size Number of messages to display on one page
372      */
a97937 373     function set_pagesize($size)
T 374     {
375         $this->page_size = (int)$size;
376     }
377
378
379     /**
cc90ed 380      * Save a search string for future listings
A 381      *
382      * @param string $filter Filter string
383      */
a97937 384     function set_search_set($filter)
T 385     {
386         $this->filter = $filter;
387     }
388
389
390     /**
cc90ed 391      * Getter for saved search properties
A 392      *
393      * @return mixed Search properties used by this class
394      */
a97937 395     function get_search_set()
T 396     {
397         return $this->filter;
398     }
399
400
401     /**
cc90ed 402      * Reset all saved results and search parameters
A 403      */
a97937 404     function reset()
T 405     {
406         $this->result = null;
407         $this->ldap_result = null;
408         $this->filter = '';
409     }
410
411
412     /**
cc90ed 413      * List the current set of contact records
A 414      *
415      * @param  array  List of cols to show
416      * @param  int    Only return this number of records
417      *
418      * @return array  Indexed list of contact records, each a hash array
419      */
a97937 420     function list_records($cols=null, $subset=0)
T 421     {
422         // add general filter to query
423         if (!empty($this->prop['filter']) && empty($this->filter))
424         {
425             $filter = $this->prop['filter'];
426             $this->set_search_set($filter);
427         }
428
429         // exec LDAP search if no result resource is stored
430         if ($this->conn && !$this->ldap_result)
431             $this->_exec_search();
432
433         // count contacts for this user
434         $this->result = $this->count();
435
436         // we have a search result resource
437         if ($this->ldap_result && $this->result->count > 0)
438         {
c63e57 439             // sorting still on the ldap server
69ea3a 440             if ($this->sort_col && $this->prop['scope'] !== 'base' && !$this->vlv_active)
a97937 441                 ldap_sort($this->conn, $this->ldap_result, $this->sort_col);
T 442
c63e57 443             // start and end of the page
69ea3a 444             $start_row = $this->vlv_active ? 0 : $this->result->first;
T 445             $start_row = $subset < 0 ? $start_row + $this->page_size + $subset : $start_row;
a97937 446             $last_row = $this->result->first + $this->page_size;
T 447             $last_row = $subset != 0 ? $start_row + abs($subset) : $last_row;
448
c63e57 449             // get all entries from the ldap server
a97937 450             $entries = ldap_get_entries($this->conn, $this->ldap_result);
c63e57 451
T 452             // filtering for group members
453             if ($this->groups and $this->group_id)
454             {
455                 $count = 0;
456                 $members = array();
457                 foreach ($entries as $entry)
458                 {
c3ba0e 459                     if ($this->group_members[self::dn_encode($entry['dn'])])
c63e57 460                     {
T 461                         $members[] = $entry;
462                         $count++;
463                     }
464                 }
465                 $entries = $members;
466                 $entries['count'] = $count;
467                 $this->result->count = $count;
468             }
469
470             // filter entries for this page
a97937 471             for ($i = $start_row; $i < min($entries['count'], $last_row); $i++)
T 472                 $this->result->add($this->_ldap2result($entries[$i]));
473         }
474         return $this->result;
475     }
476
477
478     /**
cc90ed 479      * Search contacts
A 480      *
481      * @param mixed   $fields   The field name of array of field names to search in
482      * @param mixed   $value    Search value (or array of values when $fields is array)
483      * @param boolean $strict   True for strict, False for partial (fuzzy) matching
484      * @param boolean $select   True if results are requested, False if count only
485      * @param boolean $nocount  (Not used)
486      * @param array   $required List of fields that cannot be empty
487      *
488      * @return array  Indexed list of contact records and 'count' value
489      */
a97937 490     function search($fields, $value, $strict=false, $select=true, $nocount=false, $required=array())
T 491     {
492         // special treatment for ID-based search
493         if ($fields == 'ID' || $fields == $this->primary_key)
494         {
648674 495             $ids = !is_array($value) ? explode(',', $value) : $value;
a97937 496             $result = new rcube_result_set();
T 497             foreach ($ids as $id)
498             {
499                 if ($rec = $this->get_record($id, true))
500                 {
501                     $result->add($rec);
502                     $result->count++;
503                 }
504             }
505             return $result;
506         }
507
e9a9f2 508         // use AND operator for advanced searches
A 509         $filter = is_array($value) ? '(&' : '(|';
510         $wc     = !$strict && $this->prop['fuzzy_search'] ? '*' : '';
511
3cacf9 512         if ($fields == '*')
3e2637 513         {
T 514             // search_fields are required for fulltext search
3cacf9 515             if (empty($this->prop['search_fields']))
3e2637 516             {
T 517                 $this->set_error(self::ERROR_SEARCH, 'nofulltextsearch');
518                 $this->result = new rcube_result_set();
519                 return $this->result;
520             }
3cacf9 521             if (is_array($this->prop['search_fields']))
A 522             {
e9a9f2 523                 foreach ($this->prop['search_fields'] as $field) {
3cacf9 524                     $filter .= "($field=$wc" . $this->_quote_string($value) . "$wc)";
e9a9f2 525                 }
3cacf9 526             }
a97937 527         }
T 528         else
529         {
e9a9f2 530             foreach ((array)$fields as $idx => $field) {
A 531                 $val = is_array($value) ? $value[$idx] : $value;
532                 if ($f = $this->_map_field($field)) {
533                     $filter .= "($f=$wc" . $this->_quote_string($val) . "$wc)";
534                 }
535             }
a97937 536         }
T 537         $filter .= ')';
538
539         // add required (non empty) fields filter
540         $req_filter = '';
541         foreach ((array)$required as $field)
542             if ($f = $this->_map_field($field))
543                 $req_filter .= "($f=*)";
544
545         if (!empty($req_filter))
546             $filter = '(&' . $req_filter . $filter . ')';
547
548         // avoid double-wildcard if $value is empty
549         $filter = preg_replace('/\*+/', '*', $filter);
550
551         // add general filter to query
552         if (!empty($this->prop['filter']))
553             $filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->prop['filter']) . ')' . $filter . ')';
554
555         // set filter string and execute search
556         $this->set_search_set($filter);
557         $this->_exec_search();
558
559         if ($select)
560             $this->list_records();
561         else
562             $this->result = $this->count();
563
564         return $this->result;
565     }
566
567
568     /**
cc90ed 569      * Count number of available contacts in database
A 570      *
571      * @return object rcube_result_set Resultset with values for 'count' and 'first'
572      */
a97937 573     function count()
T 574     {
575         $count = 0;
576         if ($this->conn && $this->ldap_result) {
69ea3a 577             $count = $this->vlv_active ? $this->vlv_count : ldap_count_entries($this->conn, $this->ldap_result);
a97937 578         } // end if
T 579         elseif ($this->conn) {
580             // We have a connection but no result set, attempt to get one.
581             if (empty($this->filter)) {
582                 // The filter is not set, set it.
583                 $this->filter = $this->prop['filter'];
584             } // end if
69ea3a 585             $this->_exec_search(true);
a97937 586             if ($this->ldap_result) {
T 587                 $count = ldap_count_entries($this->conn, $this->ldap_result);
588             } // end if
589         } // end else
590
591         return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
592     }
593
594
595     /**
cc90ed 596      * Return the last result set
A 597      *
598      * @return object rcube_result_set Current resultset or NULL if nothing selected yet
599      */
a97937 600     function get_result()
T 601     {
602         return $this->result;
603     }
604
605
606     /**
cc90ed 607      * Get a specific contact record
A 608      *
609      * @param mixed   Record identifier
610      * @param boolean Return as associative array
611      *
612      * @return mixed  Hash array or rcube_result_set with all record fields
613      */
a97937 614     function get_record($dn, $assoc=false)
T 615     {
616         $res = null;
617         if ($this->conn && $dn)
618         {
c3ba0e 619             $dn = self::dn_decode($dn);
a97937 620
T 621             $this->_debug("C: Read [dn: $dn] [(objectclass=*)]");
622
623             if ($this->ldap_result = @ldap_read($this->conn, $dn, '(objectclass=*)', array_values($this->fieldmap)))
624                 $entry = ldap_first_entry($this->conn, $this->ldap_result);
625             else
626                 $this->_debug("S: ".ldap_error($this->conn));
627
628             if ($entry && ($rec = ldap_get_attributes($this->conn, $entry)))
629             {
630                 $this->_debug("S: OK"/* . print_r($rec, true)*/);
631
632                 $rec = array_change_key_case($rec, CASE_LOWER);
633
634                 // Add in the dn for the entry.
635                 $rec['dn'] = $dn;
636                 $res = $this->_ldap2result($rec);
637                 $this->result = new rcube_result_set(1);
638                 $this->result->add($res);
6039aa 639             }
T 640         }
a97937 641
T 642         return $assoc ? $res : $this->result;
f11541 643     }
T 644
645
a97937 646     /**
e84818 647      * Check the given data before saving.
T 648      * If input not valid, the message to display can be fetched using get_error()
649      *
650      * @param array Assoziative array with data to save
cc90ed 651      *
e84818 652      * @return boolean True if input is valid, False if not.
T 653      */
654     public function validate($save_data)
655     {
656         // check for name input
657         if (empty($save_data['name'])) {
658             $this->set_error('warning', 'nonamewarning');
659             return false;
660         }
cc90ed 661
e84818 662         // validate e-mail addresses
T 663         return parent::validate($save_data);
664     }
665
666
667     /**
cc90ed 668      * Create a new contact record
A 669      *
670      * @param array    Hash array with save data
671      *
672      * @return encoded record ID on success, False on error
673      */
a97937 674     function insert($save_cols)
f11541 675     {
a97937 676         // Map out the column names to their LDAP ones to build the new entry.
T 677         $newentry = array();
678         $newentry['objectClass'] = $this->prop['LDAP_Object_Classes'];
679         foreach ($this->fieldmap as $col => $fld) {
680             $val = $save_cols[$col];
681             if (is_array($val))
682                 $val = array_filter($val);  // remove empty entries
683             if ($fld && $val) {
684                 // The field does exist, add it to the entry.
685                 $newentry[$fld] = $val;
4f9c83 686             } // end if
a97937 687         } // end foreach
T 688
689         // Verify that the required fields are set.
6f45fa 690         $missing = null;
a97937 691         foreach ($this->prop['required_fields'] as $fld) {
T 692             if (!isset($newentry[$fld])) {
693                 $missing[] = $fld;
694             }
695         }
696
697         // abort process if requiered fields are missing
698         // TODO: generate message saying which fields are missing
699         if ($missing) {
700             $this->set_error(self::ERROR_INCOMPLETE, 'formincomplete');
701             return false;
702         }
703
704         // Build the new entries DN.
d1e08f 705         $dn = $this->prop['LDAP_rdn'].'='.$this->_quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->base_dn;
a97937 706
T 707         $this->_debug("C: Add [dn: $dn]: ".print_r($newentry, true));
708
709         $res = ldap_add($this->conn, $dn, $newentry);
710         if ($res === FALSE) {
711             $this->_debug("S: ".ldap_error($this->conn));
712             $this->set_error(self::ERROR_SAVING, 'errorsaving');
713             return false;
4f9c83 714         } // end if
S 715
010274 716         $this->_debug("S: OK");
4f9c83 717
c3ba0e 718         $dn = self::dn_encode($dn);
A 719
a97937 720         // add new contact to the selected group
T 721         if ($this->groups)
c3ba0e 722             $this->add_to_group($this->group_id, $dn);
4f9c83 723
c3ba0e 724         return $dn;
e83f03 725     }
A 726
4f9c83 727
a97937 728     /**
cc90ed 729      * Update a specific contact record
A 730      *
731      * @param mixed Record identifier
732      * @param array Hash array with save data
733      *
734      * @return boolean True on success, False on error
735      */
a97937 736     function update($id, $save_cols)
f11541 737     {
a97937 738         $record = $this->get_record($id, true);
T 739         $result = $this->get_result();
740         $record = $result->first();
4aaecb 741
a97937 742         $newdata = array();
T 743         $replacedata = array();
744         $deletedata = array();
c3ba0e 745
0f1fae 746         // flatten composite fields in $record
T 747         if (is_array($record['address'])) {
748           foreach ($record['address'] as $i => $struct) {
749             foreach ($struct as $col => $val) {
750               $record[$col][$i] = $val;
751             }
752           }
753         }
cc90ed 754
a97937 755         foreach ($this->fieldmap as $col => $fld) {
T 756             $val = $save_cols[$col];
757             if ($fld) {
758                 // remove empty array values
759                 if (is_array($val))
760                     $val = array_filter($val);
761                 // The field does exist compare it to the ldap record.
762                 if ($record[$col] != $val) {
763                     // Changed, but find out how.
764                     if (!isset($record[$col])) {
765                         // Field was not set prior, need to add it.
766                         $newdata[$fld] = $val;
767                     } // end if
768                     elseif ($val == '') {
769                         // Field supplied is empty, verify that it is not required.
770                         if (!in_array($fld, $this->prop['required_fields'])) {
771                             // It is not, safe to clear.
772                             $deletedata[$fld] = $record[$col];
773                         } // end if
774                     } // end elseif
775                     else {
776                         // The data was modified, save it out.
777                         $replacedata[$fld] = $val;
778                     } // end else
779                 } // end if
780             } // end if
781         } // end foreach
010274 782
c3ba0e 783         $dn = self::dn_decode($id);
a97937 784
T 785         // Update the entry as required.
786         if (!empty($deletedata)) {
787             // Delete the fields.
788             $this->_debug("C: Delete [dn: $dn]: ".print_r($deletedata, true));
789             if (!ldap_mod_del($this->conn, $dn, $deletedata)) {
790                 $this->_debug("S: ".ldap_error($this->conn));
791                 $this->set_error(self::ERROR_SAVING, 'errorsaving');
792                 return false;
793             }
794             $this->_debug("S: OK");
795         } // end if
796
797         if (!empty($replacedata)) {
798             // Handle RDN change
799             if ($replacedata[$this->prop['LDAP_rdn']]) {
800                 $newdn = $this->prop['LDAP_rdn'].'='
801                     .$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true)
d1e08f 802                     .','.$this->base_dn;
a97937 803                 if ($dn != $newdn) {
T 804                     $newrdn = $this->prop['LDAP_rdn'].'='
805                     .$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true);
806                     unset($replacedata[$this->prop['LDAP_rdn']]);
807                 }
808             }
809             // Replace the fields.
810             if (!empty($replacedata)) {
811                 $this->_debug("C: Replace [dn: $dn]: ".print_r($replacedata, true));
812                 if (!ldap_mod_replace($this->conn, $dn, $replacedata)) {
813                     $this->_debug("S: ".ldap_error($this->conn));
814                     return false;
815                 }
816                 $this->_debug("S: OK");
817             } // end if
818         } // end if
819
820         if (!empty($newdata)) {
821             // Add the fields.
822             $this->_debug("C: Add [dn: $dn]: ".print_r($newdata, true));
823             if (!ldap_mod_add($this->conn, $dn, $newdata)) {
824                 $this->_debug("S: ".ldap_error($this->conn));
825                 $this->set_error(self::ERROR_SAVING, 'errorsaving');
826                 return false;
827             }
828             $this->_debug("S: OK");
829         } // end if
830
831         // Handle RDN change
832         if (!empty($newrdn)) {
833             $this->_debug("C: Rename [dn: $dn] [dn: $newrdn]");
834             if (!ldap_rename($this->conn, $dn, $newrdn, NULL, TRUE)) {
835                 $this->_debug("S: ".ldap_error($this->conn));
836                 return false;
837             }
838             $this->_debug("S: OK");
839
c3ba0e 840             $dn    = self::dn_encode($dn);
A 841             $newdn = self::dn_encode($newdn);
842
a97937 843             // change the group membership of the contact
T 844             if ($this->groups)
845             {
c3ba0e 846                 $group_ids = $this->get_record_groups($dn);
a97937 847                 foreach ($group_ids as $group_id)
T 848                 {
c3ba0e 849                     $this->remove_from_group($group_id, $dn);
A 850                     $this->add_to_group($group_id, $newdn);
a97937 851                 }
T 852             }
c3ba0e 853
A 854             return $newdn;
a97937 855         }
T 856
4aaecb 857         return true;
f11541 858     }
a97937 859
T 860
861     /**
cc90ed 862      * Mark one or more contact records as deleted
A 863      *
63fda8 864      * @param array   Record identifiers
A 865      * @param boolean Remove record(s) irreversible (unsupported)
cc90ed 866      *
A 867      * @return boolean True on success, False on error
868      */
63fda8 869     function delete($ids, $force=true)
f11541 870     {
a97937 871         if (!is_array($ids)) {
T 872             // Not an array, break apart the encoded DNs.
0f1fae 873             $ids = explode(',', $ids);
a97937 874         } // end if
T 875
0f1fae 876         foreach ($ids as $id) {
c3ba0e 877             $dn = self::dn_decode($id);
a97937 878             $this->_debug("C: Delete [dn: $dn]");
T 879             // Delete the record.
880             $res = ldap_delete($this->conn, $dn);
881             if ($res === FALSE) {
882                 $this->_debug("S: ".ldap_error($this->conn));
883                 $this->set_error(self::ERROR_SAVING, 'errorsaving');
884                 return false;
885             } // end if
886             $this->_debug("S: OK");
887
888             // remove contact from all groups where he was member
c3ba0e 889             if ($this->groups) {
A 890                 $dn = self::dn_encode($dn);
891                 $group_ids = $this->get_record_groups($dn);
892                 foreach ($group_ids as $group_id) {
893                     $this->remove_from_group($group_id, $dn);
a97937 894                 }
T 895             }
896         } // end foreach
897
0f1fae 898         return count($ids);
f11541 899     }
4368a0 900
A 901
a97937 902     /**
cc90ed 903      * Execute the LDAP search based on the stored credentials
A 904      */
c1db48 905     private function _exec_search($count = false)
a97937 906     {
T 907         if ($this->ready)
908         {
909             $filter = $this->filter ? $this->filter : '(objectclass=*)';
910             $function = $this->prop['scope'] == 'sub' ? 'ldap_search' : ($this->prop['scope'] == 'base' ? 'ldap_read' : 'ldap_list');
010274 911
755189 912             $this->_debug("C: Search [$filter]");
69ea3a 913
6af7e0 914             // when using VLV, we get the total count by...
c1db48 915             if (!$count && $function != 'ldap_read' && $this->prop['vlv']) {
6af7e0 916                 // ...either reading numSubOrdinates attribute
T 917                 if ($this->prop['numsub_filter'] && ($result_count = @$function($this->conn, $this->base_dn, $this->prop['numsub_filter'], array('numSubOrdinates'), 0, 0, 0))) {
918                     $counts = ldap_get_entries($this->conn, $result_count);
919                     for ($this->vlv_count = $j = 0; $j < $counts['count']; $j++)
920                         $this->vlv_count += $counts[$j]['numsubordinates'][0];
921                     $this->_debug("D: total numsubordinates = " . $this->vlv_count);
922                 }
923                 else  // ...or by fetching all records dn and count them
924                     $this->vlv_count = $this->_exec_search(true);
755189 925
69ea3a 926                 $this->vlv_active = $this->_vlv_set_controls();
T 927             }
100440 928
c1db48 929             // only fetch dn for count (should keep the payload low)
T 930             $attrs = $count ? array('dn') : array_values($this->fieldmap);
d1e08f 931             if ($this->ldap_result = @$function($this->conn, $this->base_dn, $filter,
c1db48 932                 $attrs, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']))
a97937 933             {
T 934                 $this->_debug("S: ".ldap_count_entries($this->conn, $this->ldap_result)." record(s)");
795692 935                 if ($err = ldap_errno($this->conn))
T 936                     $this->_debug("S: Error: " .ldap_err2str($err));
a97937 937                 return true;
T 938             }
939             else
940             {
941                 $this->_debug("S: ".ldap_error($this->conn));
942             }
943         }
944
945         return false;
69ea3a 946     }
cc90ed 947
69ea3a 948     /**
T 949      * Set server controls for Virtual List View (paginated listing)
950      */
951     private function _vlv_set_controls()
952     {
fd8975 953         $sort_ctrl = array('oid' => "1.2.840.113556.1.4.473",  'value' => $this->_sort_ber_encode((array)$this->prop['sort']));
69ea3a 954         $vlv_ctrl  = array('oid' => "2.16.840.1.113730.3.4.9", 'value' => $this->_vlv_ber_encode(($offset = ($this->list_page-1) * $this->page_size + 1), $this->page_size), 'iscritical' => true);
T 955
795692 956         $this->_debug("C: set controls sort=" . join(' ', unpack('H'.(strlen($sort_ctrl['value'])*2), $sort_ctrl['value'])) . " ({$this->sort_col});"
T 957             . " vlv=" . join(' ', (unpack('H'.(strlen($vlv_ctrl['value'])*2), $vlv_ctrl['value']))) . " ($offset)");
69ea3a 958
T 959         if (!ldap_set_option($this->conn, LDAP_OPT_SERVER_CONTROLS, array($sort_ctrl, $vlv_ctrl))) {
960             $this->_debug("S: ".ldap_error($this->conn));
961             $this->set_error(self::ERROR_SEARCH, 'vlvnotsupported');
962             return false;
963         }
964
965         return true;
a97937 966     }
T 967
968
969     /**
cc90ed 970      * Converts LDAP entry into an array
A 971      */
a97937 972     private function _ldap2result($rec)
T 973     {
974         $out = array();
975
976         if ($rec['dn'])
c3ba0e 977             $out[$this->primary_key] = self::dn_encode($rec['dn']);
a97937 978
T 979         foreach ($this->fieldmap as $rf => $lf)
980         {
981             for ($i=0; $i < $rec[$lf]['count']; $i++) {
982                 if (!($value = $rec[$lf][$i]))
983                     continue;
984                 if ($rf == 'email' && $this->mail_domain && !strpos($value, '@'))
985                     $out[$rf][] = sprintf('%s@%s', $value, $this->mail_domain);
986                 else if (in_array($rf, array('street','zipcode','locality','country','region')))
987                     $out['address'][$i][$rf] = $value;
988                 else if ($rec[$lf]['count'] > 1)
989                     $out[$rf][] = $value;
990                 else
991                     $out[$rf] = $value;
992             }
993         }
994
995         return $out;
996     }
997
998
999     /**
cc90ed 1000      * Return real field name (from fields map)
A 1001      */
a97937 1002     private function _map_field($field)
T 1003     {
1004         return $this->fieldmap[$field];
1005     }
1006
1007
1008     /**
cc90ed 1009      * Returns unified attribute name (resolving aliases)
A 1010      */
1011     private static function _attr_name($name)
a97937 1012     {
T 1013         // list of known attribute aliases
1014         $aliases = array(
1015             'gn' => 'givenname',
1016             'rfc822mailbox' => 'email',
1017             'userid' => 'uid',
1018             'emailaddress' => 'email',
1019             'pkcs9email' => 'email',
1020         );
1021         return isset($aliases[$name]) ? $aliases[$name] : $name;
1022     }
1023
1024
1025     /**
cc90ed 1026      * Prints debug info to the log
A 1027      */
a97937 1028     private function _debug($str)
T 1029     {
1030         if ($this->debug)
1031             write_log('ldap', $str);
1032     }
1033
1034
1035     /**
cc90ed 1036      * Quotes attribute value string
A 1037      *
1038      * @param string $str Attribute value
1039      * @param bool   $dn  True if the attribute is a DN
1040      *
1041      * @return string Quoted string
1042      */
1043     private static function _quote_string($str, $dn=false)
a97937 1044     {
T 1045         // take firt entry if array given
1046         if (is_array($str))
1047             $str = reset($str);
1048
1049         if ($dn)
1050             $replace = array(','=>'\2c', '='=>'\3d', '+'=>'\2b', '<'=>'\3c',
1051                 '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c', '"'=>'\22', '#'=>'\23');
1052         else
1053             $replace = array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c',
1054                 '/'=>'\2f');
1055
1056         return strtr($str, $replace);
1057     }
f11541 1058
6039aa 1059
T 1060     /**
1061      * Setter for the current group
1062      * (empty, has to be re-implemented by extending class)
1063      */
1064     function set_group($group_id)
1065     {
1066         if ($group_id)
1067         {
a97937 1068             if (!$this->group_cache)
T 1069                 $this->list_groups();
1070
1071             $cache_members = $this->group_cache[$group_id]['members'];
6039aa 1072
T 1073             $members = array();
ce4e0e 1074             for ($i=0; $i<$cache_members["count"]; $i++)
6039aa 1075             {
ce4e0e 1076                 if (!empty($cache_members[$i]))
c3ba0e 1077                     $members[self::dn_encode($cache_members[$i])] = 1;
6039aa 1078             }
T 1079             $this->group_members = $members;
1080             $this->group_id = $group_id;
1081         }
a97937 1082         else
T 1083             $this->group_id = 0;
6039aa 1084     }
T 1085
1086     /**
1087      * List all active contact groups of this source
1088      *
1089      * @param string  Optional search string to match group name
1090      * @return array  Indexed list of contact groups, each a hash array
1091      */
1092     function list_groups($search = null)
1093     {
d1e08f 1094         global $RCMAIL;
T 1095
a97937 1096         if (!$this->groups)
T 1097             return array();
6039aa 1098
d1e08f 1099         $this->groups_base_dn = ($this->prop['groups']['base_dn']) ?
T 1100                 $this->prop['groups']['base_dn'] : $this->base_dn;
1101
1102         // replace user specific dn
1103         if ($this->prop['user_specific'])
1104         {
1105             $fu = $RCMAIL->user->get_username();
1106             list($u, $d) = explode('@', $fu);
1107             $dc = 'dc='.strtr($d, array('.' => ',dc='));
1108             $replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u);
1109
a17030 1110             $this->groups_base_dn = strtr($this->groups_base_dn, $replaces);
d1e08f 1111         }
T 1112
1113         $base_dn = $this->groups_base_dn;
1114         $filter = $this->prop['groups']['filter'];
6039aa 1115
755189 1116         $this->_debug("C: Search [$filter][dn: $base_dn]");
A 1117
6039aa 1118         $res = ldap_search($this->conn, $base_dn, $filter, array('cn','member'));
T 1119         if ($res === false)
1120         {
1121             $this->_debug("S: ".ldap_error($this->conn));
1122             $this->set_error(self::ERROR_SAVING, 'errorsaving');
1123             return array();
1124         }
755189 1125
6039aa 1126         $ldap_data = ldap_get_entries($this->conn, $res);
755189 1127         $this->_debug("S: ".ldap_count_entries($this->conn, $res)." record(s)");
6039aa 1128
T 1129         $groups = array();
1130         $group_sortnames = array();
1131         for ($i=0; $i<$ldap_data["count"]; $i++)
1132         {
1133             $group_name = $ldap_data[$i]['cn'][0];
4d9f62 1134             if (!$search || strstr(strtolower($group_name), strtolower($search)))
T 1135             {
c3ba0e 1136                 $group_id = self::dn_encode($group_name);
4d9f62 1137                 $groups[$group_id]['ID'] = $group_id;
T 1138                 $groups[$group_id]['name'] = $group_name;
1139                 $groups[$group_id]['members'] = $ldap_data[$i]['member'];
1140                 $group_sortnames[] = strtolower($group_name);
1141             }
6039aa 1142         }
T 1143         array_multisort($group_sortnames, SORT_ASC, SORT_STRING, $groups);
1144         $this->group_cache = $groups;
a97937 1145
6039aa 1146         return $groups;
T 1147     }
1148
1149     /**
1150      * Create a contact group with the given name
1151      *
1152      * @param string The group name
1153      * @return mixed False on error, array with record props in success
1154      */
1155     function create_group($group_name)
1156     {
1157         if (!$this->group_cache)
1158             $this->list_groups();
1159
d1e08f 1160         $base_dn = $this->groups_base_dn;
6039aa 1161         $new_dn = "cn=$group_name,$base_dn";
c3ba0e 1162         $new_gid = self::dn_encode($group_name);
6039aa 1163
T 1164         $new_entry = array(
d1e08f 1165             'objectClass' => $this->prop['groups']['object_classes'],
6039aa 1166             'cn' => $group_name,
T 1167             'member' => '',
1168         );
1169
755189 1170         $this->_debug("C: Add [dn: $new_dn]: ".print_r($new_entry, true));
A 1171
6039aa 1172         $res = ldap_add($this->conn, $new_dn, $new_entry);
T 1173         if ($res === false)
1174         {
1175             $this->_debug("S: ".ldap_error($this->conn));
1176             $this->set_error(self::ERROR_SAVING, 'errorsaving');
1177             return false;
1178         }
755189 1179
A 1180         $this->_debug("S: OK");
1181
6039aa 1182         return array('id' => $new_gid, 'name' => $group_name);
T 1183     }
1184
1185     /**
1186      * Delete the given group and all linked group members
1187      *
1188      * @param string Group identifier
1189      * @return boolean True on success, false if no data was changed
1190      */
1191     function delete_group($group_id)
1192     {
1193         if (!$this->group_cache)
1194             $this->list_groups();
1195
d1e08f 1196         $base_dn = $this->groups_base_dn;
6039aa 1197         $group_name = $this->group_cache[$group_id]['name'];
T 1198         $del_dn = "cn=$group_name,$base_dn";
755189 1199
A 1200         $this->_debug("C: Delete [dn: $del_dn]");
1201
6039aa 1202         $res = ldap_delete($this->conn, $del_dn);
T 1203         if ($res === false)
1204         {
1205             $this->_debug("S: ".ldap_error($this->conn));
1206             $this->set_error(self::ERROR_SAVING, 'errorsaving');
1207             return false;
1208         }
755189 1209
A 1210         $this->_debug("S: OK");
1211
6039aa 1212         return true;
T 1213     }
1214
1215     /**
1216      * Rename a specific contact group
1217      *
1218      * @param string Group identifier
1219      * @param string New name to set for this group
360bd3 1220      * @param string New group identifier (if changed, otherwise don't set)
6039aa 1221      * @return boolean New name on success, false if no data was changed
T 1222      */
15e944 1223     function rename_group($group_id, $new_name, &$new_gid)
6039aa 1224     {
T 1225         if (!$this->group_cache)
1226             $this->list_groups();
1227
d1e08f 1228         $base_dn = $this->groups_base_dn;
6039aa 1229         $group_name = $this->group_cache[$group_id]['name'];
T 1230         $old_dn = "cn=$group_name,$base_dn";
1231         $new_rdn = "cn=$new_name";
c3ba0e 1232         $new_gid = self::dn_encode($new_name);
6039aa 1233
755189 1234         $this->_debug("C: Rename [dn: $old_dn] [dn: $new_rdn]");
A 1235
6039aa 1236         $res = ldap_rename($this->conn, $old_dn, $new_rdn, NULL, TRUE);
T 1237         if ($res === false)
1238         {
1239             $this->_debug("S: ".ldap_error($this->conn));
1240             $this->set_error(self::ERROR_SAVING, 'errorsaving');
1241             return false;
1242         }
755189 1243
A 1244         $this->_debug("S: OK");
1245
6039aa 1246         return $new_name;
T 1247     }
1248
1249     /**
1250      * Add the given contact records the a certain group
1251      *
1252      * @param string  Group identifier
1253      * @param array   List of contact identifiers to be added
1254      * @return int    Number of contacts added
1255      */
1256     function add_to_group($group_id, $contact_ids)
1257     {
1258         if (!$this->group_cache)
1259             $this->list_groups();
1260
d1e08f 1261         $base_dn = $this->groups_base_dn;
6039aa 1262         $group_name = $this->group_cache[$group_id]['name'];
T 1263         $group_dn = "cn=$group_name,$base_dn";
1264
1265         $new_attrs = array();
1266         foreach (explode(",", $contact_ids) as $id)
c3ba0e 1267             $new_attrs['member'][] = self::dn_decode($id);
6039aa 1268
755189 1269         $this->_debug("C: Add [dn: $group_dn]: ".print_r($new_attrs, true));
A 1270
6039aa 1271         $res = ldap_mod_add($this->conn, $group_dn, $new_attrs);
T 1272         if ($res === false)
1273         {
1274             $this->_debug("S: ".ldap_error($this->conn));
1275             $this->set_error(self::ERROR_SAVING, 'errorsaving');
1276             return 0;
1277         }
755189 1278
A 1279         $this->_debug("S: OK");
1280
6039aa 1281         return count($new_attrs['member']);
T 1282     }
1283
1284     /**
1285      * Remove the given contact records from a certain group
1286      *
1287      * @param string  Group identifier
1288      * @param array   List of contact identifiers to be removed
1289      * @return int    Number of deleted group members
1290      */
1291     function remove_from_group($group_id, $contact_ids)
1292     {
1293         if (!$this->group_cache)
1294             $this->list_groups();
1295
d1e08f 1296         $base_dn = $this->groups_base_dn;
6039aa 1297         $group_name = $this->group_cache[$group_id]['name'];
T 1298         $group_dn = "cn=$group_name,$base_dn";
1299
1300         $del_attrs = array();
1301         foreach (explode(",", $contact_ids) as $id)
c3ba0e 1302             $del_attrs['member'][] = self::dn_decode($id);
6039aa 1303
755189 1304         $this->_debug("C: Delete [dn: $group_dn]: ".print_r($del_attrs, true));
A 1305
6039aa 1306         $res = ldap_mod_del($this->conn, $group_dn, $del_attrs);
T 1307         if ($res === false)
1308         {
1309             $this->_debug("S: ".ldap_error($this->conn));
1310             $this->set_error(self::ERROR_SAVING, 'errorsaving');
1311             return 0;
1312         }
755189 1313
A 1314         $this->_debug("S: OK");
1315
6039aa 1316         return count($del_attrs['member']);
T 1317     }
1318
1319     /**
1320      * Get group assignments of a specific contact record
1321      *
1322      * @param mixed Record identifier
1323      *
1324      * @return array List of assigned groups as ID=>Name pairs
1325      * @since 0.5-beta
1326      */
1327     function get_record_groups($contact_id)
1328     {
a97937 1329         if (!$this->groups)
6039aa 1330             return array();
T 1331
d1e08f 1332         $base_dn = $this->groups_base_dn;
c3ba0e 1333         $contact_dn = self::dn_decode($contact_id);
94ce75 1334         $filter = strtr("(member=$contact_dn)", array('\\' => '\\\\'));
6039aa 1335
755189 1336         $this->_debug("C: Search [$filter][dn: $base_dn]");
A 1337
6039aa 1338         $res = ldap_search($this->conn, $base_dn, $filter, array('cn'));
T 1339         if ($res === false)
1340         {
1341             $this->_debug("S: ".ldap_error($this->conn));
1342             $this->set_error(self::ERROR_SAVING, 'errorsaving');
1343             return array();
1344         }
1345         $ldap_data = ldap_get_entries($this->conn, $res);
755189 1346         $this->_debug("S: ".ldap_count_entries($this->conn, $res)." record(s)");
6039aa 1347
T 1348         $groups = array();
1349         for ($i=0; $i<$ldap_data["count"]; $i++)
1350         {
1351             $group_name = $ldap_data[$i]['cn'][0];
c3ba0e 1352             $group_id = self::dn_encode($group_name);
6039aa 1353             $groups[$group_id] = $group_id;
T 1354         }
1355         return $groups;
1356     }
69ea3a 1357
T 1358
1359     /**
1360      * Generate BER encoded string for Virtual List View option
1361      *
1362      * @param integer List offset (first record)
1363      * @param integer Records per page
1364      * @return string BER encoded option value
1365      */
1366     private function _vlv_ber_encode($offset, $rpp)
1367     {
1368         # this string is ber-encoded, php will prefix this value with:
1369         # 04 (octet string) and 10 (length of 16 bytes)
1370         # the code behind this string is broken down as follows:
1371         # 30 = ber sequence with a length of 0e (14) bytes following
1372         # 20 = type integer (in two's complement form) with 2 bytes following (beforeCount): 01 00 (ie 0)
1373         # 20 = type integer (in two's complement form) with 2 bytes following (afterCount):  01 18 (ie 25-1=24)
1374         # a0 = type context-specific/constructed with a length of 06 (6) bytes following
1375         # 20 = type integer with 2 bytes following (offset): 01 01 (ie 1)
1376         # 20 = type integer with 2 bytes following (contentCount):  01 00
1377         # the following info was taken from the ISO/IEC 8825-1:2003 x.690 standard re: the
1378         # encoding of integer values (note: these values are in
1379         # two-complement form so since offset will never be negative bit 8 of the
1380         # leftmost octet should never by set to 1):
1381         # 8.3.2: If the contents octets of an integer value encoding consist
1382         # of more than one octet, then the bits of the first octet (rightmost) and bit 8
1383         # of the second (to the left of first octet) octet:
1384         # a) shall not all be ones; and
1385         # b) shall not all be zero
1386
1387         # construct the string from right to left
1388         $str = "020100"; # contentCount
cc90ed 1389
69ea3a 1390         $ber_val = self::_ber_encode_int($offset);  // returns encoded integer value in hex format
T 1391
1392         // calculate octet length of $ber_val
1393         $str = self::_ber_addseq($ber_val, '02') . $str;
1394
1395         // now compute length over $str
1396         $str = self::_ber_addseq($str, 'a0');
1397
1398         // now tack on records per page
1399         $str = sprintf("0201000201%02x", min(255, $rpp)-1) . $str;
1400
1401         // now tack on sequence identifier and length
1402         $str = self::_ber_addseq($str, '30');
1403
1404         return pack('H'.strlen($str), $str);
1405     }
1406
1407
1408     /**
1409      * create ber encoding for sort control
1410      *
c3ba0e 1411      * @param array List of cols to sort by
69ea3a 1412      * @return string BER encoded option value
T 1413      */
1414     private function _sort_ber_encode($sortcols)
1415     {
1416         $str = '';
1417         foreach (array_reverse((array)$sortcols) as $col) {
1418             $ber_val = self::_string2hex($col);
1419
1420             # 30 = ber sequence with a length of octet value
1421             # 04 = octet string with a length of the ascii value
1422             $oct = self::_ber_addseq($ber_val, '04');
1423             $str = self::_ber_addseq($oct, '30') . $str;
1424         }
1425
1426         // now tack on sequence identifier and length
1427         $str = self::_ber_addseq($str, '30');
1428
1429         return pack('H'.strlen($str), $str);
1430     }
1431
1432     /**
1433      * Add BER sequence with correct length and the given identifier
1434      */
1435     private static function _ber_addseq($str, $identifier)
1436     {
6f3fa9 1437         $len = dechex(strlen($str)/2);
69ea3a 1438         if (strlen($len) % 2 != 0)
T 1439             $len = '0'.$len;
1440
1441         return $identifier . $len . $str;
1442     }
1443
1444     /**
1445      * Returns BER encoded integer value in hex format
1446      */
1447     private static function _ber_encode_int($offset)
1448     {
6f3fa9 1449         $val = dechex($offset);
69ea3a 1450         $prefix = '';
cc90ed 1451
69ea3a 1452         // check if bit 8 of high byte is 1
T 1453         if (preg_match('/^[89abcdef]/', $val))
1454             $prefix = '00';
1455
1456         if (strlen($val)%2 != 0)
1457             $prefix .= '0';
1458
1459         return $prefix . $val;
1460     }
1461
1462     /**
1463      * Returns ascii string encoded in hex
1464      */
c3ba0e 1465     private static function _string2hex($str)
A 1466     {
69ea3a 1467         $hex = '';
T 1468         for ($i=0; $i < strlen($str); $i++)
1469             $hex .= dechex(ord($str[$i]));
1470         return $hex;
1471     }
1472
c3ba0e 1473     /**
A 1474      * HTML-safe DN string encoding
1475      *
1476      * @param string $str DN string
1477      *
1478      * @return string Encoded HTML identifier string
1479      */
1480     static function dn_encode($str)
1481     {
1482         // @TODO: to make output string shorter we could probably
1483         //        remove dc=* items from it
1484         return rtrim(strtr(base64_encode($str), '+/', '-_'), '=');
1485     }
1486
1487     /**
1488      * Decodes DN string encoded with _dn_encode()
1489      *
1490      * @param string $str Encoded HTML identifier string
1491      *
1492      * @return string DN string
1493      */
1494     static function dn_decode($str)
1495     {
1496         $str = str_pad(strtr($str, '-_', '+/'), strlen($str) % 4, '=', STR_PAD_RIGHT);
1497         return base64_decode($str);
1498     }
f11541 1499 }