PageRenderTime 64ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/roundcubemail-0.7.2-dep/program/include/rcube_ldap.php

#
PHP | 1966 lines | 1301 code | 272 blank | 393 comment | 239 complexity | 98782988ecc660cc9ff150a45e90f185 MD5 | raw file
Possible License(s): GPL-2.0, LGPL-2.1

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. /*
  3. +-----------------------------------------------------------------------+
  4. | program/include/rcube_ldap.php |
  5. | |
  6. | This file is part of the Roundcube Webmail client |
  7. | Copyright (C) 2006-2011, The Roundcube Dev Team |
  8. | Copyright (C) 2011, Kolab Systems AG |
  9. | Licensed under the GNU GPL |
  10. | |
  11. | PURPOSE: |
  12. | Interface to an LDAP address directory |
  13. | |
  14. +-----------------------------------------------------------------------+
  15. | Author: Thomas Bruederli <roundcube@gmail.com> |
  16. | Andreas Dick <andudi (at) gmx (dot) ch> |
  17. | Aleksander Machniak <machniak@kolabsys.com> |
  18. +-----------------------------------------------------------------------+
  19. $Id: rcube_ldap.php 5879 2012-02-15 08:29:33Z thomasb $
  20. */
  21. /**
  22. * Model class to access an LDAP address directory
  23. *
  24. * @package Addressbook
  25. */
  26. class rcube_ldap extends rcube_addressbook
  27. {
  28. /** public properties */
  29. public $primary_key = 'ID';
  30. public $groups = false;
  31. public $readonly = true;
  32. public $ready = false;
  33. public $group_id = 0;
  34. public $coltypes = array();
  35. /** private properties */
  36. protected $conn;
  37. protected $prop = array();
  38. protected $fieldmap = array();
  39. protected $filter = '';
  40. protected $result = null;
  41. protected $ldap_result = null;
  42. protected $mail_domain = '';
  43. protected $debug = false;
  44. private $base_dn = '';
  45. private $groups_base_dn = '';
  46. private $group_url = null;
  47. private $cache;
  48. private $vlv_active = false;
  49. private $vlv_count = 0;
  50. /**
  51. * Object constructor
  52. *
  53. * @param array LDAP connection properties
  54. * @param boolean Enables debug mode
  55. * @param string Current user mail domain name
  56. * @param integer User-ID
  57. */
  58. function __construct($p, $debug=false, $mail_domain=NULL)
  59. {
  60. $this->prop = $p;
  61. if (isset($p['searchonly']))
  62. $this->searchonly = $p['searchonly'];
  63. // check if groups are configured
  64. if (is_array($p['groups']) && count($p['groups'])) {
  65. $this->groups = true;
  66. // set member field
  67. if (!empty($p['groups']['member_attr']))
  68. $this->prop['member_attr'] = strtolower($p['groups']['member_attr']);
  69. else if (empty($p['member_attr']))
  70. $this->prop['member_attr'] = 'member';
  71. // set default name attribute to cn
  72. if (empty($this->prop['groups']['name_attr']))
  73. $this->prop['groups']['name_attr'] = 'cn';
  74. if (empty($this->prop['groups']['scope']))
  75. $this->prop['groups']['scope'] = 'sub';
  76. }
  77. // fieldmap property is given
  78. if (is_array($p['fieldmap'])) {
  79. foreach ($p['fieldmap'] as $rf => $lf)
  80. $this->fieldmap[$rf] = $this->_attr_name(strtolower($lf));
  81. }
  82. else {
  83. // read deprecated *_field properties to remain backwards compatible
  84. foreach ($p as $prop => $value)
  85. if (preg_match('/^(.+)_field$/', $prop, $matches))
  86. $this->fieldmap[$matches[1]] = $this->_attr_name(strtolower($value));
  87. }
  88. // use fieldmap to advertise supported coltypes to the application
  89. foreach ($this->fieldmap as $col => $lf) {
  90. list($col, $type) = explode(':', $col);
  91. if (!is_array($this->coltypes[$col])) {
  92. $subtypes = $type ? array($type) : null;
  93. $this->coltypes[$col] = array('limit' => 1, 'subtypes' => $subtypes);
  94. }
  95. elseif ($type) {
  96. $this->coltypes[$col]['subtypes'][] = $type;
  97. $this->coltypes[$col]['limit']++;
  98. }
  99. if ($type && !$this->fieldmap[$col])
  100. $this->fieldmap[$col] = $lf;
  101. }
  102. if ($this->fieldmap['street'] && $this->fieldmap['locality']) {
  103. $this->coltypes['address'] = array('limit' => max(1, $this->coltypes['locality']['limit']), 'subtypes' => $this->coltypes['locality']['subtypes'], 'childs' => array());
  104. foreach (array('street','locality','zipcode','region','country') as $childcol) {
  105. if ($this->fieldmap[$childcol]) {
  106. $this->coltypes['address']['childs'][$childcol] = array('type' => 'text');
  107. unset($this->coltypes[$childcol]); // remove address child col from global coltypes list
  108. }
  109. }
  110. }
  111. else if ($this->coltypes['address'])
  112. $this->coltypes['address'] = array('type' => 'textarea', 'childs' => null, 'limit' => 1, 'size' => 40);
  113. // make sure 'required_fields' is an array
  114. if (!is_array($this->prop['required_fields']))
  115. $this->prop['required_fields'] = (array) $this->prop['required_fields'];
  116. foreach ($this->prop['required_fields'] as $key => $val)
  117. $this->prop['required_fields'][$key] = $this->_attr_name(strtolower($val));
  118. $this->sort_col = is_array($p['sort']) ? $p['sort'][0] : $p['sort'];
  119. $this->debug = $debug;
  120. $this->mail_domain = $mail_domain;
  121. // initialize cache
  122. $rcmail = rcmail::get_instance();
  123. $this->cache = $rcmail->get_cache('LDAP.' . asciiwords($this->prop['name']), 'db', 600);
  124. $this->_connect();
  125. }
  126. /**
  127. * Establish a connection to the LDAP server
  128. */
  129. private function _connect()
  130. {
  131. global $RCMAIL;
  132. if (!function_exists('ldap_connect'))
  133. raise_error(array('code' => 100, 'type' => 'ldap',
  134. 'file' => __FILE__, 'line' => __LINE__,
  135. 'message' => "No ldap support in this installation of PHP"),
  136. true, true);
  137. if (is_resource($this->conn))
  138. return true;
  139. if (!is_array($this->prop['hosts']))
  140. $this->prop['hosts'] = array($this->prop['hosts']);
  141. if (empty($this->prop['ldap_version']))
  142. $this->prop['ldap_version'] = 3;
  143. foreach ($this->prop['hosts'] as $host)
  144. {
  145. $host = idn_to_ascii(rcube_parse_host($host));
  146. $hostname = $host.($this->prop['port'] ? ':'.$this->prop['port'] : '');
  147. $this->_debug("C: Connect [$hostname]");
  148. if ($lc = @ldap_connect($host, $this->prop['port']))
  149. {
  150. if ($this->prop['use_tls'] === true)
  151. if (!ldap_start_tls($lc))
  152. continue;
  153. $this->_debug("S: OK");
  154. ldap_set_option($lc, LDAP_OPT_PROTOCOL_VERSION, $this->prop['ldap_version']);
  155. $this->prop['host'] = $host;
  156. $this->conn = $lc;
  157. if (isset($this->prop['referrals']))
  158. ldap_set_option($lc, LDAP_OPT_REFERRALS, $this->prop['referrals']);
  159. break;
  160. }
  161. $this->_debug("S: NOT OK");
  162. }
  163. // See if the directory is writeable.
  164. if ($this->prop['writable']) {
  165. $this->readonly = false;
  166. }
  167. if (!is_resource($this->conn)) {
  168. raise_error(array('code' => 100, 'type' => 'ldap',
  169. 'file' => __FILE__, 'line' => __LINE__,
  170. 'message' => "Could not connect to any LDAP server, last tried $hostname"), true);
  171. return false;
  172. }
  173. $bind_pass = $this->prop['bind_pass'];
  174. $bind_user = $this->prop['bind_user'];
  175. $bind_dn = $this->prop['bind_dn'];
  176. $this->base_dn = $this->prop['base_dn'];
  177. $this->groups_base_dn = ($this->prop['groups']['base_dn']) ?
  178. $this->prop['groups']['base_dn'] : $this->base_dn;
  179. // User specific access, generate the proper values to use.
  180. if ($this->prop['user_specific']) {
  181. // No password set, use the session password
  182. if (empty($bind_pass)) {
  183. $bind_pass = $RCMAIL->decrypt($_SESSION['password']);
  184. }
  185. // Get the pieces needed for variable replacement.
  186. if ($fu = $RCMAIL->user->get_username())
  187. list($u, $d) = explode('@', $fu);
  188. else
  189. $d = $this->mail_domain;
  190. $dc = 'dc='.strtr($d, array('.' => ',dc=')); // hierarchal domain string
  191. $replaces = array('%dn' => '', '%dc' => $dc, '%d' => $d, '%fu' => $fu, '%u' => $u);
  192. if ($this->prop['search_base_dn'] && $this->prop['search_filter']) {
  193. if (!empty($this->prop['search_bind_dn']) && !empty($this->prop['search_bind_pw'])) {
  194. $this->bind($this->prop['search_bind_dn'], $this->prop['search_bind_pw']);
  195. }
  196. // Search for the dn to use to authenticate
  197. $this->prop['search_base_dn'] = strtr($this->prop['search_base_dn'], $replaces);
  198. $this->prop['search_filter'] = strtr($this->prop['search_filter'], $replaces);
  199. $this->_debug("S: searching with base {$this->prop['search_base_dn']} for {$this->prop['search_filter']}");
  200. $res = @ldap_search($this->conn, $this->prop['search_base_dn'], $this->prop['search_filter'], array('uid'));
  201. if ($res) {
  202. if (($entry = ldap_first_entry($this->conn, $res))
  203. && ($bind_dn = ldap_get_dn($this->conn, $entry))
  204. ) {
  205. $this->_debug("S: search returned dn: $bind_dn");
  206. $dn = ldap_explode_dn($bind_dn, 1);
  207. $replaces['%dn'] = $dn[0];
  208. }
  209. }
  210. else {
  211. $this->_debug("S: ".ldap_error($this->conn));
  212. }
  213. // DN not found
  214. if (empty($replaces['%dn'])) {
  215. if (!empty($this->prop['search_dn_default']))
  216. $replaces['%dn'] = $this->prop['search_dn_default'];
  217. else {
  218. raise_error(array(
  219. 'code' => 100, 'type' => 'ldap',
  220. 'file' => __FILE__, 'line' => __LINE__,
  221. 'message' => "DN not found using LDAP search."), true);
  222. return false;
  223. }
  224. }
  225. }
  226. // Replace the bind_dn and base_dn variables.
  227. $bind_dn = strtr($bind_dn, $replaces);
  228. $this->base_dn = strtr($this->base_dn, $replaces);
  229. $this->groups_base_dn = strtr($this->groups_base_dn, $replaces);
  230. if (empty($bind_user)) {
  231. $bind_user = $u;
  232. }
  233. }
  234. if (empty($bind_pass)) {
  235. $this->ready = true;
  236. }
  237. else {
  238. if (!empty($bind_dn)) {
  239. $this->ready = $this->bind($bind_dn, $bind_pass);
  240. }
  241. else if (!empty($this->prop['auth_cid'])) {
  242. $this->ready = $this->sasl_bind($this->prop['auth_cid'], $bind_pass, $bind_user);
  243. }
  244. else {
  245. $this->ready = $this->sasl_bind($bind_user, $bind_pass);
  246. }
  247. }
  248. return $this->ready;
  249. }
  250. /**
  251. * Bind connection with (SASL-) user and password
  252. *
  253. * @param string $authc Authentication user
  254. * @param string $pass Bind password
  255. * @param string $authz Autorization user
  256. *
  257. * @return boolean True on success, False on error
  258. */
  259. public function sasl_bind($authc, $pass, $authz=null)
  260. {
  261. if (!$this->conn) {
  262. return false;
  263. }
  264. if (!function_exists('ldap_sasl_bind')) {
  265. raise_error(array('code' => 100, 'type' => 'ldap',
  266. 'file' => __FILE__, 'line' => __LINE__,
  267. 'message' => "Unable to bind: ldap_sasl_bind() not exists"),
  268. true, true);
  269. }
  270. if (!empty($authz)) {
  271. $authz = 'u:' . $authz;
  272. }
  273. if (!empty($this->prop['auth_method'])) {
  274. $method = $this->prop['auth_method'];
  275. }
  276. else {
  277. $method = 'DIGEST-MD5';
  278. }
  279. $this->_debug("C: Bind [mech: $method, authc: $authc, authz: $authz] [pass: $pass]");
  280. if (ldap_sasl_bind($this->conn, NULL, $pass, $method, NULL, $authc, $authz)) {
  281. $this->_debug("S: OK");
  282. return true;
  283. }
  284. $this->_debug("S: ".ldap_error($this->conn));
  285. raise_error(array(
  286. 'code' => ldap_errno($this->conn), 'type' => 'ldap',
  287. 'file' => __FILE__, 'line' => __LINE__,
  288. 'message' => "Bind failed for authcid=$authc ".ldap_error($this->conn)),
  289. true);
  290. return false;
  291. }
  292. /**
  293. * Bind connection with DN and password
  294. *
  295. * @param string Bind DN
  296. * @param string Bind password
  297. *
  298. * @return boolean True on success, False on error
  299. */
  300. public function bind($dn, $pass)
  301. {
  302. if (!$this->conn) {
  303. return false;
  304. }
  305. $this->_debug("C: Bind [dn: $dn] [pass: $pass]");
  306. if (@ldap_bind($this->conn, $dn, $pass)) {
  307. $this->_debug("S: OK");
  308. return true;
  309. }
  310. $this->_debug("S: ".ldap_error($this->conn));
  311. raise_error(array(
  312. 'code' => ldap_errno($this->conn), 'type' => 'ldap',
  313. 'file' => __FILE__, 'line' => __LINE__,
  314. 'message' => "Bind failed for dn=$dn: ".ldap_error($this->conn)),
  315. true);
  316. return false;
  317. }
  318. /**
  319. * Close connection to LDAP server
  320. */
  321. function close()
  322. {
  323. if ($this->conn)
  324. {
  325. $this->_debug("C: Close");
  326. ldap_unbind($this->conn);
  327. $this->conn = null;
  328. }
  329. }
  330. /**
  331. * Returns address book name
  332. *
  333. * @return string Address book name
  334. */
  335. function get_name()
  336. {
  337. return $this->prop['name'];
  338. }
  339. /**
  340. * Set internal sort settings
  341. *
  342. * @param string $sort_col Sort column
  343. * @param string $sort_order Sort order
  344. */
  345. function set_sort_order($sort_col, $sort_order = null)
  346. {
  347. if ($this->fieldmap[$sort_col])
  348. $this->sort_col = $this->fieldmap[$sort_col];
  349. }
  350. /**
  351. * Save a search string for future listings
  352. *
  353. * @param string $filter Filter string
  354. */
  355. function set_search_set($filter)
  356. {
  357. $this->filter = $filter;
  358. }
  359. /**
  360. * Getter for saved search properties
  361. *
  362. * @return mixed Search properties used by this class
  363. */
  364. function get_search_set()
  365. {
  366. return $this->filter;
  367. }
  368. /**
  369. * Reset all saved results and search parameters
  370. */
  371. function reset()
  372. {
  373. $this->result = null;
  374. $this->ldap_result = null;
  375. $this->filter = '';
  376. }
  377. /**
  378. * List the current set of contact records
  379. *
  380. * @param array List of cols to show
  381. * @param int Only return this number of records
  382. *
  383. * @return array Indexed list of contact records, each a hash array
  384. */
  385. function list_records($cols=null, $subset=0)
  386. {
  387. if ($this->prop['searchonly'] && empty($this->filter) && !$this->group_id)
  388. {
  389. $this->result = new rcube_result_set(0);
  390. $this->result->searchonly = true;
  391. return $this->result;
  392. }
  393. // fetch group members recursively
  394. if ($this->group_id && $this->group_data['dn'])
  395. {
  396. $entries = $this->list_group_members($this->group_data['dn']);
  397. // make list of entries unique and sort it
  398. $seen = array();
  399. foreach ($entries as $i => $rec) {
  400. if ($seen[$rec['dn']]++)
  401. unset($entries[$i]);
  402. }
  403. usort($entries, array($this, '_entry_sort_cmp'));
  404. $entries['count'] = count($entries);
  405. $this->result = new rcube_result_set($entries['count'], ($this->list_page-1) * $this->page_size);
  406. }
  407. else
  408. {
  409. // add general filter to query
  410. if (!empty($this->prop['filter']) && empty($this->filter))
  411. $this->set_search_set($this->prop['filter']);
  412. // exec LDAP search if no result resource is stored
  413. if ($this->conn && !$this->ldap_result)
  414. $this->_exec_search();
  415. // count contacts for this user
  416. $this->result = $this->count();
  417. // we have a search result resource
  418. if ($this->ldap_result && $this->result->count > 0)
  419. {
  420. // sorting still on the ldap server
  421. if ($this->sort_col && $this->prop['scope'] !== 'base' && !$this->vlv_active)
  422. ldap_sort($this->conn, $this->ldap_result, $this->sort_col);
  423. // get all entries from the ldap server
  424. $entries = ldap_get_entries($this->conn, $this->ldap_result);
  425. }
  426. } // end else
  427. // start and end of the page
  428. $start_row = $this->vlv_active ? 0 : $this->result->first;
  429. $start_row = $subset < 0 ? $start_row + $this->page_size + $subset : $start_row;
  430. $last_row = $this->result->first + $this->page_size;
  431. $last_row = $subset != 0 ? $start_row + abs($subset) : $last_row;
  432. // filter entries for this page
  433. for ($i = $start_row; $i < min($entries['count'], $last_row); $i++)
  434. $this->result->add($this->_ldap2result($entries[$i]));
  435. return $this->result;
  436. }
  437. /**
  438. * Get all members of the given group
  439. *
  440. * @param string Group DN
  441. * @param array Group entries (if called recursively)
  442. * @return array Accumulated group members
  443. */
  444. function list_group_members($dn, $count = false, $entries = null)
  445. {
  446. $group_members = array();
  447. // fetch group object
  448. if (empty($entries)) {
  449. $result = @ldap_read($this->conn, $dn, '(objectClass=*)', array('dn','objectClass','member','uniqueMember','memberURL'));
  450. if ($result === false)
  451. {
  452. $this->_debug("S: ".ldap_error($this->conn));
  453. return $group_members;
  454. }
  455. $entries = @ldap_get_entries($this->conn, $result);
  456. }
  457. for ($i=0; $i < $entries["count"]; $i++)
  458. {
  459. $entry = $entries[$i];
  460. if (empty($entry['objectclass']))
  461. continue;
  462. foreach ((array)$entry['objectclass'] as $objectclass)
  463. {
  464. switch (strtolower($objectclass)) {
  465. case "groupofnames":
  466. case "kolabgroupofnames":
  467. $group_members = array_merge($group_members, $this->_list_group_members($dn, $entry, 'member', $count));
  468. break;
  469. case "groupofuniquenames":
  470. case "kolabgroupofuniquenames":
  471. $group_members = array_merge($group_members, $this->_list_group_members($dn, $entry, 'uniquemember', $count));
  472. break;
  473. case "groupofurls":
  474. $group_members = array_merge($group_members, $this->_list_group_memberurl($dn, $entry, $count));
  475. break;
  476. }
  477. }
  478. if ($this->prop['sizelimit'] && count($group_members) > $this->prop['sizelimit'])
  479. break;
  480. }
  481. return array_filter($group_members);
  482. }
  483. /**
  484. * Fetch members of the given group entry from server
  485. *
  486. * @param string Group DN
  487. * @param array Group entry
  488. * @param string Member attribute to use
  489. * @return array Accumulated group members
  490. */
  491. private function _list_group_members($dn, $entry, $attr, $count)
  492. {
  493. // Use the member attributes to return an array of member ldap objects
  494. // NOTE that the member attribute is supposed to contain a DN
  495. $group_members = array();
  496. if (empty($entry[$attr]))
  497. return $group_members;
  498. // read these attributes for all members
  499. $attrib = $count ? array('dn') : array_values($this->fieldmap);
  500. $attrib[] = 'objectClass';
  501. $attrib[] = 'member';
  502. $attrib[] = 'uniqueMember';
  503. $attrib[] = 'memberURL';
  504. for ($i=0; $i < $entry[$attr]['count']; $i++)
  505. {
  506. if (empty($entry[$attr][$i]))
  507. continue;
  508. $result = @ldap_read($this->conn, $entry[$attr][$i], '(objectclass=*)',
  509. $attrib, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit']);
  510. $members = @ldap_get_entries($this->conn, $result);
  511. if ($members == false)
  512. {
  513. $this->_debug("S: ".ldap_error($this->conn));
  514. $members = array();
  515. }
  516. // for nested groups, call recursively
  517. $nested_group_members = $this->list_group_members($entry[$attr][$i], $count, $members);
  518. unset($members['count']);
  519. $group_members = array_merge($group_members, array_filter($members), $nested_group_members);
  520. }
  521. return $group_members;
  522. }
  523. /**
  524. * List members of group class groupOfUrls
  525. *
  526. * @param string Group DN
  527. * @param array Group entry
  528. * @param boolean True if only used for counting
  529. * @return array Accumulated group members
  530. */
  531. private function _list_group_memberurl($dn, $entry, $count)
  532. {
  533. $group_members = array();
  534. for ($i=0; $i < $entry['memberurl']['count']; $i++)
  535. {
  536. // extract components from url
  537. if (!preg_match('!ldap:///([^\?]+)\?\?(\w+)\?(.*)$!', $entry['memberurl'][$i], $m))
  538. continue;
  539. // add search filter if any
  540. $filter = $this->filter ? '(&(' . $m[3] . ')(' . $this->filter . '))' : $m[3];
  541. $func = $m[2] == 'sub' ? 'ldap_search' : ($m[2] == 'base' ? 'ldap_read' : 'ldap_list');
  542. $attrib = $count ? array('dn') : array_values($this->fieldmap);
  543. if ($result = @$func($this->conn, $m[1], $filter,
  544. $attrib, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit'])
  545. ) {
  546. $this->_debug("S: ".ldap_count_entries($this->conn, $result)." record(s) for ".$m[1]);
  547. }
  548. else {
  549. $this->_debug("S: ".ldap_error($this->conn));
  550. return $group_members;
  551. }
  552. $entries = @ldap_get_entries($this->conn, $result);
  553. for ($j = 0; $j < $entries['count']; $j++)
  554. {
  555. if ($nested_group_members = $this->list_group_members($entries[$j]['dn'], $count))
  556. $group_members = array_merge($group_members, $nested_group_members);
  557. else
  558. $group_members[] = $entries[$j];
  559. }
  560. }
  561. return $group_members;
  562. }
  563. /**
  564. * Callback for sorting entries
  565. */
  566. function _entry_sort_cmp($a, $b)
  567. {
  568. return strcmp($a[$this->sort_col][0], $b[$this->sort_col][0]);
  569. }
  570. /**
  571. * Search contacts
  572. *
  573. * @param mixed $fields The field name of array of field names to search in
  574. * @param mixed $value Search value (or array of values when $fields is array)
  575. * @param int $mode Matching mode:
  576. * 0 - partial (*abc*),
  577. * 1 - strict (=),
  578. * 2 - prefix (abc*)
  579. * @param boolean $select True if results are requested, False if count only
  580. * @param boolean $nocount (Not used)
  581. * @param array $required List of fields that cannot be empty
  582. *
  583. * @return array Indexed list of contact records and 'count' value
  584. */
  585. function search($fields, $value, $mode=0, $select=true, $nocount=false, $required=array())
  586. {
  587. $mode = intval($mode);
  588. // special treatment for ID-based search
  589. if ($fields == 'ID' || $fields == $this->primary_key)
  590. {
  591. $ids = !is_array($value) ? explode(',', $value) : $value;
  592. $result = new rcube_result_set();
  593. foreach ($ids as $id)
  594. {
  595. if ($rec = $this->get_record($id, true))
  596. {
  597. $result->add($rec);
  598. $result->count++;
  599. }
  600. }
  601. return $result;
  602. }
  603. // use VLV pseudo-search for autocompletion
  604. if ($this->prop['vlv_search'] && $this->conn && join(',', (array)$fields) == 'email,name')
  605. {
  606. // add general filter to query
  607. if (!empty($this->prop['filter']) && empty($this->filter))
  608. $this->set_search_set($this->prop['filter']);
  609. // set VLV controls with encoded search string
  610. $this->_vlv_set_controls($this->prop, $this->list_page, $this->page_size, $value);
  611. $function = $this->_scope2func($this->prop['scope']);
  612. $this->ldap_result = @$function($this->conn, $this->base_dn, $this->filter ? $this->filter : '(objectclass=*)',
  613. array_values($this->fieldmap), 0, $this->page_size, (int)$this->prop['timelimit']);
  614. $this->result = new rcube_result_set(0);
  615. if (!$this->ldap_result) {
  616. $this->_debug("S: ".ldap_error($this->conn));
  617. return $this->result;
  618. }
  619. $this->_debug("S: ".ldap_count_entries($this->conn, $this->ldap_result)." record(s)");
  620. // get all entries of this page and post-filter those that really match the query
  621. $search = mb_strtolower($value);
  622. $entries = ldap_get_entries($this->conn, $this->ldap_result);
  623. for ($i = 0; $i < $entries['count']; $i++) {
  624. $rec = $this->_ldap2result($entries[$i]);
  625. foreach (array('email', 'name') as $f) {
  626. $val = mb_strtolower($rec[$f]);
  627. switch ($mode) {
  628. case 1:
  629. $got = ($val == $search);
  630. break;
  631. case 2:
  632. $got = ($search == substr($val, 0, strlen($search)));
  633. break;
  634. default:
  635. $got = (strpos($val, $search) !== false);
  636. break;
  637. }
  638. if ($got) {
  639. $this->result->add($rec);
  640. $this->result->count++;
  641. break;
  642. }
  643. }
  644. }
  645. return $this->result;
  646. }
  647. // use AND operator for advanced searches
  648. $filter = is_array($value) ? '(&' : '(|';
  649. // set wildcards
  650. $wp = $ws = '';
  651. if (!empty($this->prop['fuzzy_search']) && $mode != 1) {
  652. $ws = '*';
  653. if (!$mode) {
  654. $wp = '*';
  655. }
  656. }
  657. if ($fields == '*')
  658. {
  659. // search_fields are required for fulltext search
  660. if (empty($this->prop['search_fields']))
  661. {
  662. $this->set_error(self::ERROR_SEARCH, 'nofulltextsearch');
  663. $this->result = new rcube_result_set();
  664. return $this->result;
  665. }
  666. if (is_array($this->prop['search_fields']))
  667. {
  668. foreach ($this->prop['search_fields'] as $field) {
  669. $filter .= "($field=$wp" . $this->_quote_string($value) . "$ws)";
  670. }
  671. }
  672. }
  673. else
  674. {
  675. foreach ((array)$fields as $idx => $field) {
  676. $val = is_array($value) ? $value[$idx] : $value;
  677. if ($f = $this->_map_field($field)) {
  678. $filter .= "($f=$wp" . $this->_quote_string($val) . "$ws)";
  679. }
  680. }
  681. }
  682. $filter .= ')';
  683. // add required (non empty) fields filter
  684. $req_filter = '';
  685. foreach ((array)$required as $field)
  686. if ($f = $this->_map_field($field))
  687. $req_filter .= "($f=*)";
  688. if (!empty($req_filter))
  689. $filter = '(&' . $req_filter . $filter . ')';
  690. // avoid double-wildcard if $value is empty
  691. $filter = preg_replace('/\*+/', '*', $filter);
  692. // add general filter to query
  693. if (!empty($this->prop['filter']))
  694. $filter = '(&(' . preg_replace('/^\(|\)$/', '', $this->prop['filter']) . ')' . $filter . ')';
  695. // set filter string and execute search
  696. $this->set_search_set($filter);
  697. $this->_exec_search();
  698. if ($select)
  699. $this->list_records();
  700. else
  701. $this->result = $this->count();
  702. return $this->result;
  703. }
  704. /**
  705. * Count number of available contacts in database
  706. *
  707. * @return object rcube_result_set Resultset with values for 'count' and 'first'
  708. */
  709. function count()
  710. {
  711. $count = 0;
  712. if ($this->conn && $this->ldap_result) {
  713. $count = $this->vlv_active ? $this->vlv_count : ldap_count_entries($this->conn, $this->ldap_result);
  714. }
  715. else if ($this->group_id && $this->group_data['dn']) {
  716. $count = count($this->list_group_members($this->group_data['dn'], true));
  717. }
  718. else if ($this->conn) {
  719. // We have a connection but no result set, attempt to get one.
  720. if (empty($this->filter)) {
  721. // The filter is not set, set it.
  722. $this->filter = $this->prop['filter'];
  723. }
  724. $this->_exec_search(true);
  725. if ($this->ldap_result) {
  726. $count = ldap_count_entries($this->conn, $this->ldap_result);
  727. }
  728. }
  729. return new rcube_result_set($count, ($this->list_page-1) * $this->page_size);
  730. }
  731. /**
  732. * Return the last result set
  733. *
  734. * @return object rcube_result_set Current resultset or NULL if nothing selected yet
  735. */
  736. function get_result()
  737. {
  738. return $this->result;
  739. }
  740. /**
  741. * Get a specific contact record
  742. *
  743. * @param mixed Record identifier
  744. * @param boolean Return as associative array
  745. *
  746. * @return mixed Hash array or rcube_result_set with all record fields
  747. */
  748. function get_record($dn, $assoc=false)
  749. {
  750. $res = null;
  751. if ($this->conn && $dn)
  752. {
  753. $dn = self::dn_decode($dn);
  754. $this->_debug("C: Read [dn: $dn] [(objectclass=*)]");
  755. if ($this->ldap_result = @ldap_read($this->conn, $dn, '(objectclass=*)', array_values($this->fieldmap)))
  756. $entry = ldap_first_entry($this->conn, $this->ldap_result);
  757. else
  758. $this->_debug("S: ".ldap_error($this->conn));
  759. if ($entry && ($rec = ldap_get_attributes($this->conn, $entry)))
  760. {
  761. $this->_debug("S: OK"/* . print_r($rec, true)*/);
  762. $rec = array_change_key_case($rec, CASE_LOWER);
  763. // Add in the dn for the entry.
  764. $rec['dn'] = $dn;
  765. $res = $this->_ldap2result($rec);
  766. $this->result = new rcube_result_set(1);
  767. $this->result->add($res);
  768. }
  769. }
  770. return $assoc ? $res : $this->result;
  771. }
  772. /**
  773. * Check the given data before saving.
  774. * If input not valid, the message to display can be fetched using get_error()
  775. *
  776. * @param array Assoziative array with data to save
  777. * @param boolean Try to fix/complete record automatically
  778. * @return boolean True if input is valid, False if not.
  779. */
  780. public function validate(&$save_data, $autofix = false)
  781. {
  782. // check for name input
  783. if (empty($save_data['name'])) {
  784. $this->set_error(self::ERROR_VALIDATE, 'nonamewarning');
  785. return false;
  786. }
  787. // Verify that the required fields are set.
  788. $missing = null;
  789. $ldap_data = $this->_map_data($save_data);
  790. foreach ($this->prop['required_fields'] as $fld) {
  791. if (!isset($ldap_data[$fld])) {
  792. $missing[$fld] = 1;
  793. }
  794. }
  795. if ($missing) {
  796. // try to complete record automatically
  797. if ($autofix) {
  798. $reverse_map = array_flip($this->fieldmap);
  799. $name_parts = preg_split('/[\s,.]+/', $save_data['name']);
  800. if ($missing['sn']) {
  801. $sn_field = $reverse_map['sn'];
  802. $save_data[$sn_field] = array_pop ($name_parts);
  803. }
  804. if ($missing[($fn_field = $this->fieldmap['firstname'])]) {
  805. $save_data['firstname'] = array_shift($name_parts);
  806. }
  807. return $this->validate($save_data, false);
  808. }
  809. // TODO: generate message saying which fields are missing
  810. $this->set_error(self::ERROR_VALIDATE, 'formincomplete');
  811. return false;
  812. }
  813. // validate e-mail addresses
  814. return parent::validate($save_data, $autofix);
  815. }
  816. /**
  817. * Create a new contact record
  818. *
  819. * @param array Hash array with save data
  820. *
  821. * @return encoded record ID on success, False on error
  822. */
  823. function insert($save_cols)
  824. {
  825. // Map out the column names to their LDAP ones to build the new entry.
  826. $newentry = $this->_map_data($save_cols);
  827. $newentry['objectClass'] = $this->prop['LDAP_Object_Classes'];
  828. // Verify that the required fields are set.
  829. $missing = null;
  830. foreach ($this->prop['required_fields'] as $fld) {
  831. if (!isset($newentry[$fld])) {
  832. $missing[] = $fld;
  833. }
  834. }
  835. // abort process if requiered fields are missing
  836. // TODO: generate message saying which fields are missing
  837. if ($missing) {
  838. $this->set_error(self::ERROR_VALIDATE, 'formincomplete');
  839. return false;
  840. }
  841. // Build the new entries DN.
  842. $dn = $this->prop['LDAP_rdn'].'='.$this->_quote_string($newentry[$this->prop['LDAP_rdn']], true).','.$this->base_dn;
  843. $this->_debug("C: Add [dn: $dn]: ".print_r($newentry, true));
  844. $res = ldap_add($this->conn, $dn, $newentry);
  845. if ($res === FALSE) {
  846. $this->_debug("S: ".ldap_error($this->conn));
  847. $this->set_error(self::ERROR_SAVING, 'errorsaving');
  848. return false;
  849. } // end if
  850. $this->_debug("S: OK");
  851. $dn = self::dn_encode($dn);
  852. // add new contact to the selected group
  853. if ($this->group_id)
  854. $this->add_to_group($this->group_id, $dn);
  855. return $dn;
  856. }
  857. /**
  858. * Update a specific contact record
  859. *
  860. * @param mixed Record identifier
  861. * @param array Hash array with save data
  862. *
  863. * @return boolean True on success, False on error
  864. */
  865. function update($id, $save_cols)
  866. {
  867. $record = $this->get_record($id, true);
  868. $result = $this->get_result();
  869. $record = $result->first();
  870. $newdata = array();
  871. $replacedata = array();
  872. $deletedata = array();
  873. $ldap_data = $this->_map_data($save_cols);
  874. $old_data = $record['_raw_attrib'];
  875. foreach ($this->fieldmap as $col => $fld) {
  876. $val = $ldap_data[$fld];
  877. if ($fld) {
  878. // remove empty array values
  879. if (is_array($val))
  880. $val = array_filter($val);
  881. // The field does exist compare it to the ldap record.
  882. if ($old_data[$fld] != $val) {
  883. // Changed, but find out how.
  884. if (!isset($old_data[$fld])) {
  885. // Field was not set prior, need to add it.
  886. $newdata[$fld] = $val;
  887. }
  888. else if ($val == '') {
  889. // Field supplied is empty, verify that it is not required.
  890. if (!in_array($fld, $this->prop['required_fields'])) {
  891. // It is not, safe to clear.
  892. $deletedata[$fld] = $old_data[$fld];
  893. }
  894. } // end elseif
  895. else {
  896. // The data was modified, save it out.
  897. $replacedata[$fld] = $val;
  898. }
  899. } // end if
  900. } // end if
  901. } // end foreach
  902. $dn = self::dn_decode($id);
  903. // Update the entry as required.
  904. if (!empty($deletedata)) {
  905. // Delete the fields.
  906. $this->_debug("C: Delete [dn: $dn]: ".print_r($deletedata, true));
  907. if (!ldap_mod_del($this->conn, $dn, $deletedata)) {
  908. $this->_debug("S: ".ldap_error($this->conn));
  909. $this->set_error(self::ERROR_SAVING, 'errorsaving');
  910. return false;
  911. }
  912. $this->_debug("S: OK");
  913. } // end if
  914. if (!empty($replacedata)) {
  915. // Handle RDN change
  916. if ($replacedata[$this->prop['LDAP_rdn']]) {
  917. $newdn = $this->prop['LDAP_rdn'].'='
  918. .$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true)
  919. .','.$this->base_dn;
  920. if ($dn != $newdn) {
  921. $newrdn = $this->prop['LDAP_rdn'].'='
  922. .$this->_quote_string($replacedata[$this->prop['LDAP_rdn']], true);
  923. unset($replacedata[$this->prop['LDAP_rdn']]);
  924. }
  925. }
  926. // Replace the fields.
  927. if (!empty($replacedata)) {
  928. $this->_debug("C: Replace [dn: $dn]: ".print_r($replacedata, true));
  929. if (!ldap_mod_replace($this->conn, $dn, $replacedata)) {
  930. $this->_debug("S: ".ldap_error($this->conn));
  931. return false;
  932. }
  933. $this->_debug("S: OK");
  934. } // end if
  935. } // end if
  936. if (!empty($newdata)) {
  937. // Add the fields.
  938. $this->_debug("C: Add [dn: $dn]: ".print_r($newdata, true));
  939. if (!ldap_mod_add($this->conn, $dn, $newdata)) {
  940. $this->_debug("S: ".ldap_error($this->conn));
  941. $this->set_error(self::ERROR_SAVING, 'errorsaving');
  942. return false;
  943. }
  944. $this->_debug("S: OK");
  945. } // end if
  946. // Handle RDN change
  947. if (!empty($newrdn)) {
  948. $this->_debug("C: Rename [dn: $dn] [dn: $newrdn]");
  949. if (!ldap_rename($this->conn, $dn, $newrdn, NULL, TRUE)) {
  950. $this->_debug("S: ".ldap_error($this->conn));
  951. return false;
  952. }
  953. $this->_debug("S: OK");
  954. $dn = self::dn_encode($dn);
  955. $newdn = self::dn_encode($newdn);
  956. // change the group membership of the contact
  957. if ($this->groups)
  958. {
  959. $group_ids = $this->get_record_groups($dn);
  960. foreach ($group_ids as $group_id)
  961. {
  962. $this->remove_from_group($group_id, $dn);
  963. $this->add_to_group($group_id, $newdn);
  964. }
  965. }
  966. return $newdn;
  967. }
  968. return true;
  969. }
  970. /**
  971. * Mark one or more contact records as deleted
  972. *
  973. * @param array Record identifiers
  974. * @param boolean Remove record(s) irreversible (unsupported)
  975. *
  976. * @return boolean True on success, False on error
  977. */
  978. function delete($ids, $force=true)
  979. {
  980. if (!is_array($ids)) {
  981. // Not an array, break apart the encoded DNs.
  982. $ids = explode(',', $ids);
  983. } // end if
  984. foreach ($ids as $id) {
  985. $dn = self::dn_decode($id);
  986. $this->_debug("C: Delete [dn: $dn]");
  987. // Delete the record.
  988. $res = ldap_delete($this->conn, $dn);
  989. if ($res === FALSE) {
  990. $this->_debug("S: ".ldap_error($this->conn));
  991. $this->set_error(self::ERROR_SAVING, 'errorsaving');
  992. return false;
  993. } // end if
  994. $this->_debug("S: OK");
  995. // remove contact from all groups where he was member
  996. if ($this->groups) {
  997. $dn = self::dn_encode($dn);
  998. $group_ids = $this->get_record_groups($dn);
  999. foreach ($group_ids as $group_id) {
  1000. $this->remove_from_group($group_id, $dn);
  1001. }
  1002. }
  1003. } // end foreach
  1004. return count($ids);
  1005. }
  1006. /**
  1007. * Execute the LDAP search based on the stored credentials
  1008. */
  1009. private function _exec_search($count = false)
  1010. {
  1011. if ($this->ready)
  1012. {
  1013. $filter = $this->filter ? $this->filter : '(objectclass=*)';
  1014. $function = $this->_scope2func($this->prop['scope'], $ns_function);
  1015. $this->_debug("C: Search [$filter][dn: $this->base_dn]");
  1016. // when using VLV, we get the total count by...
  1017. if (!$count && $function != 'ldap_read' && $this->prop['vlv'] && !$this->group_id) {
  1018. // ...either reading numSubOrdinates attribute
  1019. if ($this->prop['numsub_filter'] && ($result_count = @$ns_function($this->conn, $this->base_dn, $this->prop['numsub_filter'], array('numSubOrdinates'), 0, 0, 0))) {
  1020. $counts = ldap_get_entries($this->conn, $result_count);
  1021. for ($this->vlv_count = $j = 0; $j < $counts['count']; $j++)
  1022. $this->vlv_count += $counts[$j]['numsubordinates'][0];
  1023. $this->_debug("D: total numsubordinates = " . $this->vlv_count);
  1024. }
  1025. else // ...or by fetching all records dn and count them
  1026. $this->vlv_count = $this->_exec_search(true);
  1027. $this->vlv_active = $this->_vlv_set_controls($this->prop, $this->list_page, $this->page_size);
  1028. }
  1029. // only fetch dn for count (should keep the payload low)
  1030. $attrs = $count ? array('dn') : array_values($this->fieldmap);
  1031. if ($this->ldap_result = @$function($this->conn, $this->base_dn, $filter,
  1032. $attrs, 0, (int)$this->prop['sizelimit'], (int)$this->prop['timelimit'])
  1033. ) {
  1034. $entries_count = ldap_count_entries($this->conn, $this->ldap_result);
  1035. $this->_debug("S: $entries_count record(s)");
  1036. return $count ? $entries_count : true;
  1037. }
  1038. else {
  1039. $this->_debug("S: ".ldap_error($this->conn));
  1040. }
  1041. }
  1042. return false;
  1043. }
  1044. /**
  1045. * Choose the right PHP function according to scope property
  1046. */
  1047. private function _scope2func($scope, &$ns_function = null)
  1048. {
  1049. switch ($scope) {
  1050. case 'sub':
  1051. $function = $ns_function = 'ldap_search';
  1052. break;
  1053. case 'base':
  1054. $function = $ns_function = 'ldap_read';
  1055. break;
  1056. default:
  1057. $function = 'ldap_list';
  1058. $ns_function = 'ldap_read';
  1059. break;
  1060. }
  1061. return $function;
  1062. }
  1063. /**
  1064. * Set server controls for Virtual List View (paginated listing)
  1065. */
  1066. private function _vlv_set_controls($prop, $list_page, $page_size, $search = null)
  1067. {
  1068. $sort_ctrl = array('oid' => "1.2.840.113556.1.4.473", 'value' => $this->_sort_ber_encode((array)$prop['sort']));
  1069. $vlv_ctrl = array('oid' => "2.16.840.1.113730.3.4.9", 'value' => $this->_vlv_ber_encode(($offset = ($list_page-1) * $page_size + 1), $page_size, $search), 'iscritical' => true);
  1070. $sort = (array)$prop['sort'];
  1071. $this->_debug("C: set controls sort=" . join(' ', unpack('H'.(strlen($sort_ctrl['value'])*2), $sort_ctrl['value'])) . " ($sort[0]);"
  1072. . " vlv=" . join(' ', (unpack('H'.(strlen($vlv_ctrl['value'])*2), $vlv_ctrl['value']))) . " ($offset/$page_size)");
  1073. if (!ldap_set_option($this->conn, LDAP_OPT_SERVER_CONTROLS, array($sort_ctrl, $vlv_ctrl))) {
  1074. $this->_debug("S: ".ldap_error($this->conn));
  1075. $this->set_error(self::ERROR_SEARCH, 'vlvnotsupported');
  1076. return false;
  1077. }
  1078. return true;
  1079. }
  1080. /**
  1081. * Converts LDAP entry into an array
  1082. */
  1083. private function _ldap2result($rec)
  1084. {
  1085. $out = array();
  1086. if ($rec['dn'])
  1087. $out[$this->primary_key] = self::dn_encode($rec['dn']);
  1088. foreach ($this->fieldmap as $rf => $lf)
  1089. {
  1090. for ($i=0; $i < $rec[$lf]['count']; $i++) {
  1091. if (!($value = $rec[$lf][$i]))
  1092. continue;
  1093. list($col, $subtype) = explode(':', $rf);
  1094. $out['_raw_attrib'][$lf][$i] = $value;
  1095. if ($rf == 'email' && $this->mail_domain && !strpos($value, '@'))
  1096. $out[$rf][] = sprintf('%s@%s', $value, $this->mail_domain);
  1097. else if (in_array($col, array('street','zipcode','locality','country','region')))
  1098. $out['address'.($subtype?':':'').$subtype][$i][$col] = $value;
  1099. else if ($rec[$lf]['count'] > 1)
  1100. $out[$rf][] = $value;
  1101. else
  1102. $out[$rf] = $value;
  1103. }
  1104. // Make sure name fields aren't arrays (#1488108)
  1105. if (is_array($out[$rf]) && in_array($rf, array('name', 'surname', 'firstname', 'middlename', 'nickname'))) {
  1106. $out[$rf] = $out['_raw_attrib'][$lf] = $out[$rf][0];
  1107. }
  1108. }
  1109. return $out;
  1110. }
  1111. /**
  1112. * Return real field name (from fields map)
  1113. */
  1114. private function _map_field($field)
  1115. {
  1116. return $this->fieldmap[$field];
  1117. }
  1118. /**
  1119. * Convert a record data set into LDAP field attributes
  1120. */
  1121. private function _map_data($save_cols)
  1122. {
  1123. // flatten composite fields first
  1124. foreach ($this->coltypes as $col => $colprop) {
  1125. if (is_array($colprop['childs']) && ($values = $this->get_col_values($col, $save_cols, false))) {
  1126. foreach ($values as $subtype => $childs) {
  1127. $subtype = $subtype ? ':'.$subtype : '';
  1128. foreach ($childs as $i => $child_values) {
  1129. foreach ((array)$child_values as $childcol => $value) {
  1130. $save_cols[$childcol.$subtype][$i] = $value;
  1131. }
  1132. }
  1133. }
  1134. }
  1135. }
  1136. $ldap_data = array();
  1137. foreach ($this->fieldmap as $col => $fld) {
  1138. $val = $save_cols[$col];
  1139. if (is_array($val))
  1140. $val = array_filter($val); // remove empty entries
  1141. if ($fld && $val) {
  1142. // The field does exist, add it to the entry.
  1143. $ldap_data[$fld] = $val;
  1144. }
  1145. }
  1146. return $ldap_data;
  1147. }
  1148. /**
  1149. * Returns unified attribute name (resolving aliases)
  1150. */
  1151. private static function _attr_name($name)
  1152. {
  1153. // list of known attribute aliases
  1154. $aliases = array(
  1155. 'gn' => 'givenname',
  1156. 'rfc822mailbox' => 'email',
  1157. 'userid' => 'uid',
  1158. 'emailaddress' => 'email',
  1159. 'pkcs9email' => 'email',
  1160. );
  1161. return isset($aliases[$name]) ? $aliases[$name] : $name;
  1162. }
  1163. /**
  1164. * Prints debug info to the log
  1165. */
  1166. private function _debug($str)
  1167. {
  1168. if ($this->debug)
  1169. write_log('ldap', $str);
  1170. }
  1171. /**
  1172. * Activate/deactivate debug mode
  1173. *
  1174. * @param boolean $dbg True if LDAP commands should be logged
  1175. * @access public
  1176. */
  1177. function set_debug($dbg = true)
  1178. {
  1179. $this->debug = $dbg;
  1180. }
  1181. /**
  1182. * Quotes attribute value string
  1183. *
  1184. * @param string $str Attribute value
  1185. * @param bool $dn True if the attribute is a DN
  1186. *
  1187. * @return string Quoted string
  1188. */
  1189. private static function _quote_string($str, $dn=false)
  1190. {
  1191. // take firt entry if array given
  1192. if (is_array($str))
  1193. $str = reset($str);
  1194. if ($dn)
  1195. $replace = array(','=>'\2c', '='=>'\3d', '+'=>'\2b', '<'=>'\3c',
  1196. '>'=>'\3e', ';'=>'\3b', '\\'=>'\5c', '"'=>'\22', '#'=>'\23');
  1197. else
  1198. $replace = array('*'=>'\2a', '('=>'\28', ')'=>'\29', '\\'=>'\5c',
  1199. '/'=>'\2f');
  1200. return strtr($str, $replace);
  1201. }
  1202. /**
  1203. * Setter for the current group
  1204. * (empty, has to be re-implemented by extending class)
  1205. */
  1206. function set_group($group_id)
  1207. {
  1208. if ($group_id)
  1209. {
  1210. if (($group_cache = $this->cache->get('groups')) === null)
  1211. $group_cache = $this->_fetch_groups();
  1212. $this->group_id = $group_id;
  1213. $this->group_data = $group_cache[$group_id];
  1214. }
  1215. else
  1216. {
  1217. $this->group_id = 0;
  1218. $this->group_data = null;
  1219. }
  1220. }
  1221. /**
  1222. * List all active contact groups of this source
  1223. *
  1224. * @param string Optional search string to match group name
  1225. * @return array Indexed list of contact groups, each a hash array

Large files files are truncated, but you can click here to view the full file