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