Aleksander Machniak
2014-05-12 f92cccac38f877ab3537dcb489bb4a6216334f84
commit | author | age
203323 1 <?php
TB 2
3 /*
4  +-----------------------------------------------------------------------+
5  | Roundcube/rcube_ldap_generic.php                                      |
6  |                                                                       |
7  | This file is part of the Roundcube Webmail client                     |
8  | Copyright (C) 2006-2013, The Roundcube Dev Team                       |
9  | Copyright (C) 2012-2013, Kolab Systems AG                             |
10  |                                                                       |
11  | Licensed under the GNU General Public License version 3 or            |
12  | any later version with exceptions for skins & plugins.                |
13  | See the README file for a full license statement.                     |
14  |                                                                       |
15  | PURPOSE:                                                              |
16  |   Provide basic functionality for accessing LDAP directories          |
17  |                                                                       |
18  +-----------------------------------------------------------------------+
19  | Author: Thomas Bruederli <roundcube@gmail.com>                        |
20  |         Aleksander Machniak <machniak@kolabsys.com>                   |
21  +-----------------------------------------------------------------------+
22 */
23
24
25 /*
26   LDAP connection properties
27   --------------------------
28
29   $prop = array(
30       'host'            => '<ldap-server-address>',
31       // or
32       'hosts'           => array('directory.verisign.com'),
33       'port'            => 389,
c9ed4b 34       'use_tls'         => true|false,
203323 35       'ldap_version'    => 3,             // using LDAPv3
TB 36       'auth_method'     => '',            // SASL authentication method (for proxy auth), e.g. DIGEST-MD5
37       'attributes'      => array('dn'),   // List of attributes to read from the server
38       'vlv'             => false,         // Enable Virtual List View to more efficiently fetch paginated data (if server supports it)
004f86 39       'config_root_dn'  => 'cn=config',   // Root DN to read config (e.g. vlv indexes) from
203323 40       'numsub_filter'   => '(objectClass=organizationalUnit)',   // with VLV, we also use numSubOrdinates to query the total number of records. Set this filter to get all numSubOrdinates attributes for counting
TB 41       'sizelimit'       => '0',           // Enables you to limit the count of entries fetched. Setting this to 0 means no limit.
42       'timelimit'       => '0',           // Sets the number of seconds how long is spend on the search. Setting this to 0 means no limit.
43       'network_timeout' => 10,            // The timeout (in seconds) for connect + bind arrempts. This is only supported in PHP >= 5.3.0 with OpenLDAP 2.x
44       'referrals'       => true|false,    // Sets the LDAP_OPT_REFERRALS option. Mostly used in multi-domain Active Directory setups
45   );
46 */
47
48 /**
49  * Model class to access an LDAP directories
50  *
51  * @package    Framework
52  * @subpackage LDAP
53  */
54 class rcube_ldap_generic
55 {
56     const UPDATE_MOD_ADD = 1;
57     const UPDATE_MOD_DELETE = 2;
58     const UPDATE_MOD_REPLACE = 4;
59     const UPDATE_MOD_FULL = 7;
60
61     public $conn;
62     public $vlv_active = false;
63
64     /** private properties */
004f86 65     protected $cache = null;
203323 66     protected $config = array();
TB 67     protected $attributes = array('dn');
68     protected $entries = null;
69     protected $result = null;
70     protected $debug = false;
71     protected $list_page = 1;
72     protected $page_size = 10;
004f86 73     protected $vlv_config = null;
203323 74
TB 75
76     /**
77     * Object constructor
78     *
fae90d 79     * @param array $p LDAP connection properties
203323 80     */
fae90d 81     function __construct($p)
203323 82     {
TB 83         $this->config = $p;
84
85         if (is_array($p['attributes']))
86             $this->attributes = $p['attributes'];
87
88         if (!is_array($p['hosts']) && !empty($p['host']))
89             $this->config['hosts'] = array($p['host']);
90     }
91
92     /**
93      * Activate/deactivate debug mode
94      *
95      * @param boolean $dbg True if LDAP commands should be logged
96      */
97     public function set_debug($dbg = true)
98     {
99         $this->debug = $dbg;
100     }
101
102     /**
103      * Set connection options
104      *
105      * @param mixed $opt Option name as string or hash array with multiple options
106      * @param mixed $val Option value
107      */
108     public function set_config($opt, $val = null)
109     {
110         if (is_array($opt))
111             $this->config = array_merge($this->config, $opt);
112         else
113             $this->config[$opt] = $value;
114     }
115
004f86 116     /**
TB 117      * Enable caching by passing an instance of rcube_cache to be used by this object
118      *
119      * @param object rcube_cache Instance or False to disable caching
120      */
121     public function set_cache($cache_engine)
122     {
123         $this->cache = $cache_engine;
124     }
203323 125
TB 126     /**
127      * Set properties for VLV-based paging
128      *
129      * @param  number $page  Page number to list (starting at 1)
130      * @param  number $size  Number of entries to display on one page
131      */
132     public function set_vlv_page($page, $size = 10)
133     {
134         $this->list_page = $page;
135         $this->page_size = $size;
136     }
137
138     /**
139     * Establish a connection to the LDAP server
140     */
141     public function connect($host = null)
142     {
143         if (!function_exists('ldap_connect')) {
144             rcube::raise_error(array('code' => 100, 'type' => 'ldap',
145                 'file' => __FILE__, 'line' => __LINE__,
146                 'message' => "No ldap support in this installation of PHP"),
147                 true);
148             return false;
149         }
150
151         if (is_resource($this->conn) && $this->config['host'] == $host)
152             return true;
153
154         if (empty($this->config['ldap_version']))
155             $this->config['ldap_version'] = 3;
156
157         // iterate over hosts if none specified
158         if (!$host) {
159             if (!is_array($this->config['hosts']))
160                 $this->config['hosts'] = array($this->config['hosts']);
161
162             foreach ($this->config['hosts'] as $host) {
163                 if ($this->connect($host)) {
164                     return true;
165                 }
166             }
167
168             return false;
169         }
170
171         // open connection to the given $host
172         $host     = rcube_utils::idn_to_ascii(rcube_utils::parse_host($host));
173         $hostname = $host . ($this->config['port'] ? ':'.$this->config['port'] : '');
174
c64bee 175         $this->_debug("C: Connect to $hostname [{$this->config['name']}]");
203323 176
TB 177         if ($lc = @ldap_connect($host, $this->config['port'])) {
178             if ($this->config['use_tls'] === true)
179                 if (!ldap_start_tls($lc))
180                     continue;
181
182             $this->_debug("S: OK");
183
184             ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->config['ldap_version']);
185             $this->config['host'] = $host;
186             $this->conn = $lc;
187
188             if (!empty($this->config['network_timeout']))
189               ldap_set_option($lc, LDAP_OPT_NETWORK_TIMEOUT, $this->config['network_timeout']);
190
191             if (isset($this->config['referrals']))
192                 ldap_set_option($lc, LDAP_OPT_REFERRALS, $this->config['referrals']);
f92ccc 193
AM 194             if (isset($this->config['dereference']))
195                 ldap_set_option($lc, LDAP_OPT_DEREF, $this->config['dereference']);
203323 196         }
TB 197         else {
198             $this->_debug("S: NOT OK");
199         }
200
201         if (!is_resource($this->conn)) {
202             rcube::raise_error(array('code' => 100, 'type' => 'ldap',
203                 'file' => __FILE__, 'line' => __LINE__,
204                 'message' => "Could not connect to any LDAP server, last tried $hostname"),
205                 true);
206             return false;
207         }
208
209         return true;
210     }
211
212     /**
213      * Bind connection with (SASL-) user and password
214      *
215      * @param string $authc Authentication user
216      * @param string $pass  Bind password
217      * @param string $authz Autorization user
218      *
219      * @return boolean True on success, False on error
220      */
221     public function sasl_bind($authc, $pass, $authz=null)
222     {
223         if (!$this->conn) {
224             return false;
225         }
226
227         if (!function_exists('ldap_sasl_bind')) {
228             rcube::raise_error(array('code' => 100, 'type' => 'ldap',
229                 'file' => __FILE__, 'line' => __LINE__,
230                 'message' => "Unable to bind: ldap_sasl_bind() not exists"),
231                 true);
232             return false;
233         }
234
235         if (!empty($authz)) {
236             $authz = 'u:' . $authz;
237         }
238
239         if (!empty($this->config['auth_method'])) {
240             $method = $this->config['auth_method'];
241         }
242         else {
243             $method = 'DIGEST-MD5';
244         }
245
0d9ccf 246         $this->_debug("C: SASL Bind [mech: $method, authc: $authc, authz: $authz, pass: **** [" . strlen($pass) . "]");
203323 247
TB 248         if (ldap_sasl_bind($this->conn, NULL, $pass, $method, NULL, $authc, $authz)) {
249             $this->_debug("S: OK");
250             return true;
251         }
252
253         $this->_debug("S: ".ldap_error($this->conn));
254
255         rcube::raise_error(array(
256             'code' => ldap_errno($this->conn), 'type' => 'ldap',
257             'file' => __FILE__, 'line' => __LINE__,
258             'message' => "SASL Bind failed for authcid=$authc ".ldap_error($this->conn)),
259             true);
260         return false;
261     }
262
263     /**
264      * Bind connection with DN and password
265      *
266      * @param string $dn   Bind DN
267      * @param string $pass Bind password
268      *
269      * @return boolean True on success, False on error
270      */
271     public function bind($dn, $pass)
272     {
273         if (!$this->conn) {
274             return false;
275         }
276
0d9ccf 277         $this->_debug("C: Bind $dn, pass: **** [" . strlen($pass) . "]");
203323 278
TB 279         if (@ldap_bind($this->conn, $dn, $pass)) {
280             $this->_debug("S: OK");
281             return true;
282         }
283
284         $this->_debug("S: ".ldap_error($this->conn));
285
286         rcube::raise_error(array(
287             'code' => ldap_errno($this->conn), 'type' => 'ldap',
288             'file' => __FILE__, 'line' => __LINE__,
289             'message' => "Bind failed for dn=$dn: ".ldap_error($this->conn)),
290             true);
291
292         return false;
293     }
294
295     /**
296      * Close connection to LDAP server
297      */
298     public function close()
299     {
300         if ($this->conn) {
301             $this->_debug("C: Close");
302             ldap_unbind($this->conn);
303             $this->conn = null;
304         }
305     }
306
307     /**
308      * Return the last result set
309      *
310      * @return object rcube_ldap_result Result object
311      */
312     function get_result()
313     {
314         return $this->result;
315     }
316
317     /**
318      * Get a specific LDAP entry, identified by its DN
319      *
320      * @param string $dn Record identifier
321      * @return array     Hash array
322      */
323     function get_entry($dn)
324     {
325         $rec = null;
326
327         if ($this->conn && $dn) {
c64bee 328             $this->_debug("C: Read $dn [(objectclass=*)]");
203323 329
TB 330             if ($ldap_result = @ldap_read($this->conn, $dn, '(objectclass=*)', $this->attributes)) {
331                 $this->_debug("S: OK");
332
333                 if ($entry = ldap_first_entry($this->conn, $ldap_result)) {
334                     $rec = ldap_get_attributes($this->conn, $entry);
335                 }
336             }
337             else {
338                 $this->_debug("S: ".ldap_error($this->conn));
339             }
340
341             if (!empty($rec)) {
342                 $rec['dn'] = $dn; // Add in the dn for the entry.
343             }
344         }
345
346         return $rec;
347     }
348
349     /**
350      * Execute the LDAP search based on the stored credentials
351      *
352      * @param string $base_dn  The base DN to query
353      * @param string $filter   The LDAP filter for search
354      * @param string $scope    The LDAP scope (list|sub|base)
355      * @param array  $attrs    List of entry attributes to read
356      * @param array  $prop     Hash array with query configuration properties:
357      *   - sort: array of sort attributes (has to be in sync with the VLV index)
358      *   - search: search string used for VLV controls
359      * @param boolean $count_only Set to true if only entry count is requested
360      *
361      * @return mixed  rcube_ldap_result object or number of entries (if count_only=true) or false on error
362      */
363     public function search($base_dn, $filter = '', $scope = 'sub', $attrs = array('dn'), $prop = array(), $count_only = false)
364     {
858af7 365         if (!$this->conn) {
AM 366             return false;
367         }
203323 368
858af7 369         if (empty($filter)) {
AM 370             $filter = '(objectclass=*)';
371         }
203323 372
858af7 373         $this->_debug("C: Search $base_dn for $filter");
203323 374
858af7 375         $function = self::scope2func($scope, $ns_function);
203323 376
858af7 377         // find available VLV index for this query
AM 378         if (!$count_only && ($vlv_sort = $this->_find_vlv($base_dn, $filter, $scope, $prop['sort']))) {
379             // when using VLV, we get the total count by...
380             // ...either reading numSubOrdinates attribute
381             if (($sub_filter = $this->config['numsub_filter']) &&
382                 ($result_count = @$ns_function($this->conn, $base_dn, $sub_filter, array('numSubOrdinates'), 0, 0, 0))
203323 383             ) {
858af7 384                 $counts = ldap_get_entries($this->conn, $result_count);
AM 385                 for ($vlv_count = $j = 0; $j < $counts['count']; $j++)
386                     $vlv_count += $counts[$j]['numsubordinates'][0];
387                 $this->_debug("D: total numsubordinates = " . $vlv_count);
203323 388             }
858af7 389             // ...or by fetching all records dn and count them
AM 390             else if (!function_exists('ldap_parse_virtuallist_control')) {
391                 $vlv_count = $this->search($base_dn, $filter, $scope, array('dn'), $prop, true);
203323 392             }
858af7 393
AM 394             $this->vlv_active = $this->_vlv_set_controls($vlv_sort, $this->list_page, $this->page_size, $prop['search']);
395         }
396         else {
397             $this->vlv_active = false;
398         }
399
400         // only fetch dn for count (should keep the payload low)
401         if ($ldap_result = @$function($this->conn, $base_dn, $filter,
402             $attrs, 0, (int)$this->config['sizelimit'], (int)$this->config['timelimit'])
403         ) {
404             // when running on a patched PHP we can use the extended functions
405             // to retrieve the total count from the LDAP search result
406             if ($this->vlv_active && function_exists('ldap_parse_virtuallist_control')) {
407                 if (ldap_parse_result($this->conn, $ldap_result, $errcode, $matcheddn, $errmsg, $referrals, $serverctrls)) {
408                     ldap_parse_virtuallist_control($this->conn, $serverctrls, $last_offset, $vlv_count, $vresult);
409                     $this->_debug("S: VLV result: last_offset=$last_offset; content_count=$vlv_count");
410                 }
411                 else {
412                     $this->_debug("S: ".($errmsg ? $errmsg : ldap_error($this->conn)));
413                 }
414             }
415             else if ($this->debug) {
416                 $this->_debug("S: ".ldap_count_entries($this->conn, $ldap_result)." record(s) found");
417             }
418
419             $this->result = new rcube_ldap_result($this->conn, $ldap_result, $base_dn, $filter, $vlv_count);
420
421             return $count_only ? $this->result->count() : $this->result;
422         }
423         else {
424             $this->_debug("S: ".ldap_error($this->conn));
203323 425         }
TB 426
427         return false;
428     }
429
430     /**
431      * Modify an LDAP entry on the server
432      *
433      * @param string $dn      Entry DN
434      * @param array  $params  Hash array of entry attributes
435      * @param int    $mode    Update mode (UPDATE_MOD_ADD | UPDATE_MOD_DELETE | UPDATE_MOD_REPLACE)
436      */
437     public function modify($dn, $parms, $mode = 255)
438     {
439         // TODO: implement this
440
441         return false;
442     }
443
444     /**
445      * Wrapper for ldap_add()
446      *
447      * @see ldap_add()
448      */
449     public function add($dn, $entry)
450     {
c64bee 451         $this->_debug("C: Add $dn: ".print_r($entry, true));
203323 452
TB 453         $res = ldap_add($this->conn, $dn, $entry);
454         if ($res === false) {
455             $this->_debug("S: ".ldap_error($this->conn));
456             return false;
457         }
458
459         $this->_debug("S: OK");
460         return true;
461     }
462
463     /**
464      * Wrapper for ldap_delete()
465      *
466      * @see ldap_delete()
467      */
468     public function delete($dn)
469     {
c64bee 470         $this->_debug("C: Delete $dn");
203323 471
TB 472         $res = ldap_delete($this->conn, $dn);
473         if ($res === false) {
474             $this->_debug("S: ".ldap_error($this->conn));
475             return false;
476         }
477
478         $this->_debug("S: OK");
479         return true;
480     }
481
482     /**
483      * Wrapper for ldap_mod_replace()
484      *
485      * @see ldap_mod_replace()
486      */
487     public function mod_replace($dn, $entry)
488     {
c64bee 489         $this->_debug("C: Replace $dn: ".print_r($entry, true));
203323 490
TB 491         if (!ldap_mod_replace($this->conn, $dn, $entry)) {
492             $this->_debug("S: ".ldap_error($this->conn));
493             return false;
494         }
495
496         $this->_debug("S: OK");
497         return true;
498     }
499
500     /**
501      * Wrapper for ldap_mod_add()
502      *
503      * @see ldap_mod_add()
504      */
505     public function mod_add($dn, $entry)
506     {
c64bee 507         $this->_debug("C: Add $dn: ".print_r($entry, true));
203323 508
TB 509         if (!ldap_mod_add($this->conn, $dn, $entry)) {
510             $this->_debug("S: ".ldap_error($this->conn));
511             return false;
512         }
513
514         $this->_debug("S: OK");
515         return true;
516     }
517
518     /**
519      * Wrapper for ldap_mod_del()
520      *
521      * @see ldap_mod_del()
522      */
523     public function mod_del($dn, $entry)
524     {
c64bee 525         $this->_debug("C: Delete $dn: ".print_r($entry, true));
203323 526
TB 527         if (!ldap_mod_del($this->conn, $dn, $entry)) {
528             $this->_debug("S: ".ldap_error($this->conn));
529             return false;
530         }
531
532         $this->_debug("S: OK");
533         return true;
534     }
535
536     /**
537      * Wrapper for ldap_rename()
538      *
539      * @see ldap_rename()
540      */
541     public function rename($dn, $newrdn, $newparent = null, $deleteoldrdn = true)
542     {
c64bee 543         $this->_debug("C: Rename $dn to $newrdn");
203323 544
TB 545         if (!ldap_rename($this->conn, $dn, $newrdn, $newparent, $deleteoldrdn)) {
546             $this->_debug("S: ".ldap_error($this->conn));
547             return false;
548         }
549
550         $this->_debug("S: OK");
551         return true;
552     }
553
554     /**
555      * Wrapper for ldap_list() + ldap_get_entries()
556      *
557      * @see ldap_list()
558      * @see ldap_get_entries()
559      */
560     public function list_entries($dn, $filter, $attributes = array('dn'))
561     {
562         $list = array();
c64bee 563         $this->_debug("C: List $dn [{$filter}]");
203323 564
TB 565         if ($result = ldap_list($this->conn, $dn, $filter, $attributes)) {
566             $list = ldap_get_entries($this->conn, $result);
567
568             if ($list === false) {
569                 $this->_debug("S: ".ldap_error($this->conn));
570                 return array();
571             }
572
573             $count = $list['count'];
574             unset($list['count']);
575
576             $this->_debug("S: $count record(s)");
577         }
578         else {
579             $this->_debug("S: ".ldap_error($this->conn));
580         }
581
582         return $list;
583     }
584
585     /**
586      * Wrapper for ldap_read() + ldap_get_entries()
587      *
588      * @see ldap_read()
589      * @see ldap_get_entries()
590      */
591     public function read_entries($dn, $filter, $attributes = null)
592     {
c64bee 593         $this->_debug("C: Read $dn [{$filter}]");
203323 594
TB 595         if ($this->conn && $dn) {
596             if (!$attributes)
597                 $attributes = $this->attributes;
598
599             $result = @ldap_read($this->conn, $dn, $filter, $attributes, 0, (int)$this->config['sizelimit'], (int)$this->config['timelimit']);
600             if ($result === false) {
601                 $this->_debug("S: ".ldap_error($this->conn));
602                 return false;
603             }
604
605             $this->_debug("S: OK");
606             return ldap_get_entries($this->conn, $result);
607         }
608
609         return false;
610     }
611
612     /**
613      * Choose the right PHP function according to scope property
614      *
615      * @param string $scope         The LDAP scope (sub|base|list)
616      * @param string $ns_function   Function to be used for numSubOrdinates queries
617      * @return string  PHP function to be used to query directory
618      */
619     public static function scope2func($scope, &$ns_function = null)
620     {
621         switch ($scope) {
622           case 'sub':
623             $function = $ns_function  = 'ldap_search';
624             break;
625           case 'base':
626             $function = $ns_function = 'ldap_read';
627             break;
628           default:
629             $function = 'ldap_list';
630             $ns_function = 'ldap_read';
631             break;
632         }
633
634         return $function;
635     }
636
637     /**
004f86 638      * Convert the given scope integer value to a string representation
TB 639      */
640     public static function scopeint2str($scope)
641     {
642         switch ($scope) {
643             case 2:  return 'sub';
644             case 1:  return 'one';
645             case 0:  return 'base';
646             default: $this->_debug("Scope $scope is not a valid scope integer");
647         }
648
649         return '';
650     }
651
652     /**
203323 653      * Escapes the given value according to RFC 2254 so that it can be safely used in LDAP filters.
TB 654      *
655      * @param string $val Value to quote
656      * @return string The escaped value
657      */
658     public static function escape_value($val)
659     {
660         return strtr($str, array('*'=>'\2a', '('=>'\28', ')'=>'\29',
661             '\\'=>'\5c', '/'=>'\2f'));
662     }
663
664     /**
665      * Escapes a DN value according to RFC 2253
666      *
667      * @param string $dn DN value o quote
668      * @return string The escaped value
669      */
670     public static function escape_dn($dn)
671     {
672         return strtr($str, array(','=>'\2c', '='=>'\3d', '+'=>'\2b',
673             '<'=>'\3c', '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c',
674             '"'=>'\22', '#'=>'\23'));
675     }
676
677     /**
678      * Normalize a LDAP result by converting entry attributes arrays into single values
679      *
680      * @param array $result LDAP result set fetched with ldap_get_entries()
681      * @return array        Hash array with normalized entries, indexed by their DNs
682      */
683     public static function normalize_result($result)
684     {
685         if (!is_array($result)) {
686             return array();
687         }
688
689         $entries  = array();
690         for ($i = 0; $i < $result['count']; $i++) {
691             $key = $result[$i]['dn'] ? $result[$i]['dn'] : $i;
692             $entries[$key] = self::normalize_entry($result[$i]);
693         }
694
695         return $entries;
696     }
c9ed4b 697
203323 698     /**
TB 699      * Turn an LDAP entry into a regular PHP array with attributes as keys.
700      *
701      * @param array $entry Attributes array as retrieved from ldap_get_attributes() or ldap_get_entries()
a4bc6e 702      *
203323 703      * @return array       Hash array with attributes as keys
TB 704      */
705     public static function normalize_entry($entry)
706     {
a4bc6e 707         if (!isset($entry['count'])) {
AM 708             return $entry;
709         }
710
203323 711         $rec = array();
a4bc6e 712
203323 713         for ($i=0; $i < $entry['count']; $i++) {
TB 714             $attr = $entry[$i];
715             if ($entry[$attr]['count'] == 1) {
716                 switch ($attr) {
717                     case 'objectclass':
718                         $rec[$attr] = array(strtolower($entry[$attr][0]));
719                         break;
720                     default:
721                         $rec[$attr] = $entry[$attr][0];
722                         break;
723                 }
724             }
725             else {
726                 for ($j=0; $j < $entry[$attr]['count']; $j++) {
727                     $rec[$attr][$j] = $entry[$attr][$j];
728                 }
729             }
730         }
731
732         return $rec;
733     }
734
735     /**
736      * Set server controls for Virtual List View (paginated listing)
737      */
004f86 738     private function _vlv_set_controls($sort, $list_page, $page_size, $search = null)
203323 739     {
004f86 740         $sort_ctrl = array('oid' => "1.2.840.113556.1.4.473",  'value' => self::_sort_ber_encode((array)$sort));
203323 741         $vlv_ctrl  = array('oid' => "2.16.840.1.113730.3.4.9", 'value' => self::_vlv_ber_encode(($offset = ($list_page-1) * $page_size + 1), $page_size, $search), 'iscritical' => true);
TB 742
c64bee 743         $this->_debug("C: Set controls sort=" . join(' ', unpack('H'.(strlen($sort_ctrl['value'])*2), $sort_ctrl['value'])) . " ($sort[0]);"
772b73 744             . " vlv=" . join(' ', (unpack('H'.(strlen($vlv_ctrl['value'])*2), $vlv_ctrl['value']))) . " ($offset/$page_size; $search)");
203323 745
TB 746         if (!ldap_set_option($this->conn, LDAP_OPT_SERVER_CONTROLS, array($sort_ctrl, $vlv_ctrl))) {
747             $this->_debug("S: ".ldap_error($this->conn));
748             $this->set_error(self::ERROR_SEARCH, 'vlvnotsupported');
749             return false;
750         }
751
752         return true;
753     }
754
755     /**
756      * Returns unified attribute name (resolving aliases)
757      */
758     private static function _attr_name($namev)
759     {
760         // list of known attribute aliases
761         static $aliases = array(
762             'gn' => 'givenname',
763             'rfc822mailbox' => 'email',
764             'userid' => 'uid',
765             'emailaddress' => 'email',
766             'pkcs9email' => 'email',
767         );
768
769         list($name, $limit) = explode(':', $namev, 2);
770         $suffix = $limit ? ':'.$limit : '';
771
772         return (isset($aliases[$name]) ? $aliases[$name] : $name) . $suffix;
773     }
774
775     /**
776      * Quotes attribute value string
777      *
778      * @param string $str Attribute value
779      * @param bool   $dn  True if the attribute is a DN
780      *
781      * @return string Quoted string
782      */
4500b2 783     public static function quote_string($str, $dn=false)
203323 784     {
TB 785         // take firt entry if array given
786         if (is_array($str))
787             $str = reset($str);
788
789         if ($dn)
790             $replace = array(','=>'\2c', '='=>'\3d', '+'=>'\2b', '<'=>'\3c',
791                 '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c', '"'=>'\22', '#'=>'\23');
792         else
793             $replace = array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c',
794                 '/'=>'\2f');
795
796         return strtr($str, $replace);
797     }
798
799     /**
004f86 800      * Prints debug info to the log
TB 801      */
802     private function _debug($str)
803     {
804         if ($this->debug && class_exists('rcube')) {
805             rcube::write_log('ldap', $str);
806         }
807     }
808
809
810     /*****************  Virtual List View (VLV) related utility functions  **************** */
811
812     /**
813      * Return the search string value to be used in VLV controls
814      */
815     private function _vlv_search($sort, $search)
816     {
817         foreach ($search as $attr => $value) {
818             if (!in_array(strtolower($attr), $sort)) {
819                 $this->_debug("d: Cannot use VLV search using attribute not indexed: $attr (not in " . var_export($sort, true) . ")");
820                 return null;
821             } else {
822                 return $value;
823             }
824         }
825     }
826
827     /**
828      * Find a VLV index matching the given query attributes
829      *
830      * @return string Sort attribute or False if no match
831      */
832     private function _find_vlv($base_dn, $filter, $scope, $sort_attrs = null)
833     {
834         if (!$this->config['vlv'] || $scope == 'base') {
835             return false;
836         }
837
838         // get vlv config
839         $vlv_config = $this->_read_vlv_config();
840
f924f5 841         if ($vlv = $vlv_config[$base_dn]) {
c64bee 842             $this->_debug("D: Found a VLV for $base_dn");
004f86 843
03c73f 844             if ($vlv['filter'] == strtolower($filter) || stripos($filter, '(&'.$vlv['filter'].'(') === 0) {
004f86 845                 $this->_debug("D: Filter matches");
TB 846                 if ($vlv['scope'] == $scope) {
847                     // Not passing any sort attributes means you don't care
848                     if (empty($sort_attrs) || in_array($sort_attrs, $vlv['sort'])) {
f924f5 849                         return $vlv['sort'][0];
004f86 850                     }
TB 851                 }
852                 else {
853                     $this->_debug("D: Scope does not match");
854                 }
855             }
856             else {
857                 $this->_debug("D: Filter does not match");
858             }
859         }
860         else {
c64bee 861             $this->_debug("D: No VLV for $base_dn");
004f86 862         }
TB 863
864         return false;
865     }
866
867     /**
868      * Return VLV indexes and searches including necessary configuration
869      * details.
870      */
871     private function _read_vlv_config()
872     {
873         if (empty($this->config['vlv']) || empty($this->config['config_root_dn'])) {
874             return array();
875         }
876         // return hard-coded VLV config
877         else if (is_array($this->config['vlv'])) {
878             return $this->config['vlv'];
879         }
880
881         // return cached result
882         if (is_array($this->vlv_config)) {
883             return $this->vlv_config;
884         }
c64bee 885
004f86 886         if ($this->cache && ($cached_config = $this->cache->get('vlvconfig'))) {
TB 887             $this->vlv_config = $cached_config;
888             return $this->vlv_config;
889         }
890
891         $this->vlv_config = array();
892
893         $ldap_result = ldap_search($this->conn, $this->config['config_root_dn'], '(objectclass=vlvsearch)', array('*'), 0, 0, 0);
894         $vlv_searches = new rcube_ldap_result($this->conn, $ldap_result, $this->config['config_root_dn'], '(objectclass=vlvsearch)');
895
896         if ($vlv_searches->count() < 1) {
897             $this->_debug("D: Empty result from search for '(objectclass=vlvsearch)' on '$config_root_dn'");
898             return array();
899         }
900
901         foreach ($vlv_searches->entries(true) as $vlv_search_dn => $vlv_search_attrs) {
902             // Multiple indexes may exist
903             $ldap_result = ldap_search($this->conn, $vlv_search_dn, '(objectclass=vlvindex)', array('*'), 0, 0, 0);
904             $vlv_indexes = new rcube_ldap_result($this->conn, $ldap_result, $vlv_search_dn, '(objectclass=vlvindex)');
905
906             // Reset this one for each VLV search.
907             $_vlv_sort = array();
908             foreach ($vlv_indexes->entries(true) as $vlv_index_dn => $vlv_index_attrs) {
909                 $_vlv_sort[] = explode(' ', $vlv_index_attrs['vlvsort']);
910             }
911
912             $this->vlv_config[$vlv_search_attrs['vlvbase']] = array(
913                 'scope'  => self::scopeint2str($vlv_search_attrs['vlvscope']),
f924f5 914                 'filter' => strtolower($vlv_search_attrs['vlvfilter']),
004f86 915                 'sort'   => $_vlv_sort,
TB 916             );
917         }
918
919         // cache this
920         if ($this->cache)
921             $this->cache->set('vlvconfig', $this->vlv_config);
922
923         $this->_debug("D: Refreshed VLV config: " . var_export($this->vlv_config, true));
f924f5 924
TB 925         return $this->vlv_config;
004f86 926     }
TB 927
928     /**
203323 929      * Generate BER encoded string for Virtual List View option
TB 930      *
931      * @param integer List offset (first record)
932      * @param integer Records per page
c9ed4b 933      *
203323 934      * @return string BER encoded option value
TB 935      */
936     private static function _vlv_ber_encode($offset, $rpp, $search = '')
937     {
c9ed4b 938         /*
AM 939             this string is ber-encoded, php will prefix this value with:
940             04 (octet string) and 10 (length of 16 bytes)
941             the code behind this string is broken down as follows:
942             30 = ber sequence with a length of 0e (14) bytes following
943             02 = type integer (in two's complement form) with 2 bytes following (beforeCount): 01 00 (ie 0)
944             02 = type integer (in two's complement form) with 2 bytes following (afterCount):  01 18 (ie 25-1=24)
945             a0 = type context-specific/constructed with a length of 06 (6) bytes following
946             02 = type integer with 2 bytes following (offset): 01 01 (ie 1)
947             02 = type integer with 2 bytes following (contentCount):  01 00
948
949             with a search string present:
950             81 = type context-specific/constructed with a length of 04 (4) bytes following (the length will change here)
951             81 indicates a user string is present where as a a0 indicates just a offset search
952             81 = type context-specific/constructed with a length of 06 (6) bytes following
953
954             The following info was taken from the ISO/IEC 8825-1:2003 x.690 standard re: the
955             encoding of integer values (note: these values are in
956             two-complement form so since offset will never be negative bit 8 of the
957             leftmost octet should never by set to 1):
958             8.3.2: If the contents octets of an integer value encoding consist
959             of more than one octet, then the bits of the first octet (rightmost)
960             and bit 8 of the second (to the left of first octet) octet:
961                 a) shall not all be ones; and
962                 b) shall not all be zero
963         */
964
965         if ($search) {
203323 966             $search = preg_replace('/[^-[:alpha:] ,.()0-9]+/', '', $search);
TB 967             $ber_val = self::_string2hex($search);
968             $str = self::_ber_addseq($ber_val, '81');
969         }
c9ed4b 970         else {
AM 971             // construct the string from right to left
203323 972             $str = "020100"; # contentCount
TB 973
974             $ber_val = self::_ber_encode_int($offset);  // returns encoded integer value in hex format
975
976             // calculate octet length of $ber_val
977             $str = self::_ber_addseq($ber_val, '02') . $str;
978
979             // now compute length over $str
980             $str = self::_ber_addseq($str, 'a0');
981         }
c9ed4b 982
203323 983         // now tack on records per page
TB 984         $str = "020100" . self::_ber_addseq(self::_ber_encode_int($rpp-1), '02') . $str;
985
986         // now tack on sequence identifier and length
987         $str = self::_ber_addseq($str, '30');
988
989         return pack('H'.strlen($str), $str);
990     }
991
992     /**
993      * create ber encoding for sort control
994      *
995      * @param array List of cols to sort by
996      * @return string BER encoded option value
997      */
998     private static function _sort_ber_encode($sortcols)
999     {
1000         $str = '';
1001         foreach (array_reverse((array)$sortcols) as $col) {
1002             $ber_val = self::_string2hex($col);
1003
c9ed4b 1004             // 30 = ber sequence with a length of octet value
AM 1005             // 04 = octet string with a length of the ascii value
203323 1006             $oct = self::_ber_addseq($ber_val, '04');
TB 1007             $str = self::_ber_addseq($oct, '30') . $str;
1008         }
1009
1010         // now tack on sequence identifier and length
1011         $str = self::_ber_addseq($str, '30');
1012
1013         return pack('H'.strlen($str), $str);
1014     }
1015
1016     /**
1017      * Add BER sequence with correct length and the given identifier
1018      */
1019     private static function _ber_addseq($str, $identifier)
1020     {
1021         $len = dechex(strlen($str)/2);
1022         if (strlen($len) % 2 != 0)
1023             $len = '0'.$len;
1024
1025         return $identifier . $len . $str;
1026     }
1027
1028     /**
1029      * Returns BER encoded integer value in hex format
1030      */
1031     private static function _ber_encode_int($offset)
1032     {
1033         $val = dechex($offset);
1034         $prefix = '';
1035
1036         // check if bit 8 of high byte is 1
1037         if (preg_match('/^[89abcdef]/', $val))
1038             $prefix = '00';
1039
1040         if (strlen($val)%2 != 0)
1041             $prefix .= '0';
1042
1043         return $prefix . $val;
1044     }
1045
1046     /**
1047      * Returns ascii string encoded in hex
1048      */
1049     private static function _string2hex($str)
1050     {
1051         $hex = '';
c9ed4b 1052         for ($i=0; $i < strlen($str); $i++) {
203323 1053             $hex .= dechex(ord($str[$i]));
c9ed4b 1054         }
203323 1055         return $hex;
TB 1056     }
1057
1058 }