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