5100704d |
1 | <?php |
4b4abf93 |
2 | |
35586184 |
3 | /** |
4 | * abook_ldap_server.php |
5 | * |
981681d5 |
6 | * Address book backend for LDAP server |
7 | * |
ae4d36f7 |
8 | * LDAP filtering code by Tim Bell |
9 | * <bhat at users.sourceforge.net> (#539534) |
f8a1ed5a |
10 | * ADS limit_scope code by Michael Brown |
ae4d36f7 |
11 | * <mcb30 at users.sourceforge.net> (#1035454) |
593370a4 |
12 | * StartTLS code by John Lane |
13 | * <starfry at users.sourceforge.net> (#1197703) |
664fd7a0 |
14 | * Code for remove, add, modify, lookup by David Härdeman |
15 | * <david at 2gen.com> (#1495763) |
ae4d36f7 |
16 | * |
664fd7a0 |
17 | * This backend uses LDAP person (RFC2256), organizationalPerson (RFC2256) |
18 | * and inetOrgPerson (RFC2798) objects and dn, description, sn, givenname, |
19 | * cn, mail attributes. Other attributes are ignored. |
20 | * |
4b5049de |
21 | * @copyright © 1999-2007 The SquirrelMail Project Team |
4b4abf93 |
22 | * @license http://opensource.org/licenses/gpl-license.php GNU Public License |
981681d5 |
23 | * @version $Id$ |
24 | * @package squirrelmail |
25 | * @subpackage addressbook |
26 | */ |
27 | |
28 | /** |
35586184 |
29 | * Address book backend for LDAP server |
30 | * |
31 | * An array with the following elements must be passed to |
3d1fa376 |
32 | * the class constructor (elements marked ? are optional) |
33 | * |
34 | * Main settings: |
981681d5 |
35 | * <pre> |
664fd7a0 |
36 | * host => LDAP server hostname, IP-address or any other URI compatible |
37 | * with used LDAP library. |
35586184 |
38 | * base => LDAP server root (base dn). Empty string allowed. |
39 | * ? port => LDAP server TCP port number (default: 389) |
40 | * ? charset => LDAP server charset (default: utf-8) |
41 | * ? name => Name for LDAP server (default "LDAP: hostname") |
42 | * Used to tag the result data |
43 | * ? maxrows => Maximum # of rows in search result |
44 | * ? timeout => Timeout for LDAP operations (in seconds, default: 30) |
45 | * Might not work for all LDAP libraries or servers. |
30e9932c |
46 | * ? binddn => LDAP Bind DN. |
47 | * ? bindpw => LDAP Bind Password. |
48 | * ? protocol => LDAP Bind protocol. |
3d1fa376 |
49 | * </pre> |
50 | * Advanced settings: |
51 | * <pre> |
ddf4a1ba |
52 | * ? filter => Filter expression to limit ldap search results. |
53 | * You can use this to *limit* the result set, based on specific |
54 | * requirements. The filter must be enclosed in parentheses, e.g.: |
55 | * '(objectclass=mailRecipient)' |
56 | * or '(&(objectclass=mailRecipient)(obectclass=myCustomClass))' |
57 | * The default value is empty. |
58 | * |
59 | * ? search_expression => Custom expression to expand ldap searches. |
60 | * This can help *expand* the result set, because of hits in more |
61 | * LDAP attributes. It must be a printf()-style string with either |
62 | * one placeholder '%s', or, if you want to repeat the expression |
63 | * many times, '%1$s'. The default value is: |
64 | * '(|(cn=*%1$s*)(mail=*%1$s*)(sn=*%1$s*))' |
65 | * that is, the search expression is search in the fields cn (common |
66 | * name), sn (surname) and mail. |
67 | * |
3d1fa376 |
68 | * ? limit_scope => Limits scope to base DN (Specific to Win2k3 ADS). |
69 | * ? listing => Controls listing of LDAP directory. |
664fd7a0 |
70 | * ? writeable => Controls write access to address book |
593370a4 |
71 | * ? search_tree => Controls subtree or one level search. |
72 | * ? starttls => Controls use of StartTLS on LDAP connections |
981681d5 |
73 | * </pre> |
664fd7a0 |
74 | * NOTE. This class should not be used directly. Use addressbook_init() |
75 | * function instead. |
d6c32258 |
76 | * @package squirrelmail |
a9d318b0 |
77 | * @subpackage addressbook |
35586184 |
78 | */ |
35586184 |
79 | class abook_ldap_server extends addressbook_backend { |
981681d5 |
80 | /** |
81 | * @var string backend type |
82 | */ |
06b4facd |
83 | var $btype = 'remote'; |
981681d5 |
84 | /** |
85 | * @var string backend name |
86 | */ |
06b4facd |
87 | var $bname = 'ldap_server'; |
88 | |
89 | /* Parameters changed by class */ |
981681d5 |
90 | /** |
91 | * @var string displayed name |
92 | */ |
06b4facd |
93 | var $sname = 'LDAP'; /* Service name */ |
981681d5 |
94 | /** |
95 | * @var string LDAP server name or address or url |
96 | */ |
97 | var $server = ''; |
98 | /** |
99 | * @var integer LDAP server port |
100 | */ |
101 | var $port = 389; |
102 | /** |
103 | * @var string LDAP base DN |
104 | */ |
105 | var $basedn = ''; |
106 | /** |
107 | * @var string charset used for entries in LDAP server |
108 | */ |
109 | var $charset = 'utf-8'; |
110 | /** |
111 | * @var object PHP LDAP link ID |
112 | */ |
113 | var $linkid = false; |
114 | /** |
115 | * @var bool True if LDAP server is bound |
116 | */ |
117 | var $bound = false; |
118 | /** |
119 | * @var integer max rows in result |
120 | */ |
121 | var $maxrows = 250; |
ae4d36f7 |
122 | /** |
123 | * @var string ldap filter |
124 | * @since 1.5.1 |
125 | */ |
126 | var $filter = ''; |
ddf4a1ba |
127 | /** |
128 | * @var string printf()-style ldap search expression. |
129 | * The default is to search for same string in cn, mail and sn. |
130 | * @since 1.5.2 |
131 | */ |
132 | var $search_expression = '(|(cn=*%1$s*)(mail=*%1$s*)(sn=*%1$s*))'; |
981681d5 |
133 | /** |
134 | * @var integer timeout of LDAP operations (in seconds) |
135 | */ |
136 | var $timeout = 30; |
137 | /** |
138 | * @var string DN to bind to (non-anonymous bind) |
139 | * @since 1.5.0 and 1.4.3 |
140 | */ |
141 | var $binddn = ''; |
142 | /** |
143 | * @var string password to bind with (non-anonymous bind) |
144 | * @since 1.5.0 and 1.4.3 |
145 | */ |
146 | var $bindpw = ''; |
147 | /** |
148 | * @var integer protocol used to connect to ldap server |
149 | * @since 1.5.0 and 1.4.3 |
150 | */ |
151 | var $protocol = ''; |
ae4d36f7 |
152 | /** |
153 | * @var boolean limits scope to base dn |
154 | * @since 1.5.1 |
155 | */ |
156 | var $limit_scope = false; |
3d1fa376 |
157 | /** |
158 | * @var boolean controls listing of directory |
159 | * @since 1.5.1 |
160 | */ |
161 | var $listing = false; |
664fd7a0 |
162 | /** |
163 | * @var boolean true if removing/adding/modifying entries is allowed |
164 | * @since 1.5.2 |
165 | */ |
9ab998a6 |
166 | var $writeable = false; |
593370a4 |
167 | /** |
168 | * @var boolean controls ldap search type. |
169 | * only first level entries are displayed if set to false |
170 | * @since 1.5.1 |
171 | */ |
172 | var $search_tree = true; |
173 | /** |
174 | * @var boolean controls use of StartTLS on ldap |
175 | * connections. Requires php 4.2+ and protocol >= 3 |
176 | * @since 1.5.1 |
177 | */ |
178 | var $starttls = false; |
981681d5 |
179 | |
180 | /** |
181 | * Constructor. Connects to database |
182 | * @param array connection options |
183 | */ |
06b4facd |
184 | function abook_ldap_server($param) { |
185 | if(!function_exists('ldap_connect')) { |
ae4d36f7 |
186 | $this->set_error(_("PHP install does not have LDAP support.")); |
06b4facd |
187 | return; |
188 | } |
189 | if(is_array($param)) { |
190 | $this->server = $param['host']; |
664fd7a0 |
191 | // remove whitespace from basedn |
192 | $this->basedn = preg_replace('/,\s*/',',',trim($param['base'])); |
ae4d36f7 |
193 | |
194 | if(!empty($param['port'])) |
06b4facd |
195 | $this->port = $param['port']; |
ae4d36f7 |
196 | |
197 | if(!empty($param['charset'])) |
06b4facd |
198 | $this->charset = strtolower($param['charset']); |
ae4d36f7 |
199 | |
200 | if(isset($param['maxrows'])) |
06b4facd |
201 | $this->maxrows = $param['maxrows']; |
ae4d36f7 |
202 | |
203 | if(isset($param['timeout'])) |
06b4facd |
204 | $this->timeout = $param['timeout']; |
ae4d36f7 |
205 | |
206 | if(isset($param['binddn'])) |
30e9932c |
207 | $this->binddn = $param['binddn']; |
ae4d36f7 |
208 | |
209 | if(isset($param['bindpw'])) |
30e9932c |
210 | $this->bindpw = $param['bindpw']; |
ae4d36f7 |
211 | |
212 | if(isset($param['protocol'])) |
593370a4 |
213 | $this->protocol = (int) $param['protocol']; |
ae4d36f7 |
214 | |
215 | if(isset($param['filter'])) |
216 | $this->filter = trim($param['filter']); |
ddf4a1ba |
217 | |
218 | if(isset($param['search_expression']) && |
219 | (strstr($param['search_expression'], '%s') || strstr($param['search_expression'], '%1$s'))) { |
220 | $this->search_expression = trim($param['search_expression']); |
221 | } |
ae4d36f7 |
222 | |
223 | if(isset($param['limit_scope'])) |
593370a4 |
224 | $this->limit_scope = (bool) $param['limit_scope']; |
ae4d36f7 |
225 | |
3d1fa376 |
226 | if(isset($param['listing'])) |
593370a4 |
227 | $this->listing = (bool) $param['listing']; |
228 | |
664fd7a0 |
229 | if(isset($param['writeable'])) { |
230 | $this->writeable = (bool) $param['writeable']; |
231 | // switch backend type to local, if it is writable |
232 | if($this->writeable) $this->btype = 'local'; |
233 | } |
234 | |
593370a4 |
235 | if(isset($param['search_tree'])) |
236 | $this->search_tree = (bool) $param['search_tree']; |
237 | |
238 | if(isset($param['starttls'])) |
239 | $this->starttls = (bool) $param['starttls']; |
3d1fa376 |
240 | |
06b4facd |
241 | if(empty($param['name'])) { |
242 | $this->sname = 'LDAP: ' . $param['host']; |
ae4d36f7 |
243 | } else { |
06b4facd |
244 | $this->sname = $param['name']; |
245 | } |
62f7daa5 |
246 | |
ae4d36f7 |
247 | /* |
248 | * don't open LDAP server on addressbook_init(), |
249 | * open ldap connection only on search. Speeds up |
250 | * addressbook_init() call. |
251 | */ |
252 | // $this->open(true); |
06b4facd |
253 | } else { |
254 | $this->set_error('Invalid argument to constructor'); |
255 | } |
256 | } |
257 | |
258 | |
981681d5 |
259 | /** |
260 | * Open the LDAP server. |
261 | * @param bool $new is it a new connection |
262 | * @return bool |
263 | */ |
06b4facd |
264 | function open($new = false) { |
265 | $this->error = ''; |
62f7daa5 |
266 | |
06b4facd |
267 | /* Connection is already open */ |
268 | if($this->linkid != false && !$new) { |
269 | return true; |
270 | } |
62f7daa5 |
271 | |
06b4facd |
272 | $this->linkid = @ldap_connect($this->server, $this->port); |
593370a4 |
273 | /** |
274 | * check if connection was successful |
275 | * It does not work with OpenLDAP 2.x libraries. Connect error will be |
276 | * displayed only on ldap command that tries to make connection |
277 | * (ldap_start_tls or ldap_bind). |
278 | */ |
06b4facd |
279 | if(!$this->linkid) { |
593370a4 |
280 | return $this->set_error($this->ldap_error('ldap_connect failed')); |
06b4facd |
281 | } |
62f7daa5 |
282 | |
981681d5 |
283 | if(!empty($this->protocol)) { |
593370a4 |
284 | // make sure that ldap_set_option() is available before using it |
285 | if(! function_exists('ldap_set_option') || |
286 | !@ldap_set_option($this->linkid, LDAP_OPT_PROTOCOL_VERSION, $this->protocol)) { |
287 | return $this->set_error('unable to set ldap protocol number'); |
288 | } |
289 | } |
290 | |
291 | /** |
292 | * http://www.php.net/ldap-start-tls |
293 | * Check if v3 or newer protocol is used, |
294 | * check if ldap_start_tls function is available. |
8f227330 |
295 | * Silently ignore setting, if these requirements are not satisfied. |
296 | * Break with error message if somebody tries to start TLS on |
297 | * ldaps or socket connection. |
593370a4 |
298 | */ |
299 | if($this->starttls && |
300 | !empty($this->protocol) && $this->protocol >= 3 && |
301 | function_exists('ldap_start_tls') ) { |
8f227330 |
302 | // make sure that $this->server is not ldaps:// or ldapi:// URL. |
303 | if (preg_match("/^ldap[si]:\/\/.+/i",$this->server)) { |
304 | return $this->set_error("you can't enable starttls on ldaps and ldapi connections."); |
593370a4 |
305 | } |
593370a4 |
306 | |
307 | // try starting tls |
308 | if (! @ldap_start_tls($this->linkid)) { |
309 | // set error if call fails |
310 | return $this->set_error($this->ldap_error('ldap_start_tls failed')); |
981681d5 |
311 | } |
312 | } |
30e9932c |
313 | |
ae4d36f7 |
314 | if(!empty($this->limit_scope) && $this->limit_scope) { |
315 | if(empty($this->protocol) || intval($this->protocol) < 3) { |
316 | return $this->set_error('limit_scope requires protocol >= 3'); |
317 | } |
318 | // See http://msdn.microsoft.com/library/en-us/ldap/ldap/ldap_server_domain_scope_oid.asp |
319 | $ctrl = array ( "oid" => "1.2.840.113556.1.4.1339", "iscritical" => TRUE ); |
593370a4 |
320 | /* |
321 | * Option is set only during connection. |
322 | * It does not cause immediate errors with OpenLDAP 2.x libraries. |
323 | */ |
324 | if(! function_exists('ldap_set_option') || |
325 | !@ldap_set_option($this->linkid, LDAP_OPT_SERVER_CONTROLS, array($ctrl))) { |
326 | return $this->set_error($this->ldap_error('limit domain scope failed')); |
ae4d36f7 |
327 | } |
328 | } |
329 | |
593370a4 |
330 | // authenticated bind |
30e9932c |
331 | if(!empty($this->binddn)) { |
332 | if(!@ldap_bind($this->linkid, $this->binddn, $this->bindpw)) { |
593370a4 |
333 | return $this->set_error($this->ldap_error('authenticated ldap_bind failed')); |
334 | } |
30e9932c |
335 | } else { |
593370a4 |
336 | // anonymous bind |
981681d5 |
337 | if(!@ldap_bind($this->linkid)) { |
593370a4 |
338 | return $this->set_error($this->ldap_error('anonymous ldap_bind failed')); |
981681d5 |
339 | } |
06b4facd |
340 | } |
30e9932c |
341 | |
06b4facd |
342 | $this->bound = true; |
62f7daa5 |
343 | |
06b4facd |
344 | return true; |
345 | } |
346 | |
981681d5 |
347 | /** |
348 | * Encode string to the charset used by this LDAP server |
349 | * @param string string that has to be encoded |
350 | * @return string encoded string |
351 | */ |
06b4facd |
352 | function charset_encode($str) { |
b64dd897 |
353 | global $default_charset; |
354 | if($this->charset != $default_charset) { |
355 | return charset_convert($default_charset,$str,$this->charset,false); |
06b4facd |
356 | } else { |
357 | return $str; |
358 | } |
359 | } |
360 | |
981681d5 |
361 | /** |
b64dd897 |
362 | * Decode from charset used by this LDAP server to charset used by translation |
981681d5 |
363 | * |
598294a7 |
364 | * Uses SquirrelMail charset_decode functions |
981681d5 |
365 | * @param string string that has to be decoded |
366 | * @return string decoded string |
367 | */ |
06b4facd |
368 | function charset_decode($str) { |
981681d5 |
369 | global $default_charset; |
370 | if ($this->charset != $default_charset) { |
b64dd897 |
371 | return charset_convert($this->charset,$str,$default_charset,false); |
06b4facd |
372 | } else { |
373 | return $str; |
374 | } |
375 | } |
376 | |
d58ed98f |
377 | /** |
378 | * Sanitizes ldap search strings. |
379 | * See rfc2254 |
380 | * @link http://www.faqs.org/rfcs/rfc2254.html |
ae4d36f7 |
381 | * @since 1.5.1 and 1.4.5 |
d58ed98f |
382 | * @param string $string |
383 | * @return string sanitized string |
384 | */ |
385 | function ldapspecialchars($string) { |
386 | $sanitized=array('\\' => '\5c', |
387 | '*' => '\2a', |
388 | '(' => '\28', |
389 | ')' => '\29', |
390 | "\x00" => '\00'); |
391 | |
392 | return str_replace(array_keys($sanitized),array_values($sanitized),$string); |
393 | } |
62f7daa5 |
394 | |
664fd7a0 |
395 | /** |
396 | * Prepares user input for use in a ldap query. |
397 | * |
398 | * Function converts input string to character set used in LDAP server |
399 | * (charset_encode() method) and sanitizes it (ldapspecialchars()). |
400 | * |
401 | * @param string $string string to encode |
402 | * @return string ldap encoded string |
403 | * @since 1.5.2 |
404 | */ |
405 | function quotevalue($string) { |
406 | $sanitized = $this->charset_encode($string); |
407 | return $this->ldapspecialchars($sanitized); |
408 | } |
409 | |
981681d5 |
410 | /** |
3d1fa376 |
411 | * Search LDAP server. |
412 | * |
413 | * Warning: You must make sure that ldap query is correctly formated and |
414 | * sanitize use of special ldap keywords. |
415 | * @param string $expression ldap query |
664fd7a0 |
416 | * @param boolean $singleentry (since 1.5.2) whether we are looking for a |
417 | * single entry. Boolean true forces LDAP_SCOPE_BASE search. |
3d1fa376 |
418 | * @return array search results (false on error) |
419 | * @since 1.5.1 |
981681d5 |
420 | */ |
664fd7a0 |
421 | function ldap_search($expression, $singleentry = false) { |
06b4facd |
422 | /* Make sure connection is there */ |
423 | if(!$this->open()) { |
424 | return false; |
425 | } |
62f7daa5 |
426 | |
664fd7a0 |
427 | $attributes = array('dn', 'description', 'sn', 'givenname', 'cn', 'mail'); |
428 | |
429 | if ($singleentry) { |
430 | // ldap_read - search for one single entry |
431 | $sret = @ldap_read($this->linkid, $expression, "objectClass=*", |
432 | $attributes, 0, $this->maxrows, $this->timeout); |
433 | } elseif ($this->search_tree) { |
593370a4 |
434 | // ldap_search - search subtree |
435 | $sret = @ldap_search($this->linkid, $this->basedn, $expression, |
664fd7a0 |
436 | $attributes, 0, $this->maxrows, $this->timeout); |
593370a4 |
437 | } else { |
438 | // ldap_list - search one level |
439 | $sret = @ldap_list($this->linkid, $this->basedn, $expression, |
664fd7a0 |
440 | $attributes, 0, $this->maxrows, $this->timeout); |
593370a4 |
441 | } |
62f7daa5 |
442 | |
593370a4 |
443 | /* Return error if search failed */ |
06b4facd |
444 | if(!$sret) { |
664fd7a0 |
445 | // Check for LDAP_NO_SUCH_OBJECT (0x20 or 32) error |
446 | if (ldap_errno($this->linkid)==32) { |
447 | return array(); |
448 | } else { |
449 | return $this->set_error($this->ldap_error('ldap_search failed')); |
450 | } |
06b4facd |
451 | } |
62f7daa5 |
452 | |
06b4facd |
453 | if(@ldap_count_entries($this->linkid, $sret) <= 0) { |
454 | return array(); |
455 | } |
62f7daa5 |
456 | |
06b4facd |
457 | /* Get results */ |
458 | $ret = array(); |
459 | $returned_rows = 0; |
460 | $res = @ldap_get_entries($this->linkid, $sret); |
461 | for($i = 0 ; $i < $res['count'] ; $i++) { |
462 | $row = $res[$i]; |
62f7daa5 |
463 | |
06b4facd |
464 | /* Extract data common for all e-mail addresses |
b682f335 |
465 | * of an object. Use only the first name */ |
06b4facd |
466 | $nickname = $this->charset_decode($row['dn']); |
b682f335 |
467 | |
468 | /** |
664fd7a0 |
469 | * remove trailing basedn |
470 | * remove whitespaces between RDNs |
471 | * remove leading "cn=" |
472 | * which gives nicknames which are shorter while still unique |
473 | */ |
474 | $nickname = preg_replace('/,\s*/',',', trim($nickname)); |
475 | $offset = strlen($nickname) - strlen($this->basedn); |
476 | |
477 | if($offset > 0 && substr($nickname, $offset) == $this->basedn) { |
478 | $nickname = substr($nickname, 0, $offset); |
479 | if(substr($nickname, -1) == ",") |
480 | $nickname = substr($nickname, 0, -1); |
06b4facd |
481 | } |
664fd7a0 |
482 | if(strncasecmp($nickname, "cn=", 3) == 0) |
483 | $nickname=substr($nickname, 3); |
484 | |
485 | if(empty($row['description'][0])) { |
06b4facd |
486 | $label = ''; |
664fd7a0 |
487 | } else { |
488 | $label = $this->charset_decode($row['description'][0]); |
06b4facd |
489 | } |
62f7daa5 |
490 | |
06b4facd |
491 | if(empty($row['givenname'][0])) { |
492 | $firstname = ''; |
493 | } else { |
494 | $firstname = $this->charset_decode($row['givenname'][0]); |
495 | } |
62f7daa5 |
496 | |
06b4facd |
497 | if(empty($row['sn'][0])) { |
498 | $surname = ''; |
499 | } else { |
664fd7a0 |
500 | // remove whitespace in order to handle sn set to empty string |
501 | $surname = trim($this->charset_decode($row['sn'][0])); |
06b4facd |
502 | } |
62f7daa5 |
503 | |
35235328 |
504 | $fullname = $this->fullname($firstname,$surname); |
664fd7a0 |
505 | |
06b4facd |
506 | /* Add one row to result for each e-mail address */ |
507 | if(isset($row['mail']['count'])) { |
508 | for($j = 0 ; $j < $row['mail']['count'] ; $j++) { |
509 | array_push($ret, array('nickname' => $nickname, |
510 | 'name' => $fullname, |
511 | 'firstname' => $firstname, |
512 | 'lastname' => $surname, |
513 | 'email' => $row['mail'][$j], |
514 | 'label' => $label, |
06b4facd |
515 | 'backend' => $this->bnum, |
516 | 'source' => &$this->sname)); |
62f7daa5 |
517 | |
06b4facd |
518 | // Limit number of hits |
519 | $returned_rows++; |
62f7daa5 |
520 | if(($returned_rows >= $this->maxrows) && |
06b4facd |
521 | ($this->maxrows > 0) ) { |
522 | ldap_free_result($sret); |
523 | return $ret; |
524 | } |
525 | |
526 | } // for($j ...) |
527 | |
528 | } // isset($row['mail']['count']) |
62f7daa5 |
529 | |
06b4facd |
530 | } |
62f7daa5 |
531 | |
06b4facd |
532 | ldap_free_result($sret); |
533 | return $ret; |
3d1fa376 |
534 | } |
535 | |
664fd7a0 |
536 | /** |
537 | * Add an entry to LDAP server. |
538 | * |
539 | * Warning: You must make sure that the arguments are correctly formated and |
540 | * sanitize use of special ldap keywords. |
541 | * @param string $dn the dn of the entry to be added |
542 | * @param array $data the values of the entry to be added |
543 | * @return boolean result (false on error) |
544 | * @since 1.5.2 |
545 | */ |
546 | function ldap_add($dn, $data) { |
547 | /* Make sure connection is there */ |
548 | if(!$this->open()) { |
549 | return false; |
550 | } |
551 | |
552 | if(!@ldap_add($this->linkid, $dn, $data)) { |
553 | $this->set_error(_("Write to address book failed")); |
554 | return false; |
555 | } |
556 | |
557 | return true; |
558 | } |
559 | |
560 | /** |
561 | * Remove an entry from LDAP server. |
562 | * |
563 | * Warning: You must make sure that the argument is correctly formated and |
564 | * sanitize use of special ldap keywords. |
565 | * @param string $dn the dn of the entry to remove |
566 | * @return boolean result (false on error) |
567 | * @since 1.5.2 |
568 | */ |
569 | function ldap_remove($dn) { |
570 | /* Make sure connection is there */ |
571 | if(!$this->open()) { |
572 | return false; |
573 | } |
574 | |
575 | if(!@ldap_delete($this->linkid, $dn)) { |
576 | $this->set_error(_("Removing entry from address book failed")); |
577 | return false; |
578 | } |
579 | |
580 | return true; |
581 | } |
582 | |
583 | /** |
584 | * Rename an entry on LDAP server. |
585 | * |
586 | * Warning: You must make sure that the arguments are correctly formated and |
587 | * sanitize use of special ldap keywords. |
588 | * @param string $sourcedn the dn of the entry to be renamed |
589 | * @param string $targetdn the dn which $sourcedn should be renamed to |
590 | * @param string $parent the dn of the parent entry |
591 | * @return boolean result (false on error) |
592 | * @since 1.5.2 |
593 | */ |
594 | function ldap_rename($sourcedn, $targetdn, $parent) { |
595 | /* Make sure connection is there */ |
596 | if(!$this->open()) { |
597 | return false; |
598 | } |
599 | |
600 | /* Make sure that the protocol version supports rename */ |
601 | if($this->protocol < 3) { |
602 | $this->set_error(_("LDAP rename is not supported by used protocol version")); |
603 | return false; |
604 | } |
605 | /** |
606 | * Function is available only in OpenLDAP 2.x.x or Netscape Directory |
607 | * SDK x.x, and was added in PHP 4.0.5 |
608 | * @todo maybe we can use copy + delete instead of ldap_rename() |
609 | */ |
610 | if(!function_exists('ldap_rename')) { |
611 | $this->set_error(_("LDAP rename is not supported by used LDAP library. You can't change nickname")); |
612 | return false; |
613 | } |
614 | |
615 | /* OK, go for it */ |
616 | if(!@ldap_rename($this->linkid, $sourcedn, $targetdn, $parent, true)) { |
617 | $this->set_error(_("LDAP rename failed")); |
618 | return false; |
619 | } |
620 | |
621 | return true; |
622 | } |
623 | |
624 | /** |
625 | * Modify the values of an entry on LDAP server. |
626 | * |
627 | * Warning: You must make sure that the arguments are correctly formated and |
628 | * sanitize use of special ldap keywords. |
629 | * @param string $dn the dn of the entry to be modified |
630 | * @param array $data the new values of the entry |
631 | * @param array $deleted_attribs attributes that should be deleted. |
632 | * @return bool result (false on error) |
633 | * @since 1.5.2 |
634 | */ |
635 | function ldap_modify($dn, $data, $deleted_attribs) { |
636 | /* Make sure connection is there */ |
637 | if(!$this->open()) { |
638 | return false; |
639 | } |
640 | |
641 | if(!@ldap_modify($this->linkid, $dn, $data)) { |
642 | $this->set_error(_("Write to address book failed")); |
643 | return false; |
644 | } |
645 | |
646 | if (!@ldap_mod_del($this->linkid, $dn, $deleted_attribs)) { |
647 | $this->set_error(_("Unable to remove some field values")); |
648 | return false; |
649 | } |
650 | |
651 | return true; |
652 | } |
653 | |
593370a4 |
654 | /** |
655 | * Get error from LDAP resource if possible |
656 | * |
657 | * Should get error from server using the ldap_errno() and ldap_err2str() functions |
658 | * @param string $sError error message used when ldap error functions |
659 | * and connection resource are unavailable |
660 | * @return string error message |
661 | * @since 1.5.1 |
662 | */ |
663 | function ldap_error($sError) { |
664 | // it is possible that function_exists() tests are not needed |
665 | if(function_exists('ldap_err2str') && |
666 | function_exists('ldap_errno') && |
667 | is_resource($this->linkid)) { |
668 | return ldap_err2str(ldap_errno($this->linkid)); |
669 | // return ldap_error($this->linkid); |
670 | } else { |
671 | return $sError; |
672 | } |
673 | } |
674 | |
3d1fa376 |
675 | /* ========================== Public ======================== */ |
676 | |
677 | /** |
678 | * Search the LDAP server |
679 | * @param string $expr search expression |
680 | * @return array search results |
681 | */ |
682 | function search($expr) { |
683 | /* To be replaced by advanded search expression parsing */ |
684 | if(is_array($expr)) return false; |
685 | |
686 | // don't allow wide search when listing is disabled. |
327e2d96 |
687 | if ($expr=='*' && ! $this->listing) { |
688 | return array(); |
689 | } elseif ($expr=='*') { |
690 | // allow use of wildcard when listing is enabled. |
691 | $expression = '(cn=*)'; |
692 | } else { |
664fd7a0 |
693 | /* Convert search from user's charset to the one used in ldap and sanitize */ |
694 | $expr = $this->quotevalue($expr); |
17a62a1b |
695 | |
ddf4a1ba |
696 | /* If search expr contains %s or %1$s, replace them with escaped values, |
697 | * so that a wrong printf()-style string is not created by mistake. |
698 | * (Probably overkill but who knows...) */ |
699 | $expr = str_replace('%s', '\\25s', $expr); |
700 | $expr = str_replace('%1$s', '\\251$s', $expr); |
701 | |
702 | /* Substitute %s or %1$s in printf()-formatted search_expresison with |
703 | * the value that the user searches for. */ |
704 | $expression = sprintf($this->search_expression, $expr); |
3d1fa376 |
705 | |
327e2d96 |
706 | /* Undo sanitizing of * symbol */ |
707 | $expression = str_replace('\2a','*',$expression); |
ddf4a1ba |
708 | |
709 | /* Replace '**', '***' etc. with '*' in case it occurs in final |
710 | * search expression */ |
711 | while(strstr($expression, '**')) { |
712 | $expression = str_replace('**', '*', $expression); |
713 | } |
327e2d96 |
714 | } |
3d1fa376 |
715 | |
716 | /* Add search filtering */ |
717 | if ($this->filter!='') |
718 | $expression = '(&' . $this->filter . $expression . ')'; |
719 | |
720 | /* Use internal search function and return search results */ |
721 | return $this->ldap_search($expression); |
722 | } |
06b4facd |
723 | |
664fd7a0 |
724 | /** |
725 | * Lookup an alias |
726 | * @param string $alias alias |
727 | * @return array search results |
728 | * @since 1.5.2 |
729 | */ |
730 | function lookup($alias) { |
731 | /* Generate the dn and try to retrieve that single entry */ |
732 | $cn = $this->quotevalue($alias); |
733 | $dn = 'cn=' . $cn . ',' . $this->basedn; |
734 | |
735 | /* Do the search */ |
736 | $result = $this->ldap_search($dn, true); |
737 | if (!is_array($result) || count($result) < 1) |
738 | return array(); |
739 | |
740 | return $result[0]; |
741 | } |
06b4facd |
742 | |
981681d5 |
743 | /** |
744 | * List all entries present in LDAP server |
06b4facd |
745 | * |
3d1fa376 |
746 | * maxrows setting might limit list of returned entries. |
981681d5 |
747 | * Careful with this -- it could get quite large for big sites. |
748 | * @return array all entries in ldap server |
749 | */ |
750 | function list_addr() { |
3d1fa376 |
751 | if (! $this->listing) |
752 | return array(); |
753 | |
754 | /* set wide search expression */ |
755 | $expression = '(cn=*)'; |
756 | |
757 | /* add filtering */ |
758 | if ($this->filter!='') |
759 | $expression = '(&' . $this->filter . $expression .')'; |
760 | |
761 | /* use internal search function and return search results */ |
762 | return $this->ldap_search($expression); |
981681d5 |
763 | } |
664fd7a0 |
764 | |
765 | /** |
766 | * Add address |
767 | * @param array $userdata new data |
768 | * @return boolean |
769 | * @since 1.5.2 |
770 | */ |
771 | function add($userdata) { |
772 | if(!$this->writeable) { |
773 | return $this->set_error(_("Address book is read-only")); |
774 | } |
775 | |
776 | /* Convert search from user's charset to the one used in ldap and sanitize */ |
777 | $cn = $this->quotevalue($userdata['nickname']); |
778 | $dn = 'cn=' . $cn . ',' . trim($this->basedn); |
779 | |
780 | /* See if user exists already */ |
781 | $user = $this->ldap_search($dn, true); |
782 | if (!is_array($user)) { |
783 | return false; |
784 | } elseif (count($user) > 0) { |
785 | return $this->set_error(sprintf(_("User \"%s\" already exists"), $userdata['nickname'])); |
786 | } |
787 | |
788 | /* init variable */ |
789 | $data = array(); |
790 | |
791 | /* Prepare data */ |
792 | $data['cn'] = $cn; |
793 | $data['mail'] = $this->quotevalue($userdata['email']); |
794 | $data["objectclass"][0] = "top"; |
795 | $data["objectclass"][1] = "person"; |
796 | $data["objectclass"][2] = "organizationalPerson"; |
797 | $data["objectclass"][3] = "inetOrgPerson"; |
798 | /* sn is required in person object */ |
799 | if(!empty($userdata['lastname'])) { |
800 | $data['sn'] = $this->quotevalue($userdata['lastname']); |
801 | } else { |
802 | $data['sn'] = ' '; |
803 | } |
804 | /* optional fields */ |
805 | if(!empty($userdata['firstname'])) |
806 | $data['givenName'] = $this->quotevalue($userdata['firstname']); |
807 | if(!empty($userdata['label'])) { |
808 | $data['description'] = $this->quotevalue($userdata['label']); |
809 | } |
810 | return $this->ldap_add($dn, $data); |
811 | } |
812 | |
813 | /** |
814 | * Delete address |
815 | * @param array $aliases array of entries that have to be removed. |
816 | * @return boolean |
817 | * @since 1.5.2 |
818 | */ |
819 | function remove($aliases) { |
820 | if(!$this->writeable) { |
821 | return $this->set_error(_("Address book is read-only")); |
822 | } |
823 | |
824 | foreach ($aliases as $alias) { |
825 | /* Convert nickname from user's charset and derive cn/dn */ |
826 | $cn = $this->quotevalue($alias); |
827 | $dn = 'cn=' . $cn . ',' . $this->basedn; |
828 | |
829 | if (!$this->ldap_remove($dn)) |
830 | return false; |
831 | } |
832 | |
833 | return true; |
834 | } |
835 | |
836 | /** |
837 | * Modify address |
838 | * @param string $alias modified alias |
839 | * @param array $userdata new data |
840 | * @return boolean |
841 | * @since 1.5.2 |
842 | */ |
843 | function modify($alias, $userdata) { |
844 | if(!$this->writeable) { |
845 | return $this->set_error(_("Address book is read-only")); |
846 | } |
847 | |
848 | /* Convert search from user's charset to the one used in ldap and sanitize */ |
849 | $sourcecn = $this->quotevalue($alias); |
850 | $sourcedn = 'cn=' . $sourcecn . ',' . trim($this->basedn); |
851 | $targetcn = $this->quotevalue($userdata['nickname']); |
852 | $targetdn = 'cn=' . $targetcn . ',' . trim($this->basedn); |
853 | |
854 | /* Check that the dn to modify exists */ |
855 | $sourceuser = $this->lookup($alias); |
856 | if (!is_array($sourceuser) || count($sourceuser) < 1) |
857 | return false; |
858 | |
859 | /* Check if dn is going to change */ |
860 | if ($alias != $userdata['nickname']) { |
861 | |
862 | /* Check that the target dn doesn't exist */ |
863 | $targetuser = $this->lookup($userdata['nickname']); |
864 | if (is_array($targetuser) && count($targetuser) > 0) |
865 | return $this->set_error(sprintf(_("User \"%s\" already exists"), $userdata['nickname'])); |
866 | |
867 | /* Rename from the source dn to target dn */ |
868 | if (!$this->ldap_rename($sourcedn, 'cn=' . $targetcn, $this->basedn)) |
869 | return $this->set_error(sprintf(_("Unable to rename user \"%s\" to \"%s\""), $alias, $userdata['nickname'])); |
870 | } |
871 | |
872 | // initial vars |
873 | $data = array(); |
874 | $deleted_attribs = array(); |
875 | |
876 | /* Prepare data */ |
877 | $data['cn'] = $this->quotevalue($targetcn); |
878 | $data['mail'] = $this->quotevalue($userdata['email']); |
879 | $data["objectclass"][0] = "top"; |
880 | $data["objectclass"][1] = "person"; |
881 | $data["objectclass"][2] = "organizationalPerson"; |
882 | $data["objectclass"][3] = "inetOrgPerson"; |
883 | |
884 | if(!empty($userdata['firstname'])) { |
885 | $data['givenName'] = $this->quotevalue($userdata['firstname']); |
886 | } elseif (!empty($sourceuser['firstname'])) { |
887 | $deleted_attribs['givenName'] = $this->quotevalue($sourceuser['firstname']); |
888 | } |
889 | |
890 | if(!empty($userdata['lastname'])) { |
891 | $data['sn'] = $this->quotevalue($userdata['lastname']); |
892 | } else { |
893 | // sn is required attribute in LDAP person object. |
894 | // SquirrelMail requires givenName or Surname |
895 | $data['sn'] = ' '; |
896 | } |
897 | |
898 | if(!empty($userdata['label'])) { |
899 | $data['description'] = $this->quotevalue($userdata['label']); |
900 | } elseif (!empty($sourceuser['label'])) { |
901 | $deleted_attribs['description'] = $this->quotevalue($sourceuser['label']); |
902 | } |
903 | |
904 | return $this->ldap_modify($targetdn, $data, $deleted_attribs); |
905 | } |
06b4facd |
906 | } |