PageRenderTime 66ms CodeModel.GetById 29ms RepoModel.GetById 0ms app.codeStats 1ms

/program/lib/Roundcube/rcube_ldap_generic.php

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