PageRenderTime 89ms CodeModel.GetById 17ms RepoModel.GetById 2ms app.codeStats 0ms

/program/lib/Roundcube/rcube_ldap_generic.php

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