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 |
4030aca0 |
15 | * <david at hardeman.nu> (#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 | * |
22387c8d |
21 | * @copyright 1999-2017 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 | /** |
f774a1ed |
181 | * Constructor (PHP5 style, required in some future version of PHP) |
182 | * Connects to the database |
981681d5 |
183 | * @param array connection options |
184 | */ |
f774a1ed |
185 | function __construct($param) { |
06b4facd |
186 | if(!function_exists('ldap_connect')) { |
ae4d36f7 |
187 | $this->set_error(_("PHP install does not have LDAP support.")); |
06b4facd |
188 | return; |
189 | } |
190 | if(is_array($param)) { |
191 | $this->server = $param['host']; |
664fd7a0 |
192 | // remove whitespace from basedn |
193 | $this->basedn = preg_replace('/,\s*/',',',trim($param['base'])); |
ae4d36f7 |
194 | |
195 | if(!empty($param['port'])) |
06b4facd |
196 | $this->port = $param['port']; |
ae4d36f7 |
197 | |
198 | if(!empty($param['charset'])) |
06b4facd |
199 | $this->charset = strtolower($param['charset']); |
ae4d36f7 |
200 | |
201 | if(isset($param['maxrows'])) |
06b4facd |
202 | $this->maxrows = $param['maxrows']; |
ae4d36f7 |
203 | |
204 | if(isset($param['timeout'])) |
06b4facd |
205 | $this->timeout = $param['timeout']; |
ae4d36f7 |
206 | |
207 | if(isset($param['binddn'])) |
30e9932c |
208 | $this->binddn = $param['binddn']; |
ae4d36f7 |
209 | |
210 | if(isset($param['bindpw'])) |
30e9932c |
211 | $this->bindpw = $param['bindpw']; |
ae4d36f7 |
212 | |
213 | if(isset($param['protocol'])) |
593370a4 |
214 | $this->protocol = (int) $param['protocol']; |
ae4d36f7 |
215 | |
216 | if(isset($param['filter'])) |
217 | $this->filter = trim($param['filter']); |
ddf4a1ba |
218 | |
219 | if(isset($param['search_expression']) && |
220 | (strstr($param['search_expression'], '%s') || strstr($param['search_expression'], '%1$s'))) { |
221 | $this->search_expression = trim($param['search_expression']); |
222 | } |
ae4d36f7 |
223 | |
224 | if(isset($param['limit_scope'])) |
593370a4 |
225 | $this->limit_scope = (bool) $param['limit_scope']; |
ae4d36f7 |
226 | |
3d1fa376 |
227 | if(isset($param['listing'])) |
593370a4 |
228 | $this->listing = (bool) $param['listing']; |
229 | |
664fd7a0 |
230 | if(isset($param['writeable'])) { |
231 | $this->writeable = (bool) $param['writeable']; |
232 | // switch backend type to local, if it is writable |
233 | if($this->writeable) $this->btype = 'local'; |
234 | } |
235 | |
593370a4 |
236 | if(isset($param['search_tree'])) |
237 | $this->search_tree = (bool) $param['search_tree']; |
238 | |
239 | if(isset($param['starttls'])) |
240 | $this->starttls = (bool) $param['starttls']; |
3d1fa376 |
241 | |
06b4facd |
242 | if(empty($param['name'])) { |
243 | $this->sname = 'LDAP: ' . $param['host']; |
ae4d36f7 |
244 | } else { |
06b4facd |
245 | $this->sname = $param['name']; |
246 | } |
62f7daa5 |
247 | |
ae4d36f7 |
248 | /* |
249 | * don't open LDAP server on addressbook_init(), |
250 | * open ldap connection only on search. Speeds up |
251 | * addressbook_init() call. |
252 | */ |
253 | // $this->open(true); |
06b4facd |
254 | } else { |
255 | $this->set_error('Invalid argument to constructor'); |
256 | } |
257 | } |
258 | |
f774a1ed |
259 | /** |
260 | * Constructor (PHP4 style, kept for compatibility reasons) |
261 | * Connects to the database |
262 | * @param array connection options |
263 | */ |
264 | function abook_ldap_server($param) { |
265 | return self::__construct($param); |
266 | } |
06b4facd |
267 | |
981681d5 |
268 | /** |
269 | * Open the LDAP server. |
270 | * @param bool $new is it a new connection |
271 | * @return bool |
272 | */ |
06b4facd |
273 | function open($new = false) { |
274 | $this->error = ''; |
62f7daa5 |
275 | |
06b4facd |
276 | /* Connection is already open */ |
277 | if($this->linkid != false && !$new) { |
278 | return true; |
279 | } |
62f7daa5 |
280 | |
06b4facd |
281 | $this->linkid = @ldap_connect($this->server, $this->port); |
593370a4 |
282 | /** |
283 | * check if connection was successful |
284 | * It does not work with OpenLDAP 2.x libraries. Connect error will be |
285 | * displayed only on ldap command that tries to make connection |
286 | * (ldap_start_tls or ldap_bind). |
287 | */ |
06b4facd |
288 | if(!$this->linkid) { |
593370a4 |
289 | return $this->set_error($this->ldap_error('ldap_connect failed')); |
06b4facd |
290 | } |
62f7daa5 |
291 | |
981681d5 |
292 | if(!empty($this->protocol)) { |
593370a4 |
293 | // make sure that ldap_set_option() is available before using it |
294 | if(! function_exists('ldap_set_option') || |
295 | !@ldap_set_option($this->linkid, LDAP_OPT_PROTOCOL_VERSION, $this->protocol)) { |
296 | return $this->set_error('unable to set ldap protocol number'); |
297 | } |
298 | } |
299 | |
300 | /** |
301 | * http://www.php.net/ldap-start-tls |
302 | * Check if v3 or newer protocol is used, |
303 | * check if ldap_start_tls function is available. |
8f227330 |
304 | * Silently ignore setting, if these requirements are not satisfied. |
305 | * Break with error message if somebody tries to start TLS on |
306 | * ldaps or socket connection. |
593370a4 |
307 | */ |
308 | if($this->starttls && |
309 | !empty($this->protocol) && $this->protocol >= 3 && |
310 | function_exists('ldap_start_tls') ) { |
8f227330 |
311 | // make sure that $this->server is not ldaps:// or ldapi:// URL. |
312 | if (preg_match("/^ldap[si]:\/\/.+/i",$this->server)) { |
313 | return $this->set_error("you can't enable starttls on ldaps and ldapi connections."); |
593370a4 |
314 | } |
593370a4 |
315 | |
316 | // try starting tls |
317 | if (! @ldap_start_tls($this->linkid)) { |
318 | // set error if call fails |
319 | return $this->set_error($this->ldap_error('ldap_start_tls failed')); |
981681d5 |
320 | } |
321 | } |
30e9932c |
322 | |
ae4d36f7 |
323 | if(!empty($this->limit_scope) && $this->limit_scope) { |
324 | if(empty($this->protocol) || intval($this->protocol) < 3) { |
325 | return $this->set_error('limit_scope requires protocol >= 3'); |
326 | } |
327 | // See http://msdn.microsoft.com/library/en-us/ldap/ldap/ldap_server_domain_scope_oid.asp |
328 | $ctrl = array ( "oid" => "1.2.840.113556.1.4.1339", "iscritical" => TRUE ); |
593370a4 |
329 | /* |
330 | * Option is set only during connection. |
331 | * It does not cause immediate errors with OpenLDAP 2.x libraries. |
332 | */ |
333 | if(! function_exists('ldap_set_option') || |
334 | !@ldap_set_option($this->linkid, LDAP_OPT_SERVER_CONTROLS, array($ctrl))) { |
335 | return $this->set_error($this->ldap_error('limit domain scope failed')); |
ae4d36f7 |
336 | } |
337 | } |
338 | |
593370a4 |
339 | // authenticated bind |
30e9932c |
340 | if(!empty($this->binddn)) { |
341 | if(!@ldap_bind($this->linkid, $this->binddn, $this->bindpw)) { |
593370a4 |
342 | return $this->set_error($this->ldap_error('authenticated ldap_bind failed')); |
343 | } |
30e9932c |
344 | } else { |
593370a4 |
345 | // anonymous bind |
981681d5 |
346 | if(!@ldap_bind($this->linkid)) { |
593370a4 |
347 | return $this->set_error($this->ldap_error('anonymous ldap_bind failed')); |
981681d5 |
348 | } |
06b4facd |
349 | } |
30e9932c |
350 | |
06b4facd |
351 | $this->bound = true; |
62f7daa5 |
352 | |
06b4facd |
353 | return true; |
354 | } |
355 | |
981681d5 |
356 | /** |
357 | * Encode string to the charset used by this LDAP server |
358 | * @param string string that has to be encoded |
359 | * @return string encoded string |
360 | */ |
06b4facd |
361 | function charset_encode($str) { |
b64dd897 |
362 | global $default_charset; |
363 | if($this->charset != $default_charset) { |
364 | return charset_convert($default_charset,$str,$this->charset,false); |
06b4facd |
365 | } else { |
366 | return $str; |
367 | } |
368 | } |
369 | |
981681d5 |
370 | /** |
b64dd897 |
371 | * Decode from charset used by this LDAP server to charset used by translation |
981681d5 |
372 | * |
598294a7 |
373 | * Uses SquirrelMail charset_decode functions |
981681d5 |
374 | * @param string string that has to be decoded |
375 | * @return string decoded string |
376 | */ |
06b4facd |
377 | function charset_decode($str) { |
981681d5 |
378 | global $default_charset; |
379 | if ($this->charset != $default_charset) { |
b64dd897 |
380 | return charset_convert($this->charset,$str,$default_charset,false); |
06b4facd |
381 | } else { |
382 | return $str; |
383 | } |
384 | } |
385 | |
d58ed98f |
386 | /** |
387 | * Sanitizes ldap search strings. |
388 | * See rfc2254 |
389 | * @link http://www.faqs.org/rfcs/rfc2254.html |
ae4d36f7 |
390 | * @since 1.5.1 and 1.4.5 |
d58ed98f |
391 | * @param string $string |
392 | * @return string sanitized string |
393 | */ |
394 | function ldapspecialchars($string) { |
395 | $sanitized=array('\\' => '\5c', |
396 | '*' => '\2a', |
397 | '(' => '\28', |
398 | ')' => '\29', |
399 | "\x00" => '\00'); |
400 | |
401 | return str_replace(array_keys($sanitized),array_values($sanitized),$string); |
402 | } |
62f7daa5 |
403 | |
664fd7a0 |
404 | /** |
405 | * Prepares user input for use in a ldap query. |
406 | * |
407 | * Function converts input string to character set used in LDAP server |
408 | * (charset_encode() method) and sanitizes it (ldapspecialchars()). |
409 | * |
410 | * @param string $string string to encode |
411 | * @return string ldap encoded string |
412 | * @since 1.5.2 |
413 | */ |
414 | function quotevalue($string) { |
415 | $sanitized = $this->charset_encode($string); |
416 | return $this->ldapspecialchars($sanitized); |
417 | } |
418 | |
981681d5 |
419 | /** |
3d1fa376 |
420 | * Search LDAP server. |
421 | * |
422 | * Warning: You must make sure that ldap query is correctly formated and |
423 | * sanitize use of special ldap keywords. |
424 | * @param string $expression ldap query |
664fd7a0 |
425 | * @param boolean $singleentry (since 1.5.2) whether we are looking for a |
426 | * single entry. Boolean true forces LDAP_SCOPE_BASE search. |
3d1fa376 |
427 | * @return array search results (false on error) |
428 | * @since 1.5.1 |
981681d5 |
429 | */ |
664fd7a0 |
430 | function ldap_search($expression, $singleentry = false) { |
06b4facd |
431 | /* Make sure connection is there */ |
432 | if(!$this->open()) { |
433 | return false; |
434 | } |
62f7daa5 |
435 | |
4030aca0 |
436 | $attributes = array('dn', 'description', 'sn', 'givenName', 'cn', 'mail'); |
664fd7a0 |
437 | |
438 | if ($singleentry) { |
439 | // ldap_read - search for one single entry |
440 | $sret = @ldap_read($this->linkid, $expression, "objectClass=*", |
441 | $attributes, 0, $this->maxrows, $this->timeout); |
442 | } elseif ($this->search_tree) { |
593370a4 |
443 | // ldap_search - search subtree |
444 | $sret = @ldap_search($this->linkid, $this->basedn, $expression, |
664fd7a0 |
445 | $attributes, 0, $this->maxrows, $this->timeout); |
593370a4 |
446 | } else { |
447 | // ldap_list - search one level |
448 | $sret = @ldap_list($this->linkid, $this->basedn, $expression, |
664fd7a0 |
449 | $attributes, 0, $this->maxrows, $this->timeout); |
593370a4 |
450 | } |
62f7daa5 |
451 | |
593370a4 |
452 | /* Return error if search failed */ |
06b4facd |
453 | if(!$sret) { |
664fd7a0 |
454 | // Check for LDAP_NO_SUCH_OBJECT (0x20 or 32) error |
455 | if (ldap_errno($this->linkid)==32) { |
456 | return array(); |
457 | } else { |
458 | return $this->set_error($this->ldap_error('ldap_search failed')); |
459 | } |
06b4facd |
460 | } |
62f7daa5 |
461 | |
06b4facd |
462 | if(@ldap_count_entries($this->linkid, $sret) <= 0) { |
463 | return array(); |
464 | } |
62f7daa5 |
465 | |
06b4facd |
466 | /* Get results */ |
467 | $ret = array(); |
468 | $returned_rows = 0; |
469 | $res = @ldap_get_entries($this->linkid, $sret); |
470 | for($i = 0 ; $i < $res['count'] ; $i++) { |
471 | $row = $res[$i]; |
62f7daa5 |
472 | |
06b4facd |
473 | /* Extract data common for all e-mail addresses |
b682f335 |
474 | * of an object. Use only the first name */ |
06b4facd |
475 | $nickname = $this->charset_decode($row['dn']); |
b682f335 |
476 | |
477 | /** |
664fd7a0 |
478 | * remove trailing basedn |
479 | * remove whitespaces between RDNs |
480 | * remove leading "cn=" |
481 | * which gives nicknames which are shorter while still unique |
482 | */ |
483 | $nickname = preg_replace('/,\s*/',',', trim($nickname)); |
484 | $offset = strlen($nickname) - strlen($this->basedn); |
485 | |
486 | if($offset > 0 && substr($nickname, $offset) == $this->basedn) { |
487 | $nickname = substr($nickname, 0, $offset); |
488 | if(substr($nickname, -1) == ",") |
489 | $nickname = substr($nickname, 0, -1); |
06b4facd |
490 | } |
664fd7a0 |
491 | if(strncasecmp($nickname, "cn=", 3) == 0) |
492 | $nickname=substr($nickname, 3); |
493 | |
494 | if(empty($row['description'][0])) { |
06b4facd |
495 | $label = ''; |
664fd7a0 |
496 | } else { |
497 | $label = $this->charset_decode($row['description'][0]); |
06b4facd |
498 | } |
62f7daa5 |
499 | |
06b4facd |
500 | if(empty($row['givenname'][0])) { |
501 | $firstname = ''; |
502 | } else { |
503 | $firstname = $this->charset_decode($row['givenname'][0]); |
504 | } |
62f7daa5 |
505 | |
06b4facd |
506 | if(empty($row['sn'][0])) { |
507 | $surname = ''; |
508 | } else { |
664fd7a0 |
509 | // remove whitespace in order to handle sn set to empty string |
510 | $surname = trim($this->charset_decode($row['sn'][0])); |
06b4facd |
511 | } |
62f7daa5 |
512 | |
35235328 |
513 | $fullname = $this->fullname($firstname,$surname); |
664fd7a0 |
514 | |
06b4facd |
515 | /* Add one row to result for each e-mail address */ |
516 | if(isset($row['mail']['count'])) { |
517 | for($j = 0 ; $j < $row['mail']['count'] ; $j++) { |
518 | array_push($ret, array('nickname' => $nickname, |
519 | 'name' => $fullname, |
520 | 'firstname' => $firstname, |
521 | 'lastname' => $surname, |
522 | 'email' => $row['mail'][$j], |
523 | 'label' => $label, |
06b4facd |
524 | 'backend' => $this->bnum, |
525 | 'source' => &$this->sname)); |
62f7daa5 |
526 | |
06b4facd |
527 | // Limit number of hits |
528 | $returned_rows++; |
62f7daa5 |
529 | if(($returned_rows >= $this->maxrows) && |
06b4facd |
530 | ($this->maxrows > 0) ) { |
531 | ldap_free_result($sret); |
532 | return $ret; |
533 | } |
534 | |
535 | } // for($j ...) |
536 | |
537 | } // isset($row['mail']['count']) |
62f7daa5 |
538 | |
06b4facd |
539 | } |
62f7daa5 |
540 | |
06b4facd |
541 | ldap_free_result($sret); |
542 | return $ret; |
3d1fa376 |
543 | } |
544 | |
664fd7a0 |
545 | /** |
546 | * Add an entry to LDAP server. |
547 | * |
548 | * Warning: You must make sure that the arguments are correctly formated and |
549 | * sanitize use of special ldap keywords. |
550 | * @param string $dn the dn of the entry to be added |
551 | * @param array $data the values of the entry to be added |
552 | * @return boolean result (false on error) |
553 | * @since 1.5.2 |
554 | */ |
555 | function ldap_add($dn, $data) { |
556 | /* Make sure connection is there */ |
557 | if(!$this->open()) { |
558 | return false; |
559 | } |
560 | |
561 | if(!@ldap_add($this->linkid, $dn, $data)) { |
562 | $this->set_error(_("Write to address book failed")); |
563 | return false; |
564 | } |
565 | |
566 | return true; |
567 | } |
568 | |
569 | /** |
570 | * Remove an entry from LDAP server. |
571 | * |
572 | * Warning: You must make sure that the argument is correctly formated and |
573 | * sanitize use of special ldap keywords. |
574 | * @param string $dn the dn of the entry to remove |
575 | * @return boolean result (false on error) |
576 | * @since 1.5.2 |
577 | */ |
578 | function ldap_remove($dn) { |
579 | /* Make sure connection is there */ |
580 | if(!$this->open()) { |
581 | return false; |
582 | } |
583 | |
584 | if(!@ldap_delete($this->linkid, $dn)) { |
585 | $this->set_error(_("Removing entry from address book failed")); |
586 | return false; |
587 | } |
588 | |
589 | return true; |
590 | } |
591 | |
592 | /** |
593 | * Rename an entry on LDAP server. |
594 | * |
595 | * Warning: You must make sure that the arguments are correctly formated and |
596 | * sanitize use of special ldap keywords. |
597 | * @param string $sourcedn the dn of the entry to be renamed |
598 | * @param string $targetdn the dn which $sourcedn should be renamed to |
599 | * @param string $parent the dn of the parent entry |
600 | * @return boolean result (false on error) |
601 | * @since 1.5.2 |
602 | */ |
603 | function ldap_rename($sourcedn, $targetdn, $parent) { |
604 | /* Make sure connection is there */ |
605 | if(!$this->open()) { |
606 | return false; |
607 | } |
608 | |
609 | /* Make sure that the protocol version supports rename */ |
610 | if($this->protocol < 3) { |
611 | $this->set_error(_("LDAP rename is not supported by used protocol version")); |
612 | return false; |
613 | } |
614 | /** |
615 | * Function is available only in OpenLDAP 2.x.x or Netscape Directory |
616 | * SDK x.x, and was added in PHP 4.0.5 |
617 | * @todo maybe we can use copy + delete instead of ldap_rename() |
618 | */ |
619 | if(!function_exists('ldap_rename')) { |
620 | $this->set_error(_("LDAP rename is not supported by used LDAP library. You can't change nickname")); |
621 | return false; |
622 | } |
623 | |
624 | /* OK, go for it */ |
625 | if(!@ldap_rename($this->linkid, $sourcedn, $targetdn, $parent, true)) { |
626 | $this->set_error(_("LDAP rename failed")); |
627 | return false; |
628 | } |
629 | |
630 | return true; |
631 | } |
632 | |
633 | /** |
634 | * Modify the values of an entry on LDAP server. |
635 | * |
636 | * Warning: You must make sure that the arguments are correctly formated and |
637 | * sanitize use of special ldap keywords. |
638 | * @param string $dn the dn of the entry to be modified |
639 | * @param array $data the new values of the entry |
640 | * @param array $deleted_attribs attributes that should be deleted. |
641 | * @return bool result (false on error) |
642 | * @since 1.5.2 |
643 | */ |
644 | function ldap_modify($dn, $data, $deleted_attribs) { |
645 | /* Make sure connection is there */ |
646 | if(!$this->open()) { |
647 | return false; |
648 | } |
649 | |
650 | if(!@ldap_modify($this->linkid, $dn, $data)) { |
651 | $this->set_error(_("Write to address book failed")); |
652 | return false; |
653 | } |
654 | |
655 | if (!@ldap_mod_del($this->linkid, $dn, $deleted_attribs)) { |
656 | $this->set_error(_("Unable to remove some field values")); |
657 | return false; |
658 | } |
659 | |
660 | return true; |
661 | } |
662 | |
593370a4 |
663 | /** |
664 | * Get error from LDAP resource if possible |
665 | * |
666 | * Should get error from server using the ldap_errno() and ldap_err2str() functions |
667 | * @param string $sError error message used when ldap error functions |
668 | * and connection resource are unavailable |
669 | * @return string error message |
670 | * @since 1.5.1 |
671 | */ |
672 | function ldap_error($sError) { |
673 | // it is possible that function_exists() tests are not needed |
674 | if(function_exists('ldap_err2str') && |
675 | function_exists('ldap_errno') && |
676 | is_resource($this->linkid)) { |
677 | return ldap_err2str(ldap_errno($this->linkid)); |
678 | // return ldap_error($this->linkid); |
679 | } else { |
680 | return $sError; |
681 | } |
682 | } |
683 | |
4030aca0 |
684 | /** |
685 | * Determine internal attribute name given one of |
686 | * the SquirrelMail SM_ABOOK_FIELD_* constants |
687 | * |
688 | * @param integer $attr The SM_ABOOK_FIELD_* contant to look up |
689 | * |
690 | * @return string The desired attribute name, or the string "ERROR" |
691 | * if the $field is not understood (the caller |
692 | * is responsible for handing errors) |
693 | * |
694 | */ |
695 | function get_attr_name($attr) { |
696 | switch ($attr) { |
697 | case SM_ABOOK_FIELD_NICKNAME: |
698 | return 'cn'; |
699 | case SM_ABOOK_FIELD_FIRSTNAME: |
700 | return 'givenName'; |
701 | case SM_ABOOK_FIELD_LASTNAME: |
702 | return 'sn'; |
703 | case SM_ABOOK_FIELD_EMAIL: |
704 | return 'mail'; |
705 | case SM_ABOOK_FIELD_LABEL: |
706 | return 'description'; |
707 | default: |
708 | return 'ERROR'; |
709 | } |
710 | } |
711 | |
3d1fa376 |
712 | /* ========================== Public ======================== */ |
713 | |
714 | /** |
715 | * Search the LDAP server |
716 | * @param string $expr search expression |
717 | * @return array search results |
718 | */ |
719 | function search($expr) { |
720 | /* To be replaced by advanded search expression parsing */ |
721 | if(is_array($expr)) return false; |
722 | |
723 | // don't allow wide search when listing is disabled. |
327e2d96 |
724 | if ($expr=='*' && ! $this->listing) { |
725 | return array(); |
726 | } elseif ($expr=='*') { |
727 | // allow use of wildcard when listing is enabled. |
728 | $expression = '(cn=*)'; |
729 | } else { |
664fd7a0 |
730 | /* Convert search from user's charset to the one used in ldap and sanitize */ |
731 | $expr = $this->quotevalue($expr); |
17a62a1b |
732 | |
ddf4a1ba |
733 | /* If search expr contains %s or %1$s, replace them with escaped values, |
734 | * so that a wrong printf()-style string is not created by mistake. |
735 | * (Probably overkill but who knows...) */ |
736 | $expr = str_replace('%s', '\\25s', $expr); |
737 | $expr = str_replace('%1$s', '\\251$s', $expr); |
738 | |
739 | /* Substitute %s or %1$s in printf()-formatted search_expresison with |
740 | * the value that the user searches for. */ |
741 | $expression = sprintf($this->search_expression, $expr); |
3d1fa376 |
742 | |
327e2d96 |
743 | /* Undo sanitizing of * symbol */ |
744 | $expression = str_replace('\2a','*',$expression); |
ddf4a1ba |
745 | |
746 | /* Replace '**', '***' etc. with '*' in case it occurs in final |
747 | * search expression */ |
748 | while(strstr($expression, '**')) { |
749 | $expression = str_replace('**', '*', $expression); |
750 | } |
327e2d96 |
751 | } |
3d1fa376 |
752 | |
753 | /* Add search filtering */ |
754 | if ($this->filter!='') |
755 | $expression = '(&' . $this->filter . $expression . ')'; |
756 | |
757 | /* Use internal search function and return search results */ |
758 | return $this->ldap_search($expression); |
759 | } |
06b4facd |
760 | |
664fd7a0 |
761 | /** |
503c7650 |
762 | * Lookup an address by the indicated field. |
763 | * |
764 | * @param string $value The value to look up |
765 | * @param integer $field The field to look in, should be one |
766 | * of the SM_ABOOK_FIELD_* constants |
767 | * defined in include/constants.php |
768 | * (OPTIONAL; defaults to nickname field) |
bf55ebab |
769 | * NOTE: uniqueness is only guaranteed |
770 | * when the nickname field is used here; |
771 | * otherwise, the first matching address |
772 | * is returned. |
503c7650 |
773 | * |
774 | * @return array Array with lookup results when the value |
775 | * was found, an empty array if the value was |
776 | * not found. |
777 | * |
664fd7a0 |
778 | * @since 1.5.2 |
503c7650 |
779 | * |
664fd7a0 |
780 | */ |
503c7650 |
781 | function lookup($value, $field=SM_ABOOK_FIELD_NICKNAME) { |
782 | |
503c7650 |
783 | |
b1fbb25f |
784 | $attr = get_attr_name($field); |
4030aca0 |
785 | if ($attr == 'ERROR') { |
786 | return $this->set_error(sprintf(_("Unknown field name: %s"), $field)); |
787 | } |
788 | |
789 | // Generate the dn |
790 | $dn = $attr . '=' . $this->quotevalue($value) . ',' . $this->basedn; |
664fd7a0 |
791 | |
4030aca0 |
792 | // Do the search |
664fd7a0 |
793 | $result = $this->ldap_search($dn, true); |
794 | if (!is_array($result) || count($result) < 1) |
795 | return array(); |
796 | |
797 | return $result[0]; |
798 | } |
06b4facd |
799 | |
981681d5 |
800 | /** |
801 | * List all entries present in LDAP server |
06b4facd |
802 | * |
3d1fa376 |
803 | * maxrows setting might limit list of returned entries. |
981681d5 |
804 | * Careful with this -- it could get quite large for big sites. |
805 | * @return array all entries in ldap server |
806 | */ |
807 | function list_addr() { |
3d1fa376 |
808 | if (! $this->listing) |
809 | return array(); |
810 | |
811 | /* set wide search expression */ |
812 | $expression = '(cn=*)'; |
813 | |
814 | /* add filtering */ |
815 | if ($this->filter!='') |
816 | $expression = '(&' . $this->filter . $expression .')'; |
817 | |
818 | /* use internal search function and return search results */ |
819 | return $this->ldap_search($expression); |
981681d5 |
820 | } |
664fd7a0 |
821 | |
822 | /** |
823 | * Add address |
824 | * @param array $userdata new data |
825 | * @return boolean |
826 | * @since 1.5.2 |
827 | */ |
828 | function add($userdata) { |
829 | if(!$this->writeable) { |
830 | return $this->set_error(_("Address book is read-only")); |
831 | } |
832 | |
833 | /* Convert search from user's charset to the one used in ldap and sanitize */ |
834 | $cn = $this->quotevalue($userdata['nickname']); |
835 | $dn = 'cn=' . $cn . ',' . trim($this->basedn); |
836 | |
837 | /* See if user exists already */ |
838 | $user = $this->ldap_search($dn, true); |
839 | if (!is_array($user)) { |
840 | return false; |
841 | } elseif (count($user) > 0) { |
842 | return $this->set_error(sprintf(_("User \"%s\" already exists"), $userdata['nickname'])); |
843 | } |
844 | |
845 | /* init variable */ |
846 | $data = array(); |
847 | |
848 | /* Prepare data */ |
849 | $data['cn'] = $cn; |
850 | $data['mail'] = $this->quotevalue($userdata['email']); |
851 | $data["objectclass"][0] = "top"; |
852 | $data["objectclass"][1] = "person"; |
853 | $data["objectclass"][2] = "organizationalPerson"; |
854 | $data["objectclass"][3] = "inetOrgPerson"; |
855 | /* sn is required in person object */ |
856 | if(!empty($userdata['lastname'])) { |
857 | $data['sn'] = $this->quotevalue($userdata['lastname']); |
858 | } else { |
859 | $data['sn'] = ' '; |
860 | } |
861 | /* optional fields */ |
862 | if(!empty($userdata['firstname'])) |
863 | $data['givenName'] = $this->quotevalue($userdata['firstname']); |
864 | if(!empty($userdata['label'])) { |
865 | $data['description'] = $this->quotevalue($userdata['label']); |
866 | } |
867 | return $this->ldap_add($dn, $data); |
868 | } |
869 | |
870 | /** |
871 | * Delete address |
872 | * @param array $aliases array of entries that have to be removed. |
873 | * @return boolean |
874 | * @since 1.5.2 |
875 | */ |
876 | function remove($aliases) { |
877 | if(!$this->writeable) { |
878 | return $this->set_error(_("Address book is read-only")); |
879 | } |
880 | |
881 | foreach ($aliases as $alias) { |
882 | /* Convert nickname from user's charset and derive cn/dn */ |
883 | $cn = $this->quotevalue($alias); |
884 | $dn = 'cn=' . $cn . ',' . $this->basedn; |
885 | |
886 | if (!$this->ldap_remove($dn)) |
887 | return false; |
888 | } |
889 | |
890 | return true; |
891 | } |
892 | |
893 | /** |
894 | * Modify address |
895 | * @param string $alias modified alias |
896 | * @param array $userdata new data |
897 | * @return boolean |
898 | * @since 1.5.2 |
899 | */ |
900 | function modify($alias, $userdata) { |
901 | if(!$this->writeable) { |
902 | return $this->set_error(_("Address book is read-only")); |
903 | } |
904 | |
905 | /* Convert search from user's charset to the one used in ldap and sanitize */ |
906 | $sourcecn = $this->quotevalue($alias); |
907 | $sourcedn = 'cn=' . $sourcecn . ',' . trim($this->basedn); |
908 | $targetcn = $this->quotevalue($userdata['nickname']); |
909 | $targetdn = 'cn=' . $targetcn . ',' . trim($this->basedn); |
910 | |
911 | /* Check that the dn to modify exists */ |
912 | $sourceuser = $this->lookup($alias); |
913 | if (!is_array($sourceuser) || count($sourceuser) < 1) |
914 | return false; |
915 | |
916 | /* Check if dn is going to change */ |
917 | if ($alias != $userdata['nickname']) { |
918 | |
919 | /* Check that the target dn doesn't exist */ |
920 | $targetuser = $this->lookup($userdata['nickname']); |
921 | if (is_array($targetuser) && count($targetuser) > 0) |
922 | return $this->set_error(sprintf(_("User \"%s\" already exists"), $userdata['nickname'])); |
923 | |
924 | /* Rename from the source dn to target dn */ |
925 | if (!$this->ldap_rename($sourcedn, 'cn=' . $targetcn, $this->basedn)) |
926 | return $this->set_error(sprintf(_("Unable to rename user \"%s\" to \"%s\""), $alias, $userdata['nickname'])); |
927 | } |
928 | |
929 | // initial vars |
930 | $data = array(); |
931 | $deleted_attribs = array(); |
932 | |
933 | /* Prepare data */ |
934 | $data['cn'] = $this->quotevalue($targetcn); |
935 | $data['mail'] = $this->quotevalue($userdata['email']); |
936 | $data["objectclass"][0] = "top"; |
937 | $data["objectclass"][1] = "person"; |
938 | $data["objectclass"][2] = "organizationalPerson"; |
939 | $data["objectclass"][3] = "inetOrgPerson"; |
940 | |
941 | if(!empty($userdata['firstname'])) { |
942 | $data['givenName'] = $this->quotevalue($userdata['firstname']); |
943 | } elseif (!empty($sourceuser['firstname'])) { |
944 | $deleted_attribs['givenName'] = $this->quotevalue($sourceuser['firstname']); |
945 | } |
946 | |
947 | if(!empty($userdata['lastname'])) { |
948 | $data['sn'] = $this->quotevalue($userdata['lastname']); |
949 | } else { |
950 | // sn is required attribute in LDAP person object. |
951 | // SquirrelMail requires givenName or Surname |
952 | $data['sn'] = ' '; |
953 | } |
954 | |
955 | if(!empty($userdata['label'])) { |
956 | $data['description'] = $this->quotevalue($userdata['label']); |
957 | } elseif (!empty($sourceuser['label'])) { |
958 | $deleted_attribs['description'] = $this->quotevalue($sourceuser['label']); |
959 | } |
960 | |
961 | return $this->ldap_modify($targetdn, $data, $deleted_attribs); |
962 | } |
06b4facd |
963 | } |