abook_ldap_server.php 13 KB

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