PageRenderTime 64ms CodeModel.GetById 23ms RepoModel.GetById 1ms app.codeStats 0ms

/program/lib/Roundcube/rcube_ldap_generic.php

https://github.com/fretelweb/roundcubemail
PHP | 1049 lines | 611 code | 144 blank | 294 comment | 104 complexity | b976e49d70337de337fe868ed28f053e 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 ($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: $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: $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. * @return array Hash array with attributes as keys
  597. */
  598. public static function normalize_entry($entry)
  599. {
  600. $rec = array();
  601. for ($i=0; $i < $entry['count']; $i++) {
  602. $attr = $entry[$i];
  603. if ($entry[$attr]['count'] == 1) {
  604. switch ($attr) {
  605. case 'objectclass':
  606. $rec[$attr] = array(strtolower($entry[$attr][0]));
  607. break;
  608. default:
  609. $rec[$attr] = $entry[$attr][0];
  610. break;
  611. }
  612. }
  613. else {
  614. for ($j=0; $j < $entry[$attr]['count']; $j++) {
  615. $rec[$attr][$j] = $entry[$attr][$j];
  616. }
  617. }
  618. }
  619. return $rec;
  620. }
  621. /**
  622. * Set server controls for Virtual List View (paginated listing)
  623. */
  624. private function _vlv_set_controls($sort, $list_page, $page_size, $search = null)
  625. {
  626. $sort_ctrl = array('oid' => "1.2.840.113556.1.4.473", 'value' => self::_sort_ber_encode((array)$sort));
  627. $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);
  628. $this->_debug("C: Set controls sort=" . join(' ', unpack('H'.(strlen($sort_ctrl['value'])*2), $sort_ctrl['value'])) . " ($sort[0]);"
  629. . " vlv=" . join(' ', (unpack('H'.(strlen($vlv_ctrl['value'])*2), $vlv_ctrl['value']))) . " ($offset/$page_size; $search)");
  630. if (!ldap_set_option($this->conn, LDAP_OPT_SERVER_CONTROLS, array($sort_ctrl, $vlv_ctrl))) {
  631. $this->_debug("S: ".ldap_error($this->conn));
  632. $this->set_error(self::ERROR_SEARCH, 'vlvnotsupported');
  633. return false;
  634. }
  635. return true;
  636. }
  637. /**
  638. * Returns unified attribute name (resolving aliases)
  639. */
  640. private static function _attr_name($namev)
  641. {
  642. // list of known attribute aliases
  643. static $aliases = array(
  644. 'gn' => 'givenname',
  645. 'rfc822mailbox' => 'email',
  646. 'userid' => 'uid',
  647. 'emailaddress' => 'email',
  648. 'pkcs9email' => 'email',
  649. );
  650. list($name, $limit) = explode(':', $namev, 2);
  651. $suffix = $limit ? ':'.$limit : '';
  652. return (isset($aliases[$name]) ? $aliases[$name] : $name) . $suffix;
  653. }
  654. /**
  655. * Quotes attribute value string
  656. *
  657. * @param string $str Attribute value
  658. * @param bool $dn True if the attribute is a DN
  659. *
  660. * @return string Quoted string
  661. */
  662. public static function quote_string($str, $dn=false)
  663. {
  664. // take firt entry if array given
  665. if (is_array($str))
  666. $str = reset($str);
  667. if ($dn)
  668. $replace = array(','=>'\2c', '='=>'\3d', '+'=>'\2b', '<'=>'\3c',
  669. '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c', '"'=>'\22', '#'=>'\23');
  670. else
  671. $replace = array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c',
  672. '/'=>'\2f');
  673. return strtr($str, $replace);
  674. }
  675. /**
  676. * Prints debug info to the log
  677. */
  678. private function _debug($str)
  679. {
  680. if ($this->debug && class_exists('rcube')) {
  681. rcube::write_log('ldap', $str);
  682. }
  683. }
  684. /***************** Virtual List View (VLV) related utility functions **************** */
  685. /**
  686. * Return the search string value to be used in VLV controls
  687. */
  688. private function _vlv_search($sort, $search)
  689. {
  690. foreach ($search as $attr => $value) {
  691. if (!in_array(strtolower($attr), $sort)) {
  692. $this->_debug("d: Cannot use VLV search using attribute not indexed: $attr (not in " . var_export($sort, true) . ")");
  693. return null;
  694. } else {
  695. return $value;
  696. }
  697. }
  698. }
  699. /**
  700. * Find a VLV index matching the given query attributes
  701. *
  702. * @return string Sort attribute or False if no match
  703. */
  704. private function _find_vlv($base_dn, $filter, $scope, $sort_attrs = null)
  705. {
  706. if (!$this->config['vlv'] || $scope == 'base') {
  707. return false;
  708. }
  709. // get vlv config
  710. $vlv_config = $this->_read_vlv_config();
  711. if ($vlv = $vlv_config[$base_dn]) {
  712. $this->_debug("D: Found a VLV for $base_dn");
  713. if ($vlv['filter'] == strtolower($filter) || stripos($filter, '(&'.$vlv['filter'].'(') === 0) {
  714. $this->_debug("D: Filter matches");
  715. if ($vlv['scope'] == $scope) {
  716. // Not passing any sort attributes means you don't care
  717. if (empty($sort_attrs) || in_array($sort_attrs, $vlv['sort'])) {
  718. return $vlv['sort'][0];
  719. }
  720. }
  721. else {
  722. $this->_debug("D: Scope does not match");
  723. }
  724. }
  725. else {
  726. $this->_debug("D: Filter does not match");
  727. }
  728. }
  729. else {
  730. $this->_debug("D: No VLV for $base_dn");
  731. }
  732. return false;
  733. }
  734. /**
  735. * Return VLV indexes and searches including necessary configuration
  736. * details.
  737. */
  738. private function _read_vlv_config()
  739. {
  740. if (empty($this->config['vlv']) || empty($this->config['config_root_dn'])) {
  741. return array();
  742. }
  743. // return hard-coded VLV config
  744. else if (is_array($this->config['vlv'])) {
  745. return $this->config['vlv'];
  746. }
  747. // return cached result
  748. if (is_array($this->vlv_config)) {
  749. return $this->vlv_config;
  750. }
  751. if ($this->cache && ($cached_config = $this->cache->get('vlvconfig'))) {
  752. $this->vlv_config = $cached_config;
  753. return $this->vlv_config;
  754. }
  755. $this->vlv_config = array();
  756. $ldap_result = ldap_search($this->conn, $this->config['config_root_dn'], '(objectclass=vlvsearch)', array('*'), 0, 0, 0);
  757. $vlv_searches = new rcube_ldap_result($this->conn, $ldap_result, $this->config['config_root_dn'], '(objectclass=vlvsearch)');
  758. if ($vlv_searches->count() < 1) {
  759. $this->_debug("D: Empty result from search for '(objectclass=vlvsearch)' on '$config_root_dn'");
  760. return array();
  761. }
  762. foreach ($vlv_searches->entries(true) as $vlv_search_dn => $vlv_search_attrs) {
  763. // Multiple indexes may exist
  764. $ldap_result = ldap_search($this->conn, $vlv_search_dn, '(objectclass=vlvindex)', array('*'), 0, 0, 0);
  765. $vlv_indexes = new rcube_ldap_result($this->conn, $ldap_result, $vlv_search_dn, '(objectclass=vlvindex)');
  766. // Reset this one for each VLV search.
  767. $_vlv_sort = array();
  768. foreach ($vlv_indexes->entries(true) as $vlv_index_dn => $vlv_index_attrs) {
  769. $_vlv_sort[] = explode(' ', $vlv_index_attrs['vlvsort']);
  770. }
  771. $this->vlv_config[$vlv_search_attrs['vlvbase']] = array(
  772. 'scope' => self::scopeint2str($vlv_search_attrs['vlvscope']),
  773. 'filter' => strtolower($vlv_search_attrs['vlvfilter']),
  774. 'sort' => $_vlv_sort,
  775. );
  776. }
  777. // cache this
  778. if ($this->cache)
  779. $this->cache->set('vlvconfig', $this->vlv_config);
  780. $this->_debug("D: Refreshed VLV config: " . var_export($this->vlv_config, true));
  781. return $this->vlv_config;
  782. }
  783. /**
  784. * Generate BER encoded string for Virtual List View option
  785. *
  786. * @param integer List offset (first record)
  787. * @param integer Records per page
  788. *
  789. * @return string BER encoded option value
  790. */
  791. private static function _vlv_ber_encode($offset, $rpp, $search = '')
  792. {
  793. /*
  794. this string is ber-encoded, php will prefix this value with:
  795. 04 (octet string) and 10 (length of 16 bytes)
  796. the code behind this string is broken down as follows:
  797. 30 = ber sequence with a length of 0e (14) bytes following
  798. 02 = type integer (in two's complement form) with 2 bytes following (beforeCount): 01 00 (ie 0)
  799. 02 = type integer (in two's complement form) with 2 bytes following (afterCount): 01 18 (ie 25-1=24)
  800. a0 = type context-specific/constructed with a length of 06 (6) bytes following
  801. 02 = type integer with 2 bytes following (offset): 01 01 (ie 1)
  802. 02 = type integer with 2 bytes following (contentCount): 01 00
  803. with a search string present:
  804. 81 = type context-specific/constructed with a length of 04 (4) bytes following (the length will change here)
  805. 81 indicates a user string is present where as a a0 indicates just a offset search
  806. 81 = type context-specific/constructed with a length of 06 (6) bytes following
  807. The following info was taken from the ISO/IEC 8825-1:2003 x.690 standard re: the
  808. encoding of integer values (note: these values are in
  809. two-complement form so since offset will never be negative bit 8 of the
  810. leftmost octet should never by set to 1):
  811. 8.3.2: If the contents octets of an integer value encoding consist
  812. of more than one octet, then the bits of the first octet (rightmost)
  813. and bit 8 of the second (to the left of first octet) octet:
  814. a) shall not all be ones; and
  815. b) shall not all be zero
  816. */
  817. if ($search) {
  818. $search = preg_replace('/[^-[:alpha:] ,.()0-9]+/', '', $search);
  819. $ber_val = self::_string2hex($search);
  820. $str = self::_ber_addseq($ber_val, '81');
  821. }
  822. else {
  823. // construct the string from right to left
  824. $str = "020100"; # contentCount
  825. $ber_val = self::_ber_encode_int($offset); // returns encoded integer value in hex format
  826. // calculate octet length of $ber_val
  827. $str = self::_ber_addseq($ber_val, '02') . $str;
  828. // now compute length over $str
  829. $str = self::_ber_addseq($str, 'a0');
  830. }
  831. // now tack on records per page
  832. $str = "020100" . self::_ber_addseq(self::_ber_encode_int($rpp-1), '02') . $str;
  833. // now tack on sequence identifier and length
  834. $str = self::_ber_addseq($str, '30');
  835. return pack('H'.strlen($str), $str);
  836. }
  837. /**
  838. * create ber encoding for sort control
  839. *
  840. * @param array List of cols to sort by
  841. * @return string BER encoded option value
  842. */
  843. private static function _sort_ber_encode($sortcols)
  844. {
  845. $str = '';
  846. foreach (array_reverse((array)$sortcols) as $col) {
  847. $ber_val = self::_string2hex($col);
  848. // 30 = ber sequence with a length of octet value
  849. // 04 = octet string with a length of the ascii value
  850. $oct = self::_ber_addseq($ber_val, '04');
  851. $str = self::_ber_addseq($oct, '30') . $str;
  852. }
  853. // now tack on sequence identifier and length
  854. $str = self::_ber_addseq($str, '30');
  855. return pack('H'.strlen($str), $str);
  856. }
  857. /**
  858. * Add BER sequence with correct length and the given identifier
  859. */
  860. private static function _ber_addseq($str, $identifier)
  861. {
  862. $len = dechex(strlen($str)/2);
  863. if (strlen($len) % 2 != 0)
  864. $len = '0'.$len;
  865. return $identifier . $len . $str;
  866. }
  867. /**
  868. * Returns BER encoded integer value in hex format
  869. */
  870. private static function _ber_encode_int($offset)
  871. {
  872. $val = dechex($offset);
  873. $prefix = '';
  874. // check if bit 8 of high byte is 1
  875. if (preg_match('/^[89abcdef]/', $val))
  876. $prefix = '00';
  877. if (strlen($val)%2 != 0)
  878. $prefix .= '0';
  879. return $prefix . $val;
  880. }
  881. /**
  882. * Returns ascii string encoded in hex
  883. */
  884. private static function _string2hex($str)
  885. {
  886. $hex = '';
  887. for ($i=0; $i < strlen($str); $i++) {
  888. $hex .= dechex(ord($str[$i]));
  889. }
  890. return $hex;
  891. }
  892. }