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