alecpl
2011-08-01 363514e30bcc31bf4055d39c9d90044b0e63ff3a
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();
cc90ed 731
a97937 732         foreach ($this->fieldmap as $col => $fld) {
T 733             $val = $save_cols[$col];
734             if ($fld) {
735                 // remove empty array values
736                 if (is_array($val))
737                     $val = array_filter($val);
738                 // The field does exist compare it to the ldap record.
739                 if ($record[$col] != $val) {
740                     // Changed, but find out how.
741                     if (!isset($record[$col])) {
742                         // Field was not set prior, need to add it.
743                         $newdata[$fld] = $val;
744                     } // end if
745                     elseif ($val == '') {
746                         // Field supplied is empty, verify that it is not required.
747                         if (!in_array($fld, $this->prop['required_fields'])) {
748                             // It is not, safe to clear.
749                             $deletedata[$fld] = $record[$col];
750                         } // end if
751                     } // end elseif
752                     else {
753                         // The data was modified, save it out.
754                         $replacedata[$fld] = $val;
755                     } // end else
756                 } // end if
757             } // end if
758         } // end foreach
010274 759
a97937 760         $dn = base64_decode($id);
T 761
762         // Update the entry as required.
763         if (!empty($deletedata)) {
764             // Delete the fields.
765             $this->_debug("C: Delete [dn: $dn]: ".print_r($deletedata, true));
766             if (!ldap_mod_del($this->conn, $dn, $deletedata)) {
767                 $this->_debug("S: ".ldap_error($this->conn));
768                 $this->set_error(self::ERROR_SAVING, 'errorsaving');
769                 return false;
770             }
771             $this->_debug("S: OK");
772         } // end if
773
774         if (!empty($replacedata)) {
775             // Handle RDN change
776             if ($replacedata[$this->prop['LDAP_rdn']]) {
777                 $newdn = $this->prop['LDAP_rdn'].'='
778                     .$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true)
d1e08f 779                     .','.$this->base_dn;
a97937 780                 if ($dn != $newdn) {
T 781                     $newrdn = $this->prop['LDAP_rdn'].'='
782                     .$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true);
783                     unset($replacedata[$this->prop['LDAP_rdn']]);
784                 }
785             }
786             // Replace the fields.
787             if (!empty($replacedata)) {
788                 $this->_debug("C: Replace [dn: $dn]: ".print_r($replacedata, true));
789                 if (!ldap_mod_replace($this->conn, $dn, $replacedata)) {
790                     $this->_debug("S: ".ldap_error($this->conn));
791                     return false;
792                 }
793                 $this->_debug("S: OK");
794             } // end if
795         } // end if
796
797         if (!empty($newdata)) {
798             // Add the fields.
799             $this->_debug("C: Add [dn: $dn]: ".print_r($newdata, true));
800             if (!ldap_mod_add($this->conn, $dn, $newdata)) {
801                 $this->_debug("S: ".ldap_error($this->conn));
802                 $this->set_error(self::ERROR_SAVING, 'errorsaving');
803                 return false;
804             }
805             $this->_debug("S: OK");
806         } // end if
807
808         // Handle RDN change
809         if (!empty($newrdn)) {
810             $this->_debug("C: Rename [dn: $dn] [dn: $newrdn]");
811             if (!ldap_rename($this->conn, $dn, $newrdn, NULL, TRUE)) {
812                 $this->_debug("S: ".ldap_error($this->conn));
813                 return false;
814             }
815             $this->_debug("S: OK");
816
817             // change the group membership of the contact
818             if ($this->groups)
819             {
820                 $group_ids = $this->get_record_groups(base64_encode($dn));
821                 foreach ($group_ids as $group_id)
822                 {
823                     $this->remove_from_group($group_id, base64_encode($dn));
824                     $this->add_to_group($group_id, base64_encode($newdn));
825                 }
826             }
827             return base64_encode($newdn);
828         }
829
4aaecb 830         return true;
f11541 831     }
a97937 832
T 833
834     /**
cc90ed 835      * Mark one or more contact records as deleted
A 836      *
63fda8 837      * @param array   Record identifiers
A 838      * @param boolean Remove record(s) irreversible (unsupported)
cc90ed 839      *
A 840      * @return boolean True on success, False on error
841      */
63fda8 842     function delete($ids, $force=true)
f11541 843     {
a97937 844         if (!is_array($ids)) {
T 845             // Not an array, break apart the encoded DNs.
846             $dns = explode(',', $ids);
847         } // end if
848
849         foreach ($dns as $id) {
850             $dn = base64_decode($id);
851             $this->_debug("C: Delete [dn: $dn]");
852             // Delete the record.
853             $res = ldap_delete($this->conn, $dn);
854             if ($res === FALSE) {
855                 $this->_debug("S: ".ldap_error($this->conn));
856                 $this->set_error(self::ERROR_SAVING, 'errorsaving');
857                 return false;
858             } // end if
859             $this->_debug("S: OK");
860
861             // remove contact from all groups where he was member
862             if ($this->groups)
863             {
864                 $group_ids = $this->get_record_groups(base64_encode($dn));
865                 foreach ($group_ids as $group_id)
866                 {
867                     $this->remove_from_group($group_id, base64_encode($dn));
868                 }
869             }
870         } // end foreach
871
872         return count($dns);
f11541 873     }
4368a0 874
A 875
a97937 876     /**
cc90ed 877      * Execute the LDAP search based on the stored credentials
A 878      */
c1db48 879     private function _exec_search($count = false)
a97937 880     {
T 881         if ($this->ready)
882         {
883             $filter = $this->filter ? $this->filter : '(objectclass=*)';
884             $function = $this->prop['scope'] == 'sub' ? 'ldap_search' : ($this->prop['scope'] == 'base' ? 'ldap_read' : 'ldap_list');
010274 885
a97937 886             $this->_debug("C: Search [".$filter."]");
69ea3a 887
6af7e0 888             // when using VLV, we get the total count by...
c1db48 889             if (!$count && $function != 'ldap_read' && $this->prop['vlv']) {
6af7e0 890                 // ...either reading numSubOrdinates attribute
T 891                 if ($this->prop['numsub_filter'] && ($result_count = @$function($this->conn, $this->base_dn, $this->prop['numsub_filter'], array('numSubOrdinates'), 0, 0, 0))) {
892                     $counts = ldap_get_entries($this->conn, $result_count);
893                     for ($this->vlv_count = $j = 0; $j < $counts['count']; $j++)
894                         $this->vlv_count += $counts[$j]['numsubordinates'][0];
895                     $this->_debug("D: total numsubordinates = " . $this->vlv_count);
896                 }
897                 else  // ...or by fetching all records dn and count them
898                     $this->vlv_count = $this->_exec_search(true);
899                 
69ea3a 900                 $this->vlv_active = $this->_vlv_set_controls();
T 901             }
100440 902
c1db48 903             // only fetch dn for count (should keep the payload low)
T 904             $attrs = $count ? array('dn') : array_values($this->fieldmap);
d1e08f 905             if ($this->ldap_result = @$function($this->conn, $this->base_dn, $filter,
c1db48 906                 $attrs, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']))
a97937 907             {
T 908                 $this->_debug("S: ".ldap_count_entries($this->conn, $this->ldap_result)." record(s)");
795692 909                 if ($err = ldap_errno($this->conn))
T 910                     $this->_debug("S: Error: " .ldap_err2str($err));
a97937 911                 return true;
T 912             }
913             else
914             {
915                 $this->_debug("S: ".ldap_error($this->conn));
916             }
917         }
918
919         return false;
69ea3a 920     }
cc90ed 921
69ea3a 922     /**
T 923      * Set server controls for Virtual List View (paginated listing)
924      */
925     private function _vlv_set_controls()
926     {
fd8975 927         $sort_ctrl = array('oid' => "1.2.840.113556.1.4.473",  'value' => $this->_sort_ber_encode((array)$this->prop['sort']));
69ea3a 928         $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 929
795692 930         $this->_debug("C: set controls sort=" . join(' ', unpack('H'.(strlen($sort_ctrl['value'])*2), $sort_ctrl['value'])) . " ({$this->sort_col});"
T 931             . " vlv=" . join(' ', (unpack('H'.(strlen($vlv_ctrl['value'])*2), $vlv_ctrl['value']))) . " ($offset)");
69ea3a 932
T 933         if (!ldap_set_option($this->conn, LDAP_OPT_SERVER_CONTROLS, array($sort_ctrl, $vlv_ctrl))) {
934             $this->_debug("S: ".ldap_error($this->conn));
935             $this->set_error(self::ERROR_SEARCH, 'vlvnotsupported');
936             return false;
937         }
938
939         return true;
a97937 940     }
T 941
942
943     /**
cc90ed 944      * Converts LDAP entry into an array
A 945      */
a97937 946     private function _ldap2result($rec)
T 947     {
948         $out = array();
949
950         if ($rec['dn'])
951             $out[$this->primary_key] = base64_encode($rec['dn']);
952
953         foreach ($this->fieldmap as $rf => $lf)
954         {
955             for ($i=0; $i < $rec[$lf]['count']; $i++) {
956                 if (!($value = $rec[$lf][$i]))
957                     continue;
958                 if ($rf == 'email' && $this->mail_domain && !strpos($value, '@'))
959                     $out[$rf][] = sprintf('%s@%s', $value, $this->mail_domain);
960                 else if (in_array($rf, array('street','zipcode','locality','country','region')))
961                     $out['address'][$i][$rf] = $value;
962                 else if ($rec[$lf]['count'] > 1)
963                     $out[$rf][] = $value;
964                 else
965                     $out[$rf] = $value;
966             }
967         }
968
969         return $out;
970     }
971
972
973     /**
cc90ed 974      * Return real field name (from fields map)
A 975      */
a97937 976     private function _map_field($field)
T 977     {
978         return $this->fieldmap[$field];
979     }
980
981
982     /**
cc90ed 983      * Returns unified attribute name (resolving aliases)
A 984      */
985     private static function _attr_name($name)
a97937 986     {
T 987         // list of known attribute aliases
988         $aliases = array(
989             'gn' => 'givenname',
990             'rfc822mailbox' => 'email',
991             'userid' => 'uid',
992             'emailaddress' => 'email',
993             'pkcs9email' => 'email',
994         );
995         return isset($aliases[$name]) ? $aliases[$name] : $name;
996     }
997
998
999     /**
cc90ed 1000      * Prints debug info to the log
A 1001      */
a97937 1002     private function _debug($str)
T 1003     {
1004         if ($this->debug)
1005             write_log('ldap', $str);
1006     }
1007
1008
1009     /**
cc90ed 1010      * Quotes attribute value string
A 1011      *
1012      * @param string $str Attribute value
1013      * @param bool   $dn  True if the attribute is a DN
1014      *
1015      * @return string Quoted string
1016      */
1017     private static function _quote_string($str, $dn=false)
a97937 1018     {
T 1019         // take firt entry if array given
1020         if (is_array($str))
1021             $str = reset($str);
1022
1023         if ($dn)
1024             $replace = array(','=>'\2c', '='=>'\3d', '+'=>'\2b', '<'=>'\3c',
1025                 '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c', '"'=>'\22', '#'=>'\23');
1026         else
1027             $replace = array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c',
1028                 '/'=>'\2f');
1029
1030         return strtr($str, $replace);
1031     }
f11541 1032
6039aa 1033
T 1034     /**
1035      * Setter for the current group
1036      * (empty, has to be re-implemented by extending class)
1037      */
1038     function set_group($group_id)
1039     {
1040         if ($group_id)
1041         {
a97937 1042             if (!$this->group_cache)
T 1043                 $this->list_groups();
1044
1045             $cache_members = $this->group_cache[$group_id]['members'];
6039aa 1046
T 1047             $members = array();
ce4e0e 1048             for ($i=0; $i<$cache_members["count"]; $i++)
6039aa 1049             {
ce4e0e 1050                 if (!empty($cache_members[$i]))
T 1051                     $members[base64_encode($cache_members[$i])] = 1;
6039aa 1052             }
T 1053             $this->group_members = $members;
1054             $this->group_id = $group_id;
1055         }
a97937 1056         else
T 1057             $this->group_id = 0;
6039aa 1058     }
T 1059
1060     /**
1061      * List all active contact groups of this source
1062      *
1063      * @param string  Optional search string to match group name
1064      * @return array  Indexed list of contact groups, each a hash array
1065      */
1066     function list_groups($search = null)
1067     {
d1e08f 1068         global $RCMAIL;
T 1069
a97937 1070         if (!$this->groups)
T 1071             return array();
6039aa 1072
d1e08f 1073         $this->groups_base_dn = ($this->prop['groups']['base_dn']) ?
T 1074                 $this->prop['groups']['base_dn'] : $this->base_dn;
1075
1076         // replace user specific dn
1077         if ($this->prop['user_specific'])
1078         {
1079             $fu = $RCMAIL->user->get_username();
1080             list($u, $d) = explode('@', $fu);
1081             $dc = 'dc='.strtr($d, array('.' => ',dc='));
1082             $replaces = array('%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u);
1083
1084             $this->groups_base_dn = strtr($this->groups_base_dn, $replaces);;
1085         }
1086
1087         $base_dn = $this->groups_base_dn;
1088         $filter = $this->prop['groups']['filter'];
6039aa 1089
T 1090         $res = ldap_search($this->conn, $base_dn, $filter, array('cn','member'));
1091         if ($res === false)
1092         {
1093             $this->_debug("S: ".ldap_error($this->conn));
1094             $this->set_error(self::ERROR_SAVING, 'errorsaving');
1095             return array();
1096         }
1097         $ldap_data = ldap_get_entries($this->conn, $res);
1098
1099         $groups = array();
1100         $group_sortnames = array();
1101         for ($i=0; $i<$ldap_data["count"]; $i++)
1102         {
1103             $group_name = $ldap_data[$i]['cn'][0];
4d9f62 1104             if (!$search || strstr(strtolower($group_name), strtolower($search)))
T 1105             {
1106                 $group_id = base64_encode($group_name);
1107                 $groups[$group_id]['ID'] = $group_id;
1108                 $groups[$group_id]['name'] = $group_name;
1109                 $groups[$group_id]['members'] = $ldap_data[$i]['member'];
1110                 $group_sortnames[] = strtolower($group_name);
1111             }
6039aa 1112         }
T 1113         array_multisort($group_sortnames, SORT_ASC, SORT_STRING, $groups);
1114         $this->group_cache = $groups;
a97937 1115
6039aa 1116         return $groups;
T 1117     }
1118
1119     /**
1120      * Create a contact group with the given name
1121      *
1122      * @param string The group name
1123      * @return mixed False on error, array with record props in success
1124      */
1125     function create_group($group_name)
1126     {
1127         if (!$this->group_cache)
1128             $this->list_groups();
1129
d1e08f 1130         $base_dn = $this->groups_base_dn;
6039aa 1131         $new_dn = "cn=$group_name,$base_dn";
T 1132         $new_gid = base64_encode($group_name);
1133
1134         $new_entry = array(
d1e08f 1135             'objectClass' => $this->prop['groups']['object_classes'],
6039aa 1136             'cn' => $group_name,
T 1137             'member' => '',
1138         );
1139
1140         $res = ldap_add($this->conn, $new_dn, $new_entry);
1141         if ($res === false)
1142         {
1143             $this->_debug("S: ".ldap_error($this->conn));
1144             $this->set_error(self::ERROR_SAVING, 'errorsaving');
1145             return false;
1146         }
1147         return array('id' => $new_gid, 'name' => $group_name);
1148     }
1149
1150     /**
1151      * Delete the given group and all linked group members
1152      *
1153      * @param string Group identifier
1154      * @return boolean True on success, false if no data was changed
1155      */
1156     function delete_group($group_id)
1157     {
1158         if (!$this->group_cache)
1159             $this->list_groups();
1160
d1e08f 1161         $base_dn = $this->groups_base_dn;
6039aa 1162         $group_name = $this->group_cache[$group_id]['name'];
T 1163
1164         $del_dn = "cn=$group_name,$base_dn";
1165         $res = ldap_delete($this->conn, $del_dn);
1166         if ($res === false)
1167         {
1168             $this->_debug("S: ".ldap_error($this->conn));
1169             $this->set_error(self::ERROR_SAVING, 'errorsaving');
1170             return false;
1171         }
1172         return true;
1173     }
1174
1175     /**
1176      * Rename a specific contact group
1177      *
1178      * @param string Group identifier
1179      * @param string New name to set for this group
360bd3 1180      * @param string New group identifier (if changed, otherwise don't set)
6039aa 1181      * @return boolean New name on success, false if no data was changed
T 1182      */
15e944 1183     function rename_group($group_id, $new_name, &$new_gid)
6039aa 1184     {
T 1185         if (!$this->group_cache)
1186             $this->list_groups();
1187
d1e08f 1188         $base_dn = $this->groups_base_dn;
6039aa 1189         $group_name = $this->group_cache[$group_id]['name'];
T 1190         $old_dn = "cn=$group_name,$base_dn";
1191         $new_rdn = "cn=$new_name";
15e944 1192         $new_gid = base64_encode($new_name);
6039aa 1193
T 1194         $res = ldap_rename($this->conn, $old_dn, $new_rdn, NULL, TRUE);
1195         if ($res === false)
1196         {
1197             $this->_debug("S: ".ldap_error($this->conn));
1198             $this->set_error(self::ERROR_SAVING, 'errorsaving');
1199             return false;
1200         }
1201         return $new_name;
1202     }
1203
1204     /**
1205      * Add the given contact records the a certain group
1206      *
1207      * @param string  Group identifier
1208      * @param array   List of contact identifiers to be added
1209      * @return int    Number of contacts added
1210      */
1211     function add_to_group($group_id, $contact_ids)
1212     {
1213         if (!$this->group_cache)
1214             $this->list_groups();
1215
d1e08f 1216         $base_dn = $this->groups_base_dn;
6039aa 1217         $group_name = $this->group_cache[$group_id]['name'];
T 1218         $group_dn = "cn=$group_name,$base_dn";
1219
1220         $new_attrs = array();
1221         foreach (explode(",", $contact_ids) as $id)
1222             $new_attrs['member'][] = base64_decode($id);
1223
1224         $res = ldap_mod_add($this->conn, $group_dn, $new_attrs);
1225         if ($res === false)
1226         {
1227             $this->_debug("S: ".ldap_error($this->conn));
1228             $this->set_error(self::ERROR_SAVING, 'errorsaving');
1229             return 0;
1230         }
1231         return count($new_attrs['member']);
1232     }
1233
1234     /**
1235      * Remove the given contact records from a certain group
1236      *
1237      * @param string  Group identifier
1238      * @param array   List of contact identifiers to be removed
1239      * @return int    Number of deleted group members
1240      */
1241     function remove_from_group($group_id, $contact_ids)
1242     {
1243         if (!$this->group_cache)
1244             $this->list_groups();
1245
d1e08f 1246         $base_dn = $this->groups_base_dn;
6039aa 1247         $group_name = $this->group_cache[$group_id]['name'];
T 1248         $group_dn = "cn=$group_name,$base_dn";
1249
1250         $del_attrs = array();
1251         foreach (explode(",", $contact_ids) as $id)
1252             $del_attrs['member'][] = base64_decode($id);
1253
1254         $res = ldap_mod_del($this->conn, $group_dn, $del_attrs);
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         }
1261         return count($del_attrs['member']);
1262     }
1263
1264     /**
1265      * Get group assignments of a specific contact record
1266      *
1267      * @param mixed Record identifier
1268      *
1269      * @return array List of assigned groups as ID=>Name pairs
1270      * @since 0.5-beta
1271      */
1272     function get_record_groups($contact_id)
1273     {
a97937 1274         if (!$this->groups)
6039aa 1275             return array();
T 1276
d1e08f 1277         $base_dn = $this->groups_base_dn;
6039aa 1278         $contact_dn = base64_decode($contact_id);
T 1279         $filter = "(member=$contact_dn)";
1280
1281         $res = ldap_search($this->conn, $base_dn, $filter, array('cn'));
1282         if ($res === false)
1283         {
1284             $this->_debug("S: ".ldap_error($this->conn));
1285             $this->set_error(self::ERROR_SAVING, 'errorsaving');
1286             return array();
1287         }
1288         $ldap_data = ldap_get_entries($this->conn, $res);
1289
1290         $groups = array();
1291         for ($i=0; $i<$ldap_data["count"]; $i++)
1292         {
1293             $group_name = $ldap_data[$i]['cn'][0];
1294             $group_id = base64_encode($group_name);
1295             $groups[$group_id] = $group_id;
1296         }
1297         return $groups;
1298     }
69ea3a 1299
T 1300
1301     /**
1302      * Generate BER encoded string for Virtual List View option
1303      *
1304      * @param integer List offset (first record)
1305      * @param integer Records per page
1306      * @return string BER encoded option value
1307      */
1308     private function _vlv_ber_encode($offset, $rpp)
1309     {
1310         # this string is ber-encoded, php will prefix this value with:
1311         # 04 (octet string) and 10 (length of 16 bytes)
1312         # the code behind this string is broken down as follows:
1313         # 30 = ber sequence with a length of 0e (14) bytes following
1314         # 20 = type integer (in two's complement form) with 2 bytes following (beforeCount): 01 00 (ie 0)
1315         # 20 = type integer (in two's complement form) with 2 bytes following (afterCount):  01 18 (ie 25-1=24)
1316         # a0 = type context-specific/constructed with a length of 06 (6) bytes following
1317         # 20 = type integer with 2 bytes following (offset): 01 01 (ie 1)
1318         # 20 = type integer with 2 bytes following (contentCount):  01 00
1319         # the following info was taken from the ISO/IEC 8825-1:2003 x.690 standard re: the
1320         # encoding of integer values (note: these values are in
1321         # two-complement form so since offset will never be negative bit 8 of the
1322         # leftmost octet should never by set to 1):
1323         # 8.3.2: If the contents octets of an integer value encoding consist
1324         # of more than one octet, then the bits of the first octet (rightmost) and bit 8
1325         # of the second (to the left of first octet) octet:
1326         # a) shall not all be ones; and
1327         # b) shall not all be zero
1328
1329         # construct the string from right to left
1330         $str = "020100"; # contentCount
cc90ed 1331
69ea3a 1332         $ber_val = self::_ber_encode_int($offset);  // returns encoded integer value in hex format
T 1333
1334         // calculate octet length of $ber_val
1335         $str = self::_ber_addseq($ber_val, '02') . $str;
1336
1337         // now compute length over $str
1338         $str = self::_ber_addseq($str, 'a0');
1339
1340         // now tack on records per page
1341         $str = sprintf("0201000201%02x", min(255, $rpp)-1) . $str;
1342
1343         // now tack on sequence identifier and length
1344         $str = self::_ber_addseq($str, '30');
1345
1346         return pack('H'.strlen($str), $str);
1347     }
1348
1349
1350     /**
1351      * create ber encoding for sort control
1352      *
1353      * @pararm array List of cols to sort by
1354      * @return string BER encoded option value
1355      */
1356     private function _sort_ber_encode($sortcols)
1357     {
1358         $str = '';
1359         foreach (array_reverse((array)$sortcols) as $col) {
1360             $ber_val = self::_string2hex($col);
1361
1362             # 30 = ber sequence with a length of octet value
1363             # 04 = octet string with a length of the ascii value
1364             $oct = self::_ber_addseq($ber_val, '04');
1365             $str = self::_ber_addseq($oct, '30') . $str;
1366         }
1367
1368         // now tack on sequence identifier and length
1369         $str = self::_ber_addseq($str, '30');
1370
1371         return pack('H'.strlen($str), $str);
1372     }
1373
1374     /**
1375      * Add BER sequence with correct length and the given identifier
1376      */
1377     private static function _ber_addseq($str, $identifier)
1378     {
6f3fa9 1379         $len = dechex(strlen($str)/2);
69ea3a 1380         if (strlen($len) % 2 != 0)
T 1381             $len = '0'.$len;
1382
1383         return $identifier . $len . $str;
1384     }
1385
1386     /**
1387      * Returns BER encoded integer value in hex format
1388      */
1389     private static function _ber_encode_int($offset)
1390     {
6f3fa9 1391         $val = dechex($offset);
69ea3a 1392         $prefix = '';
cc90ed 1393
69ea3a 1394         // check if bit 8 of high byte is 1
T 1395         if (preg_match('/^[89abcdef]/', $val))
1396             $prefix = '00';
1397
1398         if (strlen($val)%2 != 0)
1399             $prefix .= '0';
1400
1401         return $prefix . $val;
1402     }
1403
1404     /**
1405      * Returns ascii string encoded in hex
1406      */
1407     private static function _string2hex($str) {
1408         $hex = '';
1409         for ($i=0; $i < strlen($str); $i++)
1410             $hex .= dechex(ord($str[$i]));
1411         return $hex;
1412     }
1413
f11541 1414 }