abook_ldap_server.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460
  1. <?php
  2. /**
  3. * abook_ldap_server.php
  4. *
  5. * Copyright (c) 1999-2005 The SquirrelMail Project Team
  6. * Licensed under the GNU GPL. For full terms see the file COPYING.
  7. *
  8. * Address book backend for LDAP server
  9. *
  10. * LDAP filtering code by Tim Bell
  11. * <bhat at users.sourceforge.net> (#539534)
  12. * ADS limit_scope code by Michael Brown
  13. * <mcb30 at users.sourceforge.net> (#1035454)
  14. *
  15. * @version $Id$
  16. * @package squirrelmail
  17. * @subpackage addressbook
  18. */
  19. /**
  20. * Address book backend for LDAP server
  21. *
  22. * An array with the following elements must be passed to
  23. * the class constructor (elements marked ? are optional)
  24. *
  25. * Main settings:
  26. * <pre>
  27. * host => LDAP server hostname/IP-address
  28. * base => LDAP server root (base dn). Empty string allowed.
  29. * ? port => LDAP server TCP port number (default: 389)
  30. * ? charset => LDAP server charset (default: utf-8)
  31. * ? name => Name for LDAP server (default "LDAP: hostname")
  32. * Used to tag the result data
  33. * ? maxrows => Maximum # of rows in search result
  34. * ? timeout => Timeout for LDAP operations (in seconds, default: 30)
  35. * Might not work for all LDAP libraries or servers.
  36. * ? binddn => LDAP Bind DN.
  37. * ? bindpw => LDAP Bind Password.
  38. * ? protocol => LDAP Bind protocol.
  39. * </pre>
  40. * Advanced settings:
  41. * <pre>
  42. * ? filter => Filter expression to limit ldap searches
  43. * ? limit_scope => Limits scope to base DN (Specific to Win2k3 ADS).
  44. * ? listing => Controls listing of LDAP directory.
  45. * </pre>
  46. * NOTE. This class should not be used directly. Use the
  47. * "AddressBook" class instead.
  48. * @package squirrelmail
  49. * @subpackage addressbook
  50. */
  51. class abook_ldap_server extends addressbook_backend {
  52. /**
  53. * @var string backend type
  54. */
  55. var $btype = 'remote';
  56. /**
  57. * @var string backend name
  58. */
  59. var $bname = 'ldap_server';
  60. /* Parameters changed by class */
  61. /**
  62. * @var string displayed name
  63. */
  64. var $sname = 'LDAP'; /* Service name */
  65. /**
  66. * @var string LDAP server name or address or url
  67. */
  68. var $server = '';
  69. /**
  70. * @var integer LDAP server port
  71. */
  72. var $port = 389;
  73. /**
  74. * @var string LDAP base DN
  75. */
  76. var $basedn = '';
  77. /**
  78. * @var string charset used for entries in LDAP server
  79. */
  80. var $charset = 'utf-8';
  81. /**
  82. * @var object PHP LDAP link ID
  83. */
  84. var $linkid = false;
  85. /**
  86. * @var bool True if LDAP server is bound
  87. */
  88. var $bound = false;
  89. /**
  90. * @var integer max rows in result
  91. */
  92. var $maxrows = 250;
  93. /**
  94. * @var string ldap filter
  95. * @since 1.5.1
  96. */
  97. var $filter = '';
  98. /**
  99. * @var integer timeout of LDAP operations (in seconds)
  100. */
  101. var $timeout = 30;
  102. /**
  103. * @var string DN to bind to (non-anonymous bind)
  104. * @since 1.5.0 and 1.4.3
  105. */
  106. var $binddn = '';
  107. /**
  108. * @var string password to bind with (non-anonymous bind)
  109. * @since 1.5.0 and 1.4.3
  110. */
  111. var $bindpw = '';
  112. /**
  113. * @var integer protocol used to connect to ldap server
  114. * @since 1.5.0 and 1.4.3
  115. */
  116. var $protocol = '';
  117. /**
  118. * @var boolean limits scope to base dn
  119. * @since 1.5.1
  120. */
  121. var $limit_scope = false;
  122. /**
  123. * @var boolean controls listing of directory
  124. * @since 1.5.1
  125. */
  126. var $listing = false;
  127. /**
  128. * Constructor. Connects to database
  129. * @param array connection options
  130. */
  131. function abook_ldap_server($param) {
  132. if(!function_exists('ldap_connect')) {
  133. $this->set_error(_("PHP install does not have LDAP support."));
  134. return;
  135. }
  136. if(is_array($param)) {
  137. $this->server = $param['host'];
  138. $this->basedn = $param['base'];
  139. if(!empty($param['port']))
  140. $this->port = $param['port'];
  141. if(!empty($param['charset']))
  142. $this->charset = strtolower($param['charset']);
  143. if(isset($param['maxrows']))
  144. $this->maxrows = $param['maxrows'];
  145. if(isset($param['timeout']))
  146. $this->timeout = $param['timeout'];
  147. if(isset($param['binddn']))
  148. $this->binddn = $param['binddn'];
  149. if(isset($param['bindpw']))
  150. $this->bindpw = $param['bindpw'];
  151. if(isset($param['protocol']))
  152. $this->protocol = $param['protocol'];
  153. if(isset($param['filter']))
  154. $this->filter = trim($param['filter']);
  155. if(isset($param['limit_scope']))
  156. $this->limit_scope = $param['limit_scope'];
  157. if(isset($param['listing']))
  158. $this->listing = $param['listing'];
  159. if(empty($param['name'])) {
  160. $this->sname = 'LDAP: ' . $param['host'];
  161. } else {
  162. $this->sname = $param['name'];
  163. }
  164. /*
  165. * don't open LDAP server on addressbook_init(),
  166. * open ldap connection only on search. Speeds up
  167. * addressbook_init() call.
  168. */
  169. // $this->open(true);
  170. } else {
  171. $this->set_error('Invalid argument to constructor');
  172. }
  173. }
  174. /**
  175. * Open the LDAP server.
  176. * @param bool $new is it a new connection
  177. * @return bool
  178. */
  179. function open($new = false) {
  180. $this->error = '';
  181. /* Connection is already open */
  182. if($this->linkid != false && !$new) {
  183. return true;
  184. }
  185. $this->linkid = @ldap_connect($this->server, $this->port);
  186. if(!$this->linkid) {
  187. if(function_exists('ldap_error') && is_resource($this->linkid)) {
  188. return $this->set_error(ldap_error($this->linkid));
  189. } else {
  190. return $this->set_error('ldap_connect failed');
  191. }
  192. }
  193. if(!empty($this->protocol)) {
  194. if(!@ldap_set_option($this->linkid, LDAP_OPT_PROTOCOL_VERSION, $this->protocol)) {
  195. if(function_exists('ldap_error')) {
  196. return $this->set_error(ldap_error($this->linkid));
  197. } else {
  198. return $this->set_error('ldap_set_option failed');
  199. }
  200. }
  201. }
  202. if(!empty($this->limit_scope) && $this->limit_scope) {
  203. if(empty($this->protocol) || intval($this->protocol) < 3) {
  204. return $this->set_error('limit_scope requires protocol >= 3');
  205. }
  206. // See http://msdn.microsoft.com/library/en-us/ldap/ldap/ldap_server_domain_scope_oid.asp
  207. $ctrl = array ( "oid" => "1.2.840.113556.1.4.1339", "iscritical" => TRUE );
  208. if(!@ldap_set_option($this->linkid, LDAP_OPT_SERVER_CONTROLS, array($ctrl))) {
  209. if(function_exists('ldap_error')) {
  210. return $this->set_error(ldap_error($this->linkid));
  211. } else {
  212. return $this->set_error('limit domain scope failed');
  213. }
  214. }
  215. }
  216. if(!empty($this->binddn)) {
  217. if(!@ldap_bind($this->linkid, $this->binddn, $this->bindpw)) {
  218. if(function_exists('ldap_error')) {
  219. return $this->set_error(ldap_error($this->linkid));
  220. } else {
  221. return $this->set_error('authenticated ldap_bind failed');
  222. }
  223. }
  224. } else {
  225. if(!@ldap_bind($this->linkid)) {
  226. if(function_exists('ldap_error')) {
  227. return $this->set_error(ldap_error($this->linkid));
  228. } else {
  229. return $this->set_error('anonymous ldap_bind failed');
  230. }
  231. }
  232. }
  233. $this->bound = true;
  234. return true;
  235. }
  236. /**
  237. * Encode string to the charset used by this LDAP server
  238. * @param string string that has to be encoded
  239. * @return string encoded string
  240. */
  241. function charset_encode($str) {
  242. global $default_charset;
  243. if($this->charset != $default_charset) {
  244. return charset_convert($default_charset,$str,$this->charset,false);
  245. } else {
  246. return $str;
  247. }
  248. }
  249. /**
  250. * Decode from charset used by this LDAP server to charset used by translation
  251. *
  252. * Uses SquirrelMail charset_decode functions
  253. * @param string string that has to be decoded
  254. * @return string decoded string
  255. */
  256. function charset_decode($str) {
  257. global $default_charset;
  258. if ($this->charset != $default_charset) {
  259. return charset_convert($this->charset,$str,$default_charset,false);
  260. } else {
  261. return $str;
  262. }
  263. }
  264. /**
  265. * Sanitizes ldap search strings.
  266. * See rfc2254
  267. * @link http://www.faqs.org/rfcs/rfc2254.html
  268. * @since 1.5.1 and 1.4.5
  269. * @param string $string
  270. * @return string sanitized string
  271. */
  272. function ldapspecialchars($string) {
  273. $sanitized=array('\\' => '\5c',
  274. '*' => '\2a',
  275. '(' => '\28',
  276. ')' => '\29',
  277. "\x00" => '\00');
  278. return str_replace(array_keys($sanitized),array_values($sanitized),$string);
  279. }
  280. /**
  281. * Search LDAP server.
  282. *
  283. * Warning: You must make sure that ldap query is correctly formated and
  284. * sanitize use of special ldap keywords.
  285. * @param string $expression ldap query
  286. * @return array search results (false on error)
  287. * @since 1.5.1
  288. */
  289. function ldap_search($expression) {
  290. /* Make sure connection is there */
  291. if(!$this->open()) {
  292. return false;
  293. }
  294. // TODO: ldap_search() | ldap_list() | ldap_read() option
  295. $sret = @ldap_search($this->linkid, $this->basedn, $expression,
  296. array('dn', 'o', 'ou', 'sn', 'givenname', 'cn', 'mail'),
  297. 0, $this->maxrows, $this->timeout);
  298. /* Should get error from server using the ldap_error() function,
  299. * but it only exist in the PHP LDAP documentation. */
  300. if(!$sret) {
  301. if(function_exists('ldap_error')) {
  302. return $this->set_error(ldap_error($this->linkid));
  303. } else {
  304. return $this->set_error('ldap_search failed');
  305. }
  306. }
  307. if(@ldap_count_entries($this->linkid, $sret) <= 0) {
  308. return array();
  309. }
  310. /* Get results */
  311. $ret = array();
  312. $returned_rows = 0;
  313. $res = @ldap_get_entries($this->linkid, $sret);
  314. for($i = 0 ; $i < $res['count'] ; $i++) {
  315. $row = $res[$i];
  316. /* Extract data common for all e-mail addresses
  317. * of an object. Use only the first name */
  318. $nickname = $this->charset_decode($row['dn']);
  319. // TODO: remove basedn from $nickname
  320. $fullname = $this->charset_decode($row['cn'][0]);
  321. if(!empty($row['ou'][0])) {
  322. $label = $this->charset_decode($row['ou'][0]);
  323. }
  324. else if(!empty($row['o'][0])) {
  325. $label = $this->charset_decode($row['o'][0]);
  326. } else {
  327. $label = '';
  328. }
  329. if(empty($row['givenname'][0])) {
  330. $firstname = '';
  331. } else {
  332. $firstname = $this->charset_decode($row['givenname'][0]);
  333. }
  334. if(empty($row['sn'][0])) {
  335. $surname = '';
  336. } else {
  337. $surname = $this->charset_decode($row['sn'][0]);
  338. }
  339. /* Add one row to result for each e-mail address */
  340. if(isset($row['mail']['count'])) {
  341. for($j = 0 ; $j < $row['mail']['count'] ; $j++) {
  342. array_push($ret, array('nickname' => $nickname,
  343. 'name' => $fullname,
  344. 'firstname' => $firstname,
  345. 'lastname' => $surname,
  346. 'email' => $row['mail'][$j],
  347. 'label' => $label,
  348. 'backend' => $this->bnum,
  349. 'source' => &$this->sname));
  350. // Limit number of hits
  351. $returned_rows++;
  352. if(($returned_rows >= $this->maxrows) &&
  353. ($this->maxrows > 0) ) {
  354. ldap_free_result($sret);
  355. return $ret;
  356. }
  357. } // for($j ...)
  358. } // isset($row['mail']['count'])
  359. }
  360. ldap_free_result($sret);
  361. return $ret;
  362. }
  363. /* ========================== Public ======================== */
  364. /**
  365. * Search the LDAP server
  366. * @param string $expr search expression
  367. * @return array search results
  368. */
  369. function search($expr) {
  370. /* To be replaced by advanded search expression parsing */
  371. if(is_array($expr)) return false;
  372. // don't allow wide search when listing is disabled.
  373. if ($expr=='*' && ! $this->listing)
  374. return array();
  375. /* Convert search from user's charset to the one used in ldap */
  376. $expr = $this->charset_encode($expr);
  377. /* Make sure that search does not contain ldap special chars */
  378. $expression = '(cn=*' . $this->ldapspecialchars($expr) . '*)';
  379. /* Add search filtering */
  380. if ($this->filter!='')
  381. $expression = '(&' . $this->filter . $expression . ')';
  382. /* Use internal search function and return search results */
  383. return $this->ldap_search($expression);
  384. }
  385. /**
  386. * List all entries present in LDAP server
  387. *
  388. * maxrows setting might limit list of returned entries.
  389. * Careful with this -- it could get quite large for big sites.
  390. * @return array all entries in ldap server
  391. */
  392. function list_addr() {
  393. if (! $this->listing)
  394. return array();
  395. /* set wide search expression */
  396. $expression = '(cn=*)';
  397. /* add filtering */
  398. if ($this->filter!='')
  399. $expression = '(&' . $this->filter . $expression .')';
  400. /* use internal search function and return search results */
  401. return $this->ldap_search($expression);
  402. }
  403. }
  404. ?>