5100704d |
1 | <?php |
35586184 |
2 | /** |
3 | * abook_ldap_server.php |
4 | * |
6c84ba1e |
5 | * Copyright (c) 1999-2005 The SquirrelMail Project Team |
35586184 |
6 | * Licensed under the GNU GPL. For full terms see the file COPYING. |
7 | * |
981681d5 |
8 | * Address book backend for LDAP server |
9 | * |
ae4d36f7 |
10 | * LDAP filtering code by Tim Bell |
11 | * <bhat at users.sourceforge.net> (#539534) |
f8a1ed5a |
12 | * ADS limit_scope code by Michael Brown |
ae4d36f7 |
13 | * <mcb30 at users.sourceforge.net> (#1035454) |
593370a4 |
14 | * StartTLS code by John Lane |
15 | * <starfry at users.sourceforge.net> (#1197703) |
ae4d36f7 |
16 | * |
981681d5 |
17 | * @version $Id$ |
18 | * @package squirrelmail |
19 | * @subpackage addressbook |
20 | */ |
21 | |
22 | /** |
35586184 |
23 | * Address book backend for LDAP server |
24 | * |
25 | * An array with the following elements must be passed to |
3d1fa376 |
26 | * the class constructor (elements marked ? are optional) |
27 | * |
28 | * Main settings: |
981681d5 |
29 | * <pre> |
35586184 |
30 | * host => LDAP server hostname/IP-address |
31 | * base => LDAP server root (base dn). Empty string allowed. |
32 | * ? port => LDAP server TCP port number (default: 389) |
33 | * ? charset => LDAP server charset (default: utf-8) |
34 | * ? name => Name for LDAP server (default "LDAP: hostname") |
35 | * Used to tag the result data |
36 | * ? maxrows => Maximum # of rows in search result |
37 | * ? timeout => Timeout for LDAP operations (in seconds, default: 30) |
38 | * Might not work for all LDAP libraries or servers. |
30e9932c |
39 | * ? binddn => LDAP Bind DN. |
40 | * ? bindpw => LDAP Bind Password. |
41 | * ? protocol => LDAP Bind protocol. |
3d1fa376 |
42 | * </pre> |
43 | * Advanced settings: |
44 | * <pre> |
45 | * ? filter => Filter expression to limit ldap searches |
46 | * ? limit_scope => Limits scope to base DN (Specific to Win2k3 ADS). |
47 | * ? listing => Controls listing of LDAP directory. |
593370a4 |
48 | * ? search_tree => Controls subtree or one level search. |
49 | * ? starttls => Controls use of StartTLS on LDAP connections |
981681d5 |
50 | * </pre> |
35586184 |
51 | * NOTE. This class should not be used directly. Use the |
52 | * "AddressBook" class instead. |
d6c32258 |
53 | * @package squirrelmail |
a9d318b0 |
54 | * @subpackage addressbook |
35586184 |
55 | */ |
35586184 |
56 | class abook_ldap_server extends addressbook_backend { |
981681d5 |
57 | /** |
58 | * @var string backend type |
59 | */ |
06b4facd |
60 | var $btype = 'remote'; |
981681d5 |
61 | /** |
62 | * @var string backend name |
63 | */ |
06b4facd |
64 | var $bname = 'ldap_server'; |
65 | |
66 | /* Parameters changed by class */ |
981681d5 |
67 | /** |
68 | * @var string displayed name |
69 | */ |
06b4facd |
70 | var $sname = 'LDAP'; /* Service name */ |
981681d5 |
71 | /** |
72 | * @var string LDAP server name or address or url |
73 | */ |
74 | var $server = ''; |
75 | /** |
76 | * @var integer LDAP server port |
77 | */ |
78 | var $port = 389; |
79 | /** |
80 | * @var string LDAP base DN |
81 | */ |
82 | var $basedn = ''; |
83 | /** |
84 | * @var string charset used for entries in LDAP server |
85 | */ |
86 | var $charset = 'utf-8'; |
87 | /** |
88 | * @var object PHP LDAP link ID |
89 | */ |
90 | var $linkid = false; |
91 | /** |
92 | * @var bool True if LDAP server is bound |
93 | */ |
94 | var $bound = false; |
95 | /** |
96 | * @var integer max rows in result |
97 | */ |
98 | var $maxrows = 250; |
ae4d36f7 |
99 | /** |
100 | * @var string ldap filter |
101 | * @since 1.5.1 |
102 | */ |
103 | var $filter = ''; |
981681d5 |
104 | /** |
105 | * @var integer timeout of LDAP operations (in seconds) |
106 | */ |
107 | var $timeout = 30; |
108 | /** |
109 | * @var string DN to bind to (non-anonymous bind) |
110 | * @since 1.5.0 and 1.4.3 |
111 | */ |
112 | var $binddn = ''; |
113 | /** |
114 | * @var string password to bind with (non-anonymous bind) |
115 | * @since 1.5.0 and 1.4.3 |
116 | */ |
117 | var $bindpw = ''; |
118 | /** |
119 | * @var integer protocol used to connect to ldap server |
120 | * @since 1.5.0 and 1.4.3 |
121 | */ |
122 | var $protocol = ''; |
ae4d36f7 |
123 | /** |
124 | * @var boolean limits scope to base dn |
125 | * @since 1.5.1 |
126 | */ |
127 | var $limit_scope = false; |
3d1fa376 |
128 | /** |
129 | * @var boolean controls listing of directory |
130 | * @since 1.5.1 |
131 | */ |
132 | var $listing = false; |
593370a4 |
133 | /** |
134 | * @var boolean controls ldap search type. |
135 | * only first level entries are displayed if set to false |
136 | * @since 1.5.1 |
137 | */ |
138 | var $search_tree = true; |
139 | /** |
140 | * @var boolean controls use of StartTLS on ldap |
141 | * connections. Requires php 4.2+ and protocol >= 3 |
142 | * @since 1.5.1 |
143 | */ |
144 | var $starttls = false; |
981681d5 |
145 | |
146 | /** |
147 | * Constructor. Connects to database |
148 | * @param array connection options |
149 | */ |
06b4facd |
150 | function abook_ldap_server($param) { |
151 | if(!function_exists('ldap_connect')) { |
ae4d36f7 |
152 | $this->set_error(_("PHP install does not have LDAP support.")); |
06b4facd |
153 | return; |
154 | } |
155 | if(is_array($param)) { |
156 | $this->server = $param['host']; |
157 | $this->basedn = $param['base']; |
ae4d36f7 |
158 | |
159 | if(!empty($param['port'])) |
06b4facd |
160 | $this->port = $param['port']; |
ae4d36f7 |
161 | |
162 | if(!empty($param['charset'])) |
06b4facd |
163 | $this->charset = strtolower($param['charset']); |
ae4d36f7 |
164 | |
165 | if(isset($param['maxrows'])) |
06b4facd |
166 | $this->maxrows = $param['maxrows']; |
ae4d36f7 |
167 | |
168 | if(isset($param['timeout'])) |
06b4facd |
169 | $this->timeout = $param['timeout']; |
ae4d36f7 |
170 | |
171 | if(isset($param['binddn'])) |
30e9932c |
172 | $this->binddn = $param['binddn']; |
ae4d36f7 |
173 | |
174 | if(isset($param['bindpw'])) |
30e9932c |
175 | $this->bindpw = $param['bindpw']; |
ae4d36f7 |
176 | |
177 | if(isset($param['protocol'])) |
593370a4 |
178 | $this->protocol = (int) $param['protocol']; |
ae4d36f7 |
179 | |
180 | if(isset($param['filter'])) |
181 | $this->filter = trim($param['filter']); |
182 | |
183 | if(isset($param['limit_scope'])) |
593370a4 |
184 | $this->limit_scope = (bool) $param['limit_scope']; |
ae4d36f7 |
185 | |
3d1fa376 |
186 | if(isset($param['listing'])) |
593370a4 |
187 | $this->listing = (bool) $param['listing']; |
188 | |
189 | if(isset($param['search_tree'])) |
190 | $this->search_tree = (bool) $param['search_tree']; |
191 | |
192 | if(isset($param['starttls'])) |
193 | $this->starttls = (bool) $param['starttls']; |
3d1fa376 |
194 | |
06b4facd |
195 | if(empty($param['name'])) { |
196 | $this->sname = 'LDAP: ' . $param['host']; |
ae4d36f7 |
197 | } else { |
06b4facd |
198 | $this->sname = $param['name']; |
199 | } |
62f7daa5 |
200 | |
ae4d36f7 |
201 | /* |
202 | * don't open LDAP server on addressbook_init(), |
203 | * open ldap connection only on search. Speeds up |
204 | * addressbook_init() call. |
205 | */ |
206 | // $this->open(true); |
06b4facd |
207 | } else { |
208 | $this->set_error('Invalid argument to constructor'); |
209 | } |
210 | } |
211 | |
212 | |
981681d5 |
213 | /** |
214 | * Open the LDAP server. |
215 | * @param bool $new is it a new connection |
216 | * @return bool |
217 | */ |
06b4facd |
218 | function open($new = false) { |
219 | $this->error = ''; |
62f7daa5 |
220 | |
06b4facd |
221 | /* Connection is already open */ |
222 | if($this->linkid != false && !$new) { |
223 | return true; |
224 | } |
62f7daa5 |
225 | |
06b4facd |
226 | $this->linkid = @ldap_connect($this->server, $this->port); |
593370a4 |
227 | /** |
228 | * check if connection was successful |
229 | * It does not work with OpenLDAP 2.x libraries. Connect error will be |
230 | * displayed only on ldap command that tries to make connection |
231 | * (ldap_start_tls or ldap_bind). |
232 | */ |
06b4facd |
233 | if(!$this->linkid) { |
593370a4 |
234 | return $this->set_error($this->ldap_error('ldap_connect failed')); |
06b4facd |
235 | } |
62f7daa5 |
236 | |
981681d5 |
237 | if(!empty($this->protocol)) { |
593370a4 |
238 | // make sure that ldap_set_option() is available before using it |
239 | if(! function_exists('ldap_set_option') || |
240 | !@ldap_set_option($this->linkid, LDAP_OPT_PROTOCOL_VERSION, $this->protocol)) { |
241 | return $this->set_error('unable to set ldap protocol number'); |
242 | } |
243 | } |
244 | |
245 | /** |
246 | * http://www.php.net/ldap-start-tls |
247 | * Check if v3 or newer protocol is used, |
248 | * check if ldap_start_tls function is available. |
8f227330 |
249 | * Silently ignore setting, if these requirements are not satisfied. |
250 | * Break with error message if somebody tries to start TLS on |
251 | * ldaps or socket connection. |
593370a4 |
252 | */ |
253 | if($this->starttls && |
254 | !empty($this->protocol) && $this->protocol >= 3 && |
255 | function_exists('ldap_start_tls') ) { |
8f227330 |
256 | // make sure that $this->server is not ldaps:// or ldapi:// URL. |
257 | if (preg_match("/^ldap[si]:\/\/.+/i",$this->server)) { |
258 | return $this->set_error("you can't enable starttls on ldaps and ldapi connections."); |
593370a4 |
259 | } |
593370a4 |
260 | |
261 | // try starting tls |
262 | if (! @ldap_start_tls($this->linkid)) { |
263 | // set error if call fails |
264 | return $this->set_error($this->ldap_error('ldap_start_tls failed')); |
981681d5 |
265 | } |
266 | } |
30e9932c |
267 | |
ae4d36f7 |
268 | if(!empty($this->limit_scope) && $this->limit_scope) { |
269 | if(empty($this->protocol) || intval($this->protocol) < 3) { |
270 | return $this->set_error('limit_scope requires protocol >= 3'); |
271 | } |
272 | // See http://msdn.microsoft.com/library/en-us/ldap/ldap/ldap_server_domain_scope_oid.asp |
273 | $ctrl = array ( "oid" => "1.2.840.113556.1.4.1339", "iscritical" => TRUE ); |
593370a4 |
274 | /* |
275 | * Option is set only during connection. |
276 | * It does not cause immediate errors with OpenLDAP 2.x libraries. |
277 | */ |
278 | if(! function_exists('ldap_set_option') || |
279 | !@ldap_set_option($this->linkid, LDAP_OPT_SERVER_CONTROLS, array($ctrl))) { |
280 | return $this->set_error($this->ldap_error('limit domain scope failed')); |
ae4d36f7 |
281 | } |
282 | } |
283 | |
593370a4 |
284 | // authenticated bind |
30e9932c |
285 | if(!empty($this->binddn)) { |
286 | if(!@ldap_bind($this->linkid, $this->binddn, $this->bindpw)) { |
593370a4 |
287 | return $this->set_error($this->ldap_error('authenticated ldap_bind failed')); |
288 | } |
30e9932c |
289 | } else { |
593370a4 |
290 | // anonymous bind |
981681d5 |
291 | if(!@ldap_bind($this->linkid)) { |
593370a4 |
292 | return $this->set_error($this->ldap_error('anonymous ldap_bind failed')); |
981681d5 |
293 | } |
06b4facd |
294 | } |
30e9932c |
295 | |
06b4facd |
296 | $this->bound = true; |
62f7daa5 |
297 | |
06b4facd |
298 | return true; |
299 | } |
300 | |
981681d5 |
301 | /** |
302 | * Encode string to the charset used by this LDAP server |
303 | * @param string string that has to be encoded |
304 | * @return string encoded string |
305 | */ |
06b4facd |
306 | function charset_encode($str) { |
b64dd897 |
307 | global $default_charset; |
308 | if($this->charset != $default_charset) { |
309 | return charset_convert($default_charset,$str,$this->charset,false); |
06b4facd |
310 | } else { |
311 | return $str; |
312 | } |
313 | } |
314 | |
981681d5 |
315 | /** |
b64dd897 |
316 | * Decode from charset used by this LDAP server to charset used by translation |
981681d5 |
317 | * |
598294a7 |
318 | * Uses SquirrelMail charset_decode functions |
981681d5 |
319 | * @param string string that has to be decoded |
320 | * @return string decoded string |
321 | */ |
06b4facd |
322 | function charset_decode($str) { |
981681d5 |
323 | global $default_charset; |
324 | if ($this->charset != $default_charset) { |
b64dd897 |
325 | return charset_convert($this->charset,$str,$default_charset,false); |
06b4facd |
326 | } else { |
327 | return $str; |
328 | } |
329 | } |
330 | |
d58ed98f |
331 | /** |
332 | * Sanitizes ldap search strings. |
333 | * See rfc2254 |
334 | * @link http://www.faqs.org/rfcs/rfc2254.html |
ae4d36f7 |
335 | * @since 1.5.1 and 1.4.5 |
d58ed98f |
336 | * @param string $string |
337 | * @return string sanitized string |
338 | */ |
339 | function ldapspecialchars($string) { |
340 | $sanitized=array('\\' => '\5c', |
341 | '*' => '\2a', |
342 | '(' => '\28', |
343 | ')' => '\29', |
344 | "\x00" => '\00'); |
345 | |
346 | return str_replace(array_keys($sanitized),array_values($sanitized),$string); |
347 | } |
62f7daa5 |
348 | |
981681d5 |
349 | /** |
3d1fa376 |
350 | * Search LDAP server. |
351 | * |
352 | * Warning: You must make sure that ldap query is correctly formated and |
353 | * sanitize use of special ldap keywords. |
354 | * @param string $expression ldap query |
355 | * @return array search results (false on error) |
356 | * @since 1.5.1 |
981681d5 |
357 | */ |
3d1fa376 |
358 | function ldap_search($expression) { |
06b4facd |
359 | /* Make sure connection is there */ |
360 | if(!$this->open()) { |
361 | return false; |
362 | } |
62f7daa5 |
363 | |
593370a4 |
364 | if ($this->search_tree) { |
365 | // ldap_search - search subtree |
366 | $sret = @ldap_search($this->linkid, $this->basedn, $expression, |
367 | array('dn', 'o', 'ou', 'sn', 'givenname', 'cn', 'mail'), |
368 | 0, $this->maxrows, $this->timeout); |
369 | } else { |
370 | // ldap_list - search one level |
371 | $sret = @ldap_list($this->linkid, $this->basedn, $expression, |
372 | array('dn', 'o', 'ou', 'sn', 'givenname', 'cn', 'mail'), |
373 | 0, $this->maxrows, $this->timeout); |
374 | } |
62f7daa5 |
375 | |
593370a4 |
376 | /* Return error if search failed */ |
06b4facd |
377 | if(!$sret) { |
593370a4 |
378 | return $this->set_error($this->ldap_error('ldap_search failed')); |
06b4facd |
379 | } |
62f7daa5 |
380 | |
06b4facd |
381 | if(@ldap_count_entries($this->linkid, $sret) <= 0) { |
382 | return array(); |
383 | } |
62f7daa5 |
384 | |
06b4facd |
385 | /* Get results */ |
386 | $ret = array(); |
387 | $returned_rows = 0; |
388 | $res = @ldap_get_entries($this->linkid, $sret); |
389 | for($i = 0 ; $i < $res['count'] ; $i++) { |
390 | $row = $res[$i]; |
62f7daa5 |
391 | |
06b4facd |
392 | /* Extract data common for all e-mail addresses |
b682f335 |
393 | * of an object. Use only the first name */ |
06b4facd |
394 | $nickname = $this->charset_decode($row['dn']); |
b682f335 |
395 | |
396 | /** |
397 | * calculate length of basedn and remove it from nickname |
398 | * ignore whitespaces between RDNs |
399 | * Nicknames are shorter and still unique |
400 | */ |
401 | $basedn_len=strlen(preg_replace('/,\s*/',',',trim($this->basedn))); |
402 | $nickname=substr(preg_replace('/,\s*/',',',$nickname),0,(-1 - $basedn_len)); |
3d1fa376 |
403 | |
06b4facd |
404 | $fullname = $this->charset_decode($row['cn'][0]); |
62f7daa5 |
405 | |
06b4facd |
406 | if(!empty($row['ou'][0])) { |
407 | $label = $this->charset_decode($row['ou'][0]); |
408 | } |
409 | else if(!empty($row['o'][0])) { |
410 | $label = $this->charset_decode($row['o'][0]); |
411 | } else { |
412 | $label = ''; |
413 | } |
62f7daa5 |
414 | |
06b4facd |
415 | if(empty($row['givenname'][0])) { |
416 | $firstname = ''; |
417 | } else { |
418 | $firstname = $this->charset_decode($row['givenname'][0]); |
419 | } |
62f7daa5 |
420 | |
06b4facd |
421 | if(empty($row['sn'][0])) { |
422 | $surname = ''; |
423 | } else { |
424 | $surname = $this->charset_decode($row['sn'][0]); |
425 | } |
62f7daa5 |
426 | |
06b4facd |
427 | /* Add one row to result for each e-mail address */ |
428 | if(isset($row['mail']['count'])) { |
429 | for($j = 0 ; $j < $row['mail']['count'] ; $j++) { |
430 | array_push($ret, array('nickname' => $nickname, |
431 | 'name' => $fullname, |
432 | 'firstname' => $firstname, |
433 | 'lastname' => $surname, |
434 | 'email' => $row['mail'][$j], |
435 | 'label' => $label, |
06b4facd |
436 | 'backend' => $this->bnum, |
437 | 'source' => &$this->sname)); |
62f7daa5 |
438 | |
06b4facd |
439 | // Limit number of hits |
440 | $returned_rows++; |
62f7daa5 |
441 | if(($returned_rows >= $this->maxrows) && |
06b4facd |
442 | ($this->maxrows > 0) ) { |
443 | ldap_free_result($sret); |
444 | return $ret; |
445 | } |
446 | |
447 | } // for($j ...) |
448 | |
449 | } // isset($row['mail']['count']) |
62f7daa5 |
450 | |
06b4facd |
451 | } |
62f7daa5 |
452 | |
06b4facd |
453 | ldap_free_result($sret); |
454 | return $ret; |
3d1fa376 |
455 | } |
456 | |
593370a4 |
457 | /** |
458 | * Get error from LDAP resource if possible |
459 | * |
460 | * Should get error from server using the ldap_errno() and ldap_err2str() functions |
461 | * @param string $sError error message used when ldap error functions |
462 | * and connection resource are unavailable |
463 | * @return string error message |
464 | * @since 1.5.1 |
465 | */ |
466 | function ldap_error($sError) { |
467 | // it is possible that function_exists() tests are not needed |
468 | if(function_exists('ldap_err2str') && |
469 | function_exists('ldap_errno') && |
470 | is_resource($this->linkid)) { |
471 | return ldap_err2str(ldap_errno($this->linkid)); |
472 | // return ldap_error($this->linkid); |
473 | } else { |
474 | return $sError; |
475 | } |
476 | } |
477 | |
3d1fa376 |
478 | /* ========================== Public ======================== */ |
479 | |
480 | /** |
481 | * Search the LDAP server |
482 | * @param string $expr search expression |
483 | * @return array search results |
484 | */ |
485 | function search($expr) { |
486 | /* To be replaced by advanded search expression parsing */ |
487 | if(is_array($expr)) return false; |
488 | |
489 | // don't allow wide search when listing is disabled. |
327e2d96 |
490 | if ($expr=='*' && ! $this->listing) { |
491 | return array(); |
492 | } elseif ($expr=='*') { |
493 | // allow use of wildcard when listing is enabled. |
494 | $expression = '(cn=*)'; |
495 | } else { |
496 | /* Convert search from user's charset to the one used in ldap */ |
497 | $expr = $this->charset_encode($expr); |
3d1fa376 |
498 | |
327e2d96 |
499 | /* Make sure that search does not contain ldap special chars */ |
500 | $expression = '(cn=*' . $this->ldapspecialchars($expr) . '*)'; |
3d1fa376 |
501 | |
327e2d96 |
502 | /* Undo sanitizing of * symbol */ |
503 | $expression = str_replace('\2a','*',$expression); |
504 | /* TODO: implement any single character (?) matching */ |
505 | } |
3d1fa376 |
506 | |
507 | /* Add search filtering */ |
508 | if ($this->filter!='') |
509 | $expression = '(&' . $this->filter . $expression . ')'; |
510 | |
511 | /* Use internal search function and return search results */ |
512 | return $this->ldap_search($expression); |
513 | } |
06b4facd |
514 | |
515 | |
981681d5 |
516 | /** |
517 | * List all entries present in LDAP server |
06b4facd |
518 | * |
3d1fa376 |
519 | * maxrows setting might limit list of returned entries. |
981681d5 |
520 | * Careful with this -- it could get quite large for big sites. |
521 | * @return array all entries in ldap server |
522 | */ |
523 | function list_addr() { |
3d1fa376 |
524 | if (! $this->listing) |
525 | return array(); |
526 | |
527 | /* set wide search expression */ |
528 | $expression = '(cn=*)'; |
529 | |
530 | /* add filtering */ |
531 | if ($this->filter!='') |
532 | $expression = '(&' . $this->filter . $expression .')'; |
533 | |
534 | /* use internal search function and return search results */ |
535 | return $this->ldap_search($expression); |
981681d5 |
536 | } |
06b4facd |
537 | } |
62f7daa5 |
538 | ?> |