1: <?php
2: /**
3: * The main Horde_Ldap class.
4: *
5: * Copyright 2003-2007 Tarjej Huse, Jan Wagner, Del Elson, Benedikt Hallinger
6: * Copyright 2009-2012 Horde LLC (http://www.horde.org/)
7: *
8: * @package Ldap
9: * @author Tarjej Huse <tarjei@bergfald.no>
10: * @author Jan Wagner <wagner@netsols.de>
11: * @author Del <del@babel.com.au>
12: * @author Benedikt Hallinger <beni@php.net>
13: * @author Ben Klang <ben@alkaloid.net>
14: * @author Chuck Hagenbuch <chuck@horde.org>
15: * @author Jan Schneider <jan@horde.org>
16: * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3
17: */
18: class Horde_Ldap
19: {
20: /**
21: * Class configuration array
22: *
23: * - hostspec: the LDAP host to connect to (may be an array of
24: * several hosts to try).
25: * - port: the server port.
26: * - version: LDAP version (defaults to 3).
27: * - tls: when set, ldap_start_tls() is run after connecting.
28: * - binddn: the DN to bind as when searching.
29: * - bindpw: password to use when searching LDAP.
30: * - basedn: LDAP base.
31: * - options: hash of LDAP options to set.
32: * - filter: default search filter.
33: * - scope: default search scope.
34: * - user: configuration parameters for {@link findUserDN()},
35: * must contain 'uid', and 'filter' or 'objectclass'
36: * entries.
37: * - auto_reconnect: if true, the class will automatically
38: * attempt to reconnect to the LDAP server in certain
39: * failure conditions when attempting a search, or other
40: * LDAP operations. Defaults to false. Note that if you
41: * set this to true, calls to search() may block
42: * indefinitely if there is a catastrophic server failure.
43: * - min_backoff: minimum reconnection delay period (in seconds).
44: * - current_backof: initial reconnection delay period (in seconds).
45: * - max_backoff: maximum reconnection delay period (in seconds).
46: * - cache a Horde_Cache instance for caching schema requests.
47: *
48: * @var array
49: */
50: protected $_config = array(
51: 'hostspec' => 'localhost',
52: 'port' => 389,
53: 'version' => 3,
54: 'tls' => false,
55: 'binddn' => '',
56: 'bindpw' => '',
57: 'basedn' => '',
58: 'options' => array(),
59: 'filter' => '(objectClass=*)',
60: 'scope' => 'sub',
61: 'user' => array(),
62: 'auto_reconnect' => false,
63: 'min_backoff' => 1,
64: 'current_backoff' => 1,
65: 'max_backoff' => 32,
66: 'cache' => false,
67: 'cachettl' => 3600);
68:
69: /**
70: * List of hosts we try to establish a connection to.
71: *
72: * @var array
73: */
74: protected $_hostList = array();
75:
76: /**
77: * List of hosts that are known to be down.
78: *
79: * @var array
80: */
81: protected $_downHostList = array();
82:
83: /**
84: * LDAP resource link.
85: *
86: * @var resource
87: */
88: protected $_link;
89:
90: /**
91: * Schema object.
92: *
93: * @see schema()
94: * @var Horde_Ldap_Schema
95: */
96: protected $_schema;
97:
98: /**
99: * Schema cache function callback.
100: *
101: * @see registerSchemaCache()
102: * @var string
103: */
104: protected $_schemaCache;
105:
106: /**
107: * Cache for attribute encoding checks.
108: *
109: * @var array Hash with attribute names as key and boolean value
110: * to determine whether they should be utf8 encoded or not.
111: */
112: protected $_schemaAttrs = array();
113:
114: /**
115: * Cache for rootDSE objects
116: *
117: * Hash with requested rootDSE attr names as key and rootDSE
118: * object as value.
119: *
120: * Since the RootDSE object itself may request a rootDSE object,
121: * {@link rootDSE()} caches successful requests.
122: * Internally, Horde_Ldap needs several lookups to this object, so
123: * caching increases performance significally.
124: *
125: * @var array
126: */
127: protected $_rootDSECache = array();
128:
129: /**
130: * Constructor.
131: *
132: * @see $_config
133: *
134: * @param array $config Configuration array.
135: */
136: public function __construct($config = array())
137: {
138: if (!Horde_Util::loadExtension('ldap')) {
139: throw new Horde_Ldap_Exception('No PHP LDAP extension');
140: }
141: $this->setConfig($config);
142: $this->bind();
143: }
144:
145: /**
146: * Destructor.
147: */
148: public function __destruct()
149: {
150: $this->disconnect();
151: }
152:
153: /**
154: * Sets the internal configuration array.
155: *
156: * @param array $config Configuration hash.
157: */
158: protected function setConfig($config)
159: {
160: /* Parameter check -- probably should raise an error here if
161: * config is not an array. */
162: if (!is_array($config)) {
163: return;
164: }
165:
166: foreach ($config as $k => $v) {
167: if (isset($this->_config[$k])) {
168: $this->_config[$k] = $v;
169: }
170: }
171:
172: /* Ensure the host list is an array. */
173: if (is_array($this->_config['hostspec'])) {
174: $this->_hostList = $this->_config['hostspec'];
175: } else {
176: if (strlen($this->_config['hostspec'])) {
177: $this->_hostList = array($this->_config['hostspec']);
178: } else {
179: $this->_hostList = array();
180: /* This will cause an error in _connect(), so
181: * the user is notified about the failure. */
182: }
183: }
184:
185: /* Reset the down host list, which seems like a sensible thing
186: * to do if the config is being reset for some reason. */
187: $this->_downHostList = array();
188: }
189:
190: /**
191: * Bind or rebind to the LDAP server.
192: *
193: * This function binds with the given DN and password to the
194: * server. In case no connection has been made yet, it will be
195: * started and STARTTLS issued if appropiate.
196: *
197: * The internal bind configuration is not being updated, so if you
198: * call bind() without parameters, you can rebind with the
199: * credentials provided at first connecting to the server.
200: *
201: * @param string $dn DN for binding.
202: * @param string $password Password for binding.
203: *
204: * @throws Horde_Ldap_Exception
205: */
206: public function bind($dn = null, $password = null)
207: {
208: /* Fetch current bind credentials. */
209: if (empty($dn)) {
210: $dn = $this->_config['binddn'];
211: }
212: if (empty($password)) {
213: $password = $this->_config['bindpw'];
214: }
215:
216: /* Connect first, if we haven't so far. This will also bind
217: * us to the server. */
218: if (!$this->_link) {
219: /* Store old credentials so we can revert them later, then
220: * overwrite config with new bind credentials. */
221: $olddn = $this->_config['binddn'];
222: $oldpw = $this->_config['bindpw'];
223:
224: /* Overwrite bind credentials in config so
225: * _connect() knows about them. */
226: $this->_config['binddn'] = $dn;
227: $this->_config['bindpw'] = $password;
228:
229: /* Try to connect with provided credentials. */
230: $msg = $this->_connect();
231:
232: /* Reset to previous config. */
233: $this->_config['binddn'] = $olddn;
234: $this->_config['bindpw'] = $oldpw;
235: return;
236: }
237:
238: /* Do the requested bind as we are asked to bind manually. */
239: if (empty($dn)) {
240: /* Anonymous bind. */
241: $msg = @ldap_bind($this->_link);
242: } else {
243: /* Privileged bind. */
244: $msg = @ldap_bind($this->_link, $dn, $password);
245: }
246: if (!$msg) {
247: throw new Horde_Ldap_Exception('Bind failed: ' . @ldap_error($this->_link),
248: @ldap_errno($this->_link));
249: }
250: }
251:
252: /**
253: * Connects to the LDAP server.
254: *
255: * This function connects to the LDAP server specified in the
256: * configuration, binds and set up the LDAP protocol as needed.
257: *
258: * @throws Horde_Ldap_Exception
259: */
260: protected function _connect()
261: {
262: /* Connecting is briefly described in RFC1777. Basicly it works like
263: * this:
264: * 1. set up TCP connection
265: * 2. secure that connection if neccessary
266: * 3a. setVersion to tell server which version we want to speak
267: * 3b. perform bind
268: * 3c. setVersion to tell server which version we want to speak
269: * together with a test for supported versions
270: * 4. set additional protocol options */
271:
272: /* Return if we are already connected. */
273: if ($this->_link) {
274: return;
275: }
276:
277: /* Connnect to the LDAP server if we are not connected. Note that
278: * ldap_connect() may return a link value even if no connection is
279: * made. We need to do at least one anonymous bind to ensure that a
280: * connection is actually valid.
281: *
282: * See: http://www.php.net/manual/en/function.ldap-connect.php */
283:
284: /* Default error message in case all connection attempts fail but no
285: * message is set. */
286: $current_error = new Horde_Ldap_Exception('Unknown connection error');
287:
288: /* Catch empty $_hostList arrays. */
289: if (!is_array($this->_hostList) || !count($this->_hostList)) {
290: throw new Horde_Ldap_Exception('No servers configured');
291: }
292:
293: /* Cycle through the host list. */
294: foreach ($this->_hostList as $host) {
295: /* Ensure we have a valid string for host name. */
296: if (is_array($host)) {
297: $current_error = new Horde_Ldap_Exception('No Servers configured');
298: continue;
299: }
300:
301: /* Skip this host if it is known to be down. */
302: if (in_array($host, $this->_downHostList)) {
303: continue;
304: }
305:
306: /* Record the host that we are actually connecting to in case we
307: * need it later. */
308: $this->_config['hostspec'] = $host;
309:
310: /* Attempt a connection. */
311: $this->_link = @ldap_connect($host, $this->_config['port']);
312: if (!$this->_link) {
313: $current_error = new Horde_Ldap_Exception('Could not connect to ' . $host . ':' . $this->_config['port']);
314: $this->_downHostList[] = $host;
315: continue;
316: }
317:
318: /* If we're supposed to use TLS, do so before we try to bind, as
319: * some strict servers only allow binding via secure
320: * connections. */
321: if ($this->_config['tls']) {
322: try {
323: $this->startTLS();
324: } catch (Horde_Ldap_Exception $e) {
325: $current_error = $e;
326: $this->_link = false;
327: $this->_downHostList[] = $host;
328: continue;
329: }
330: }
331:
332: /* Try to set the configured LDAP version on the connection if LDAP
333: * server needs that before binding (eg OpenLDAP).
334: * This could be necessary since RFC 1777 states that the protocol
335: * version has to be set at the bind request.
336: * We use force here which means that the test in the rootDSE is
337: * skipped; this is neccessary, because some strict LDAP servers
338: * only allow to read the LDAP rootDSE (which tells us the
339: * supported protocol versions) with authenticated clients.
340: * This may fail in which case we try again after binding.
341: * In this case, most probably the bind() or setVersion() call
342: * below will also fail, providing error messages. */
343: $version_set = false;
344: $this->setVersion(0, true);
345:
346: /* Attempt to bind to the server. If we have credentials
347: * configured, we try to use them, otherwise it's an anonymous
348: * bind.
349: * As stated by RFC 1777, the bind request should be the first
350: * operation to be performed after the connection is established.
351: * This may give an protocol error if the server does not support
352: * v2 binds and the above call to setVersion() failed.
353: * If the above call failed, we try an v2 bind here and set the
354: * version afterwards (with checking to the rootDSE). */
355: try {
356: $this->bind();
357: } catch (Exception $e) {
358: /* The bind failed, discard link and save error msg.
359: * Then record the host as down and try next one. */
360: if ($this->errorName($e->getCode()) == 'LDAP_PROTOCOL_ERROR' &&
361: !$version_set) {
362: /* Provide a finer grained error message if protocol error
363: * arises because of invalid version. */
364: $e = new Horde_Ldap_Exception($e->getMessage() . ' (could not set LDAP protocol version to ' . $this->_config['version'].')', $e->getCode());
365: }
366: $this->_link = false;
367: $current_error = $e;
368: $this->_downHostList[] = $host;
369: continue;
370: }
371:
372: /* Set desired LDAP version if not successfully set before.
373: * Here, a check against the rootDSE is performed, so we get a
374: * error message if the server does not support the version.
375: * The rootDSE entry should tell us which LDAP versions are
376: * supported. However, some strict LDAP servers only allow
377: * bound users to read the rootDSE. */
378: if (!$version_set) {
379: try {
380: $this->setVersion();
381: } catch (Exception $e) {
382: $current_error = $e;
383: $this->_link = false;
384: $this->_downHostList[] = $host;
385: continue;
386: }
387: }
388:
389: /* Set LDAP parameters, now that we know we have a valid
390: * connection. */
391: if (isset($this->_config['options']) &&
392: is_array($this->_config['options']) &&
393: count($this->_config['options'])) {
394: foreach ($this->_config['options'] as $opt => $val) {
395: try {
396: $this->setOption($opt, $val);
397: } catch (Exception $e) {
398: $current_error = $e;
399: $this->_link = false;
400: $this->_downHostList[] = $host;
401: continue 2;
402: }
403: }
404: }
405:
406: /* At this stage we have connected, bound, and set up options, so
407: * we have a known good LDAP server. Time to go home. */
408: return;
409: }
410:
411: /* All connection attempts have failed, return the last error. */
412: throw $current_error;
413: }
414:
415: /**
416: * Reconnects to the LDAP server.
417: *
418: * In case the connection to the LDAP service has dropped out for some
419: * reason, this function will reconnect, and re-bind if a bind has been
420: * attempted in the past. It is probably most useful when the server list
421: * provided to the new() or _connect() function is an array rather than a
422: * single host name, because in that case it will be able to connect to a
423: * failover or secondary server in case the primary server goes down.
424: *
425: * This method just tries to re-establish the current connection. It will
426: * sleep for the current backoff period (seconds) before attempting the
427: * connect, and if the connection fails it will double the backoff period,
428: * but not try again. If you want to ensure a reconnection during a
429: * transient period of server downtime then you need to call this function
430: * in a loop.
431: *
432: * @throws Horde_Ldap_Exception
433: */
434: protected function _reconnect()
435: {
436: /* Return if we are already connected. */
437: if ($this->_link) {
438: return;
439: }
440:
441: /* Sleep for a backoff period in seconds. */
442: sleep($this->_config['current_backoff']);
443:
444: /* Retry all available connections. */
445: $this->_downHostList = array();
446:
447: try {
448: $this->_connect();
449: } catch (Horde_Ldap_Exception $e) {
450: $this->_config['current_backoff'] *= 2;
451: if ($this->_config['current_backoff'] > $this->_config['max_backoff']) {
452: $this->_config['current_backoff'] = $this->_config['max_backoff'];
453: }
454: throw $e;
455: }
456:
457: /* Now we should be able to safely (re-)bind. */
458: try {
459: $this->bind();
460: } catch (Exception $e) {
461: $this->_config['current_backoff'] *= 2;
462: if ($this->_config['current_backoff'] > $this->_config['max_backoff']) {
463: $this->_config['current_backoff'] = $this->_config['max_backoff'];
464: }
465:
466: /* $this->_config['hostspec'] should have had the last connected
467: * host stored in it by _connect(). Since we are unable to
468: * bind to that host we can safely assume that it is down or has
469: * some other problem. */
470: $this->_downHostList[] = $this->_config['hostspec'];
471: throw $e;
472: }
473:
474: /* At this stage we have connected, bound, and set up options, so we
475: * have a known good LDAP server. Time to go home. */
476: $this->_config['current_backoff'] = $this->_config['min_backoff'];
477: }
478:
479: /**
480: * Closes the LDAP connection.
481: */
482: public function disconnect()
483: {
484: @ldap_close($this->_link);
485: }
486:
487: /**
488: * Starts an encrypted session.
489: *
490: * @throws Horde_Ldap_Exception
491: */
492: public function startTLS()
493: {
494: /* Test to see if the server supports TLS first.
495: * This is done via testing the extensions offered by the server.
496: * The OID 1.3.6.1.4.1.1466.20037 tells whether TLS is supported. */
497: try {
498: $rootDSE = $this->rootDSE();
499: } catch (Exception $e) {
500: throw new Horde_Ldap_Exception('Unable to fetch rootDSE entry to see if TLS is supported: ' . $e->getMessage(), $e->getCode());
501: }
502:
503: try {
504: $supported_extensions = $rootDSE->getValue('supportedExtension');
505: } catch (Exception $e) {
506: throw new Horde_Ldap_Exception('Unable to fetch rootDSE attribute "supportedExtension" to see if TLS is supoported: ' . $e->getMessage(), $e->getCode());
507: }
508:
509: if (!in_array('1.3.6.1.4.1.1466.20037', $supported_extensions)) {
510: throw new Horde_Ldap_Exception('Server reports that it does not support TLS');
511: }
512:
513: if (!@ldap_start_tls($this->_link)) {
514: throw new Horde_Ldap_Exception('TLS not started: ' . @ldap_error($this->_link),
515: @ldap_errno($this->_link));
516: }
517: }
518:
519: /**
520: * Adds a new entry to the directory.
521: *
522: * This also links the entry to the connection used for the add, if it was
523: * a fresh entry.
524: *
525: * @see HordeLdap_Entry::createFresh()
526: *
527: * @param Horde_Ldap_Entry $entry An LDAP entry.
528: *
529: * @throws Horde_Ldap_Exception
530: */
531: public function add(Horde_Ldap_Entry $entry)
532: {
533: /* Continue attempting the add operation in a loop until we get a
534: * success, a definitive failure, or the world ends. */
535: while (true) {
536: $link = $this->getLink();
537: if ($link === false) {
538: /* We do not have a successful connection yet. The call to
539: * getLink() would have kept trying if we wanted one. */
540: throw new Horde_Ldap_Exception('Could not add entry ' . $entry->dn() . ' no valid LDAP connection could be found.');
541: }
542:
543: if (@ldap_add($link, $entry->dn(), $entry->getValues())) {
544: /* Entry successfully added, we should update its Horde_Ldap
545: * reference in case it is not set so far (fresh entry). */
546: try {
547: $entry->getLDAP();
548: } catch (Horde_Ldap_Exception $e) {
549: $entry->setLDAP($this);
550: }
551: /* Store that the entry is present inside the directory. */
552: $entry->markAsNew(false);
553: return;
554: }
555:
556: /* We have a failure. What kind? We may be able to reconnect and
557: * try again. */
558: $error_code = @ldap_errno($link);
559: if ($this->errorName($error_code) != 'LDAP_OPERATIONS_ERROR' |
560: !$this->_config['auto_reconnect']) {
561: /* Errors other than the above are just passed back to the user
562: * so he may react upon them. */
563: throw new Horde_Ldap_Exception('Could not add entry ' . $entry->dn() . ': ' . ldap_err2str($error_code), $error_code);
564: }
565:
566: /* The server has disconnected before trying the operation. We
567: * should try again, possibly with a different server. */
568: $this->_link = false;
569: $this->_reconnect();
570: }
571: }
572:
573: /**
574: * Deletes an entry from the directory.
575: *
576: * @param string|Horde_Ldap_Entry $dn DN string or Horde_Ldap_Entry.
577: * @param boolean $recursive Should we delete all children
578: * recursivelx as well?
579: * @throws Horde_Ldap_Exception
580: */
581: public function delete($dn, $recursive = false)
582: {
583: if ($dn instanceof Horde_Ldap_Entry) {
584: $dn = $dn->dn();
585: }
586: if (!is_string($dn)) {
587: throw new Horde_Ldap_Exception('Parameter is not a string nor an entry object!');
588: }
589:
590: /* Recursive delete searches for children and calls delete for them. */
591: if ($recursive) {
592: $result = @ldap_list($this->_link, $dn, '(objectClass=*)', array(null), 0, 0);
593: if ($result && @ldap_count_entries($this->_link, $result)) {
594: for ($subentry = @ldap_first_entry($this->_link, $result);
595: $subentry;
596: $subentry = @ldap_next_entry($this->_link, $subentry)) {
597: $this->delete(@ldap_get_dn($this->_link, $subentry), true);
598: }
599: }
600: }
601:
602: /* Continue the delete operation in a loop until we get a success, or a
603: * definitive failure. */
604: while (true) {
605: $link = $this->getLink();
606: if (!$link) {
607: /* We do not have a successful connection yet. The call to
608: * getLink() would have kept trying if we wanted one. */
609: throw new Horde_Ldap_Exception('Could not add entry ' . $dn . ' no valid LDAP connection could be found.');
610: }
611:
612: $s = @ldap_delete($link, $dn);
613: if ($s) {
614: /* Entry successfully deleted. */
615: return;
616: }
617:
618: /* We have a failure. What kind? We may be able to reconnect and
619: * try again. */
620: $error_code = @ldap_errno($link);
621: if ($this->errorName($error_code) == 'LDAP_OPERATIONS_ERROR' &&
622: $this->_config['auto_reconnect']) {
623: /* The server has disconnected before trying the operation. We
624: * should try again, possibly with a different server. */
625: $this->_link = false;
626: $this->_reconnect();
627: } elseif ($this->errorName($error_code) == 'LDAP_NOT_ALLOWED_ON_NONLEAF') {
628: /* Subentries present, server refused to delete.
629: * Deleting subentries is the clients responsibility, but since
630: * the user may not know of the subentries, we do not force
631: * that here but instead notify the developer so he may take
632: * actions himself. */
633: throw new Horde_Ldap_Exception('Could not delete entry ' . $dn . ' because of subentries. Use the recursive parameter to delete them.', $error_code);
634: } else {
635: /* Errors other than the above catched are just passed back to
636: * the user so he may react upon them. */
637: throw new Horde_Ldap_Exception('Could not delete entry ' . $dn . ': ' . ldap_err2str($error_code), $error_code);
638: }
639: }
640: }
641:
642: /**
643: * Modifies an LDAP entry on the server.
644: *
645: * The $params argument is an array of actions and should be something like
646: * this:
647: * <code>
648: * array('add' => array('attribute1' => array('val1', 'val2'),
649: * 'attribute2' => array('val1')),
650: * 'delete' => array('attribute1'),
651: * 'replace' => array('attribute1' => array('val1')),
652: * 'changes' => array('add' => ...,
653: * 'replace' => ...,
654: * 'delete' => array('attribute1', 'attribute2' => array('val1')))
655: * </code>
656: *
657: * The order of execution is as following:
658: * 1. adds from 'add' array
659: * 2. deletes from 'delete' array
660: * 3. replaces from 'replace' array
661: * 4. changes (add, replace, delete) in order of appearance
662: *
663: * The function calls the corresponding functions of an Horde_Ldap_Entry
664: * object. A detailed description of array structures can be found there.
665: *
666: * Unlike the modification methods provided by the Horde_Ldap_Entry object,
667: * this method will instantly carry out an update() after each operation,
668: * thus modifying "directly" on the server.
669: *
670: * @see Horde_Ldap_Entry::add()
671: * @see Horde_Ldap_Entry::delete()
672: * @see Horde_Ldap_Entry::replace()
673: *
674: * @param string|Horde_Ldap_Entry $entry DN string or Horde_Ldap_Entry.
675: * @param array $parms Array of changes
676: *
677: * @throws Horde_Ldap_Exception
678: */
679: public function modify($entry, $parms = array())
680: {
681: if (is_string($entry)) {
682: $entry = $this->getEntry($entry);
683: }
684: if (!($entry instanceof Horde_Ldap_Entry)) {
685: throw new Horde_Ldap_Exception('Parameter is not a string nor an entry object!');
686: }
687:
688: /* Perform changes mentioned separately. */
689: foreach (array('add', 'delete', 'replace') as $action) {
690: if (!isset($parms[$action])) {
691: continue;
692: }
693: $entry->$action($parms[$action]);
694: $entry->setLDAP($this);
695:
696: /* Because the ldap_*() functions are called inside
697: * Horde_Ldap_Entry::update(), we have to trap the error codes
698: * issued from that if we want to support reconnection. */
699: while (true) {
700: try {
701: $entry->update();
702: break;
703: } catch (Exception $e) {
704: /* We have a failure. What kind? We may be able to
705: * reconnect and try again. */
706: if ($this->errorName($e->getCode()) != 'LDAP_OPERATIONS_ERROR' ||
707: !$this->_config['auto_reconnect']) {
708: /* Errors other than the above catched are just passed
709: * back to the user so he may react upon them. */
710: throw new Horde_Ldap_Exception('Could not modify entry: ' . $e->getMessage());
711: }
712: /* The server has disconnected before trying the operation.
713: * We should try again, possibly with a different
714: * server. */
715: $this->_link = false;
716: $this->_reconnect();
717: }
718: }
719: }
720:
721: if (!isset($parms['changes']) || !is_array($parms['changes'])) {
722: return;
723: }
724:
725: /* Perform combined changes in 'changes' array. */
726: foreach ($parms['changes'] as $action => $value) {
727: $this->modify($entry, array($action => $value));
728: }
729: }
730:
731: /**
732: * Runs an LDAP search query.
733: *
734: * $base and $filter may be ommitted. The one from config will then be
735: * used. $base is either a DN-string or an Horde_Ldap_Entry object in which
736: * case its DN will be used.
737: *
738: * $params may contain:
739: * - scope: The scope which will be used for searching, defaults to 'sub':
740: * - base: Just one entry
741: * - sub: The whole tree
742: * - one: Immediately below $base
743: * - sizelimit: Limit the number of entries returned
744: * (default: 0 = unlimited)
745: * - timelimit: Limit the time spent for searching (default: 0 = unlimited)
746: * - attrsonly: If true, the search will only return the attribute names
747: * - attributes: Array of attribute names, which the entry should contain.
748: * It is good practice to limit this to just the ones you
749: * need.
750: *
751: * You cannot override server side limitations to sizelimit and timelimit:
752: * You can always only lower a given limit.
753: *
754: * @todo implement search controls (sorting etc)
755: *
756: * @param string|Horde_Ldap_Entry $base LDAP searchbase.
757: * @param string|Horde_Ldap_Filter $filter LDAP search filter.
758: * @param array $params Array of options.
759: *
760: * @return Horde_Ldap_Search The search result.
761: * @throws Horde_Ldap_Exception
762: */
763: public function search($base = null, $filter = null, $params = array())
764: {
765: if (is_null($base)) {
766: $base = $this->_config['basedn'];
767: }
768: if ($base instanceof Horde_Ldap_Entry) {
769: /* Fetch DN of entry, making searchbase relative to the entry. */
770: $base = $base->dn();
771: }
772: if (is_null($filter)) {
773: $filter = $this->_config['filter'];
774: }
775: if ($filter instanceof Horde_Ldap_Filter) {
776: /* Convert Horde_Ldap_Filter to string representation. */
777: $filter = (string)$filter;
778: }
779:
780: /* Setting search parameters. */
781: $sizelimit = isset($params['sizelimit']) ? $params['sizelimit'] : 0;
782: $timelimit = isset($params['timelimit']) ? $params['timelimit'] : 0;
783: $attrsonly = isset($params['attrsonly']) ? $params['attrsonly'] : 0;
784: $attributes = isset($params['attributes']) ? $params['attributes'] : array();
785:
786: /* Ensure $attributes to be an array in case only one attribute name
787: * was given as string. */
788: if (!is_array($attributes)) {
789: $attributes = array($attributes);
790: }
791:
792: /* Reorganize the $attributes array index keys sometimes there are
793: * problems with not consecutive indexes. */
794: $attributes = array_values($attributes);
795:
796: /* Scoping makes searches faster! */
797: $scope = isset($params['scope'])
798: ? $params['scope']
799: : $this->_config['scope'];
800:
801: switch ($scope) {
802: case 'one':
803: $search_function = 'ldap_list';
804: break;
805: case 'base':
806: $search_function = 'ldap_read';
807: break;
808: default:
809: $search_function = 'ldap_search';
810: }
811:
812: /* Continue attempting the search operation until we get a success or a
813: * definitive failure. */
814: while (true) {
815: $link = $this->getLink();
816: $search = @call_user_func($search_function,
817: $link,
818: $base,
819: $filter,
820: $attributes,
821: $attrsonly,
822: $sizelimit,
823: $timelimit);
824:
825: if ($errno = @ldap_errno($link)) {
826: $err = $this->errorName($errno);
827: if ($err == 'LDAP_NO_SUCH_OBJECT' ||
828: $err == 'LDAP_SIZELIMIT_EXCEEDED') {
829: return new Horde_Ldap_Search($search, $this, $attributes);
830: }
831: if ($err == 'LDAP_FILTER_ERROR') {
832: /* Bad search filter. */
833: throw new Horde_Ldap_Exception(ldap_err2str($errno) . ' ($filter)', $errno);
834: }
835: if ($err == 'LDAP_OPERATIONS_ERROR' &&
836: $this->_config['auto_reconnect']) {
837: $this->_link = false;
838: $this->_reconnect();
839: } else {
840: $msg = "\nParameters:\nBase: $base\nFilter: $filter\nScope: $scope";
841: throw new Horde_Ldap_Exception(ldap_err2str($errno) . $msg, $errno);
842: }
843: } else {
844: return new Horde_Ldap_Search($search, $this, $attributes);
845: }
846: }
847: }
848:
849: /**
850: * Returns the DN of a user.
851: *
852: * The purpose is to quickly find the full DN of a user so it can be used
853: * to re-bind as this user. This method requires the 'user' configuration
854: * parameter to be set.
855: *
856: * @param string $user The user to find.
857: *
858: * @return string The user's full DN.
859: * @throws Horde_Ldap_Exception
860: * @throws Horde_Exception_NotFound
861: */
862: public function findUserDN($user)
863: {
864: $filter = Horde_Ldap_Filter::combine(
865: 'and',
866: array(Horde_Ldap_Filter::build($this->_config['user']),
867: Horde_Ldap_Filter::create($this->_config['user']['uid'], 'equals', $user)));
868: $search = $this->search(
869: null,
870: $filter,
871: array('attributes' => array($this->_config['user']['uid'])));
872: if (!$search->count()) {
873: throw new Horde_Exception_NotFound('DN for user ' . $user . ' not found');
874: }
875: $entry = $search->shiftEntry();
876: return $entry->currentDN();
877: }
878:
879: /**
880: * Sets an LDAP option.
881: *
882: * @param string $option Option to set.
883: * @param mixed $value Value to set option to.
884: *
885: * @throws Horde_Ldap_Exception
886: */
887: public function setOption($option, $value)
888: {
889: if (!$this->_link) {
890: throw new Horde_Ldap_Exception('Could not set LDAP option: No LDAP connection');
891: }
892: if (!defined($option)) {
893: throw new Horde_Ldap_Exception('Unkown option requested');
894: }
895: if (@ldap_set_option($this->_link, constant($option), $value)) {
896: return;
897: }
898: $err = @ldap_errno($this->_link);
899: if ($err) {
900: throw new Horde_Ldap_Exception(ldap_err2str($err), $err);
901: }
902: throw new Horde_Ldap_Exception('Unknown error');
903: }
904:
905: /**
906: * Returns an LDAP option value.
907: *
908: * @param string $option Option to get.
909: *
910: * @return Horde_Ldap_Error|string Horde_Ldap_Error or option value
911: * @throws Horde_Ldap_Exception
912: */
913: public function getOption($option)
914: {
915: if (!$this->_link) {
916: throw new Horde_Ldap_Exception('No LDAP connection');
917: }
918: if (!defined($option)) {
919: throw new Horde_Ldap_Exception('Unkown option requested');
920: }
921: if (@ldap_get_option($this->_link, constant($option), $value)) {
922: return $value;
923: }
924: $err = @ldap_errno($this->_link);
925: if ($err) {
926: throw new Horde_Ldap_Exception(ldap_err2str($err), $err);
927: }
928: throw new Horde_Ldap_Exception('Unknown error');
929: }
930:
931: /**
932: * Returns the LDAP protocol version that is used on the connection.
933: *
934: * A lot of LDAP functionality is defined by what protocol version
935: * the LDAP server speaks. This might be 2 or 3.
936: *
937: * @return integer The protocol version.
938: */
939: public function getVersion()
940: {
941: if ($this->_link) {
942: $version = $this->getOption('LDAP_OPT_PROTOCOL_VERSION');
943: } else {
944: $version = $this->_config['version'];
945: }
946: return $version;
947: }
948:
949: /**
950: * Sets the LDAP protocol version that is used on the connection.
951: *
952: * @todo Checking via the rootDSE takes much time - why? fetching
953: * and instanciation is quick!
954: *
955: * @param integer $version LDAP version that should be used.
956: * @param boolean $force If set to true, the check against the rootDSE
957: * will be skipped.
958: *
959: * @throws Horde_Ldap_Exception
960: */
961: public function setVersion($version = 0, $force = false)
962: {
963: if (!$version) {
964: $version = $this->_config['version'];
965: }
966:
967: /* Check to see if the server supports this version first.
968: *
969: * TODO: Why is this so horribly slow? $this->rootDSE() is very fast,
970: * as well as Horde_Ldap_RootDse(). Seems like a problem at copying the
971: * object inside PHP?? Additionally, this is not always
972: * reproducable... */
973: if (!$force) {
974: try {
975: $rootDSE = $this->rootDSE();
976: $supported_versions = $rootDSE->getValue('supportedLDAPVersion');
977: if (is_string($supported_versions)) {
978: $supported_versions = array($supported_versions);
979: }
980: $check_ok = in_array($version, $supported_versions);
981: } catch (Horde_Ldap_Exception $e) {
982: /* If we don't get a root DSE, this is probably a v2 server. */
983: $check_ok = $version < 3;
984: }
985: }
986: $check_ok = true;
987:
988: if ($force || $check_ok) {
989: return $this->setOption('LDAP_OPT_PROTOCOL_VERSION', $version);
990: }
991: throw new Horde_Ldap_Exception('LDAP Server does not support protocol version ' . $version);
992: }
993:
994:
995: /**
996: * Returns whether a DN exists in the directory.
997: *
998: * @param string|Horde_Ldap_Entry $dn The DN of the object to test.
999: *
1000: * @return boolean True if the DN exists.
1001: * @throws Horde_Ldap_Exception
1002: */
1003: public function exists($dn)
1004: {
1005: if ($dn instanceof Horde_Ldap_Entry) {
1006: $dn = $dn->dn();
1007: }
1008: if (!is_string($dn)) {
1009: throw new Horde_Ldap_Exception('Parameter $dn is not a string nor an entry object!');
1010: }
1011:
1012: /* Make dn relative to parent. */
1013: $base = Horde_Ldap_Util::explodeDN($dn, array('casefold' => 'none', 'reverse' => false, 'onlyvalues' => false));
1014: $entry_rdn = array_shift($base);
1015: $base = Horde_Ldap_Util::canonicalDN($base);
1016:
1017: $result = @ldap_list($this->_link, $base, $entry_rdn, array(), 1, 1);
1018: if (@ldap_count_entries($this->_link, $result)) {
1019: return true;
1020: }
1021: if ($this->errorName(@ldap_errno($this->_link)) == 'LDAP_NO_SUCH_OBJECT') {
1022: return false;
1023: }
1024: if (@ldap_errno($this->_link)) {
1025: throw new Horde_Ldap_Exception(@ldap_error($this->_link), @ldap_errno($this->_link));
1026: }
1027: return false;
1028: }
1029:
1030:
1031: /**
1032: * Returns a specific entry based on the DN.
1033: *
1034: * @todo Maybe a check against the schema should be done to be
1035: * sure the attribute type exists.
1036: *
1037: * @param string $dn DN of the entry that should be fetched.
1038: * @param array $attributes Array of Attributes to select. If ommitted, all
1039: * attributes are fetched.
1040: *
1041: * @return Horde_Ldap_Entry A Horde_Ldap_Entry object.
1042: * @throws Horde_Ldap_Exception
1043: * @throws Horde_Exception_NotFound
1044: */
1045: public function getEntry($dn, $attributes = array())
1046: {
1047: if (!is_array($attributes)) {
1048: $attributes = array($attributes);
1049: }
1050: $result = $this->search($dn, '(objectClass=*)',
1051: array('scope' => 'base', 'attributes' => $attributes));
1052: if (!$result->count()) {
1053: throw new Horde_Exception_NotFound(sprintf('Could not fetch entry %s: no entry found', $dn));
1054: }
1055: $entry = $result->shiftEntry();
1056: if (!$entry) {
1057: throw new Horde_Ldap_Exception('Could not fetch entry (error retrieving entry from search result)');
1058: }
1059: return $entry;
1060: }
1061:
1062: /**
1063: * Renames or moves an entry.
1064: *
1065: * This method will instantly carry out an update() after the
1066: * move, so the entry is moved instantly.
1067: *
1068: * You can pass an optional Horde_Ldap object. In this case, a
1069: * cross directory move will be performed which deletes the entry
1070: * in the source (THIS) directory and adds it in the directory
1071: * $target_ldap.
1072: *
1073: * A cross directory move will switch the entry's internal LDAP
1074: * reference so updates to the entry will go to the new directory.
1075: *
1076: * If you want to do a cross directory move, you need to pass an
1077: * Horde_Ldap_Entry object, otherwise the attributes will be
1078: * empty.
1079: *
1080: * @param string|Horde_Ldap_Entry $entry An LDAP entry.
1081: * @param string $newdn The new location.
1082: * @param Horde_Ldap $target_ldap Target directory for cross
1083: * server move.
1084: *
1085: * @throws Horde_Ldap_Exception
1086: */
1087: public function move($entry, $newdn, $target_ldap = null)
1088: {
1089: if (is_string($entry)) {
1090: if ($target_ldap && $target_ldap !== $this) {
1091: throw new Horde_Ldap_Exception('Unable to perform cross directory move: operation requires a Horde_Ldap_Entry object');
1092: }
1093: $entry = $this->getEntry($entry);
1094: }
1095: if (!$entry instanceof Horde_Ldap_Entry) {
1096: throw new Horde_Ldap_Exception('Parameter $entry is expected to be a Horde_Ldap_Entry object! (If DN was passed, conversion failed)');
1097: }
1098: if ($target_ldap && !($target_ldap instanceof Horde_Ldap)) {
1099: throw new Horde_Ldap_Exception('Parameter $target_ldap is expected to be a Horde_Ldap object!');
1100: }
1101:
1102: if (!$target_ldap || $target_ldap === $this) {
1103: /* Local move. */
1104: $entry->dn($newdn);
1105: $entry->setLDAP($this);
1106: $entry->update();
1107: return;
1108: }
1109:
1110: /* Cross directory move. */
1111: if ($target_ldap->exists($newdn)) {
1112: throw new Horde_Ldap_Exception('Unable to perform cross directory move: entry does exist in target directory');
1113: }
1114: $entry->dn($newdn);
1115: try {
1116: $target_ldap->add($entry);
1117: } catch (Exception $e) {
1118: throw new Horde_Ldap_Exception('Unable to perform cross directory move: ' . $e->getMessage() . ' in target directory');
1119: }
1120:
1121: try {
1122: $this->delete($entry->currentDN());
1123: } catch (Exception $e) {
1124: try {
1125: $add_error_string = '';
1126: /* Undo add. */
1127: $target_ldap->delete($entry);
1128: } catch (Exception $e) {
1129: $add_error_string = ' Additionally, the deletion (undo add) of $entry in target directory failed.';
1130: }
1131: throw new Horde_Ldap_Exception('Unable to perform cross directory move: ' . $e->getMessage() . ' in source directory.' . $add_error_string);
1132: }
1133: $entry->setLDAP($target_ldap);
1134: }
1135:
1136: /**
1137: * Copies an entry to a new location.
1138: *
1139: * The entry will be immediately copied. Only attributes you have
1140: * selected will be copied.
1141: *
1142: * @param Horde_Ldap_Entry $entry An LDAP entry.
1143: * @param string $newdn New FQF-DN of the entry.
1144: *
1145: * @return Horde_Ldap_Entry The copied entry.
1146: * @throws Horde_Ldap_Exception
1147: */
1148: public function copy($entry, $newdn)
1149: {
1150: if (!$entry instanceof Horde_Ldap_Entry) {
1151: throw new Horde_Ldap_Exception('Parameter $entry is expected to be a Horde_Ldap_Entry object');
1152: }
1153:
1154: $newentry = Horde_Ldap_Entry::createFresh($newdn, $entry->getValues());
1155: $this->add($newentry);
1156:
1157: return $newentry;
1158: }
1159:
1160:
1161: /**
1162: * Returns the string for an LDAP errorcode.
1163: *
1164: * Made to be able to make better errorhandling. Function based
1165: * on DB::errorMessage().
1166: *
1167: * Hint: The best description of the errorcodes is found here:
1168: * http://www.directory-info.com/Ldap/LDAPErrorCodes.html
1169: *
1170: * @param integer $errorcode An error code.
1171: *
1172: * @return string The description for the error.
1173: */
1174: public static function errorName($errorcode)
1175: {
1176: $errorMessages = array(
1177: 0x00 => 'LDAP_SUCCESS',
1178: 0x01 => 'LDAP_OPERATIONS_ERROR',
1179: 0x02 => 'LDAP_PROTOCOL_ERROR',
1180: 0x03 => 'LDAP_TIMELIMIT_EXCEEDED',
1181: 0x04 => 'LDAP_SIZELIMIT_EXCEEDED',
1182: 0x05 => 'LDAP_COMPARE_FALSE',
1183: 0x06 => 'LDAP_COMPARE_TRUE',
1184: 0x07 => 'LDAP_AUTH_METHOD_NOT_SUPPORTED',
1185: 0x08 => 'LDAP_STRONG_AUTH_REQUIRED',
1186: 0x09 => 'LDAP_PARTIAL_RESULTS',
1187: 0x0a => 'LDAP_REFERRAL',
1188: 0x0b => 'LDAP_ADMINLIMIT_EXCEEDED',
1189: 0x0c => 'LDAP_UNAVAILABLE_CRITICAL_EXTENSION',
1190: 0x0d => 'LDAP_CONFIDENTIALITY_REQUIRED',
1191: 0x0e => 'LDAP_SASL_BIND_INPROGRESS',
1192: 0x10 => 'LDAP_NO_SUCH_ATTRIBUTE',
1193: 0x11 => 'LDAP_UNDEFINED_TYPE',
1194: 0x12 => 'LDAP_INAPPROPRIATE_MATCHING',
1195: 0x13 => 'LDAP_CONSTRAINT_VIOLATION',
1196: 0x14 => 'LDAP_TYPE_OR_VALUE_EXISTS',
1197: 0x15 => 'LDAP_INVALID_SYNTAX',
1198: 0x20 => 'LDAP_NO_SUCH_OBJECT',
1199: 0x21 => 'LDAP_ALIAS_PROBLEM',
1200: 0x22 => 'LDAP_INVALID_DN_SYNTAX',
1201: 0x23 => 'LDAP_IS_LEAF',
1202: 0x24 => 'LDAP_ALIAS_DEREF_PROBLEM',
1203: 0x30 => 'LDAP_INAPPROPRIATE_AUTH',
1204: 0x31 => 'LDAP_INVALID_CREDENTIALS',
1205: 0x32 => 'LDAP_INSUFFICIENT_ACCESS',
1206: 0x33 => 'LDAP_BUSY',
1207: 0x34 => 'LDAP_UNAVAILABLE',
1208: 0x35 => 'LDAP_UNWILLING_TO_PERFORM',
1209: 0x36 => 'LDAP_LOOP_DETECT',
1210: 0x3C => 'LDAP_SORT_CONTROL_MISSING',
1211: 0x3D => 'LDAP_INDEX_RANGE_ERROR',
1212: 0x40 => 'LDAP_NAMING_VIOLATION',
1213: 0x41 => 'LDAP_OBJECT_CLASS_VIOLATION',
1214: 0x42 => 'LDAP_NOT_ALLOWED_ON_NONLEAF',
1215: 0x43 => 'LDAP_NOT_ALLOWED_ON_RDN',
1216: 0x44 => 'LDAP_ALREADY_EXISTS',
1217: 0x45 => 'LDAP_NO_OBJECT_CLASS_MODS',
1218: 0x46 => 'LDAP_RESULTS_TOO_LARGE',
1219: 0x47 => 'LDAP_AFFECTS_MULTIPLE_DSAS',
1220: 0x50 => 'LDAP_OTHER',
1221: 0x51 => 'LDAP_SERVER_DOWN',
1222: 0x52 => 'LDAP_LOCAL_ERROR',
1223: 0x53 => 'LDAP_ENCODING_ERROR',
1224: 0x54 => 'LDAP_DECODING_ERROR',
1225: 0x55 => 'LDAP_TIMEOUT',
1226: 0x56 => 'LDAP_AUTH_UNKNOWN',
1227: 0x57 => 'LDAP_FILTER_ERROR',
1228: 0x58 => 'LDAP_USER_CANCELLED',
1229: 0x59 => 'LDAP_PARAM_ERROR',
1230: 0x5a => 'LDAP_NO_MEMORY',
1231: 0x5b => 'LDAP_CONNECT_ERROR',
1232: 0x5c => 'LDAP_NOT_SUPPORTED',
1233: 0x5d => 'LDAP_CONTROL_NOT_FOUND',
1234: 0x5e => 'LDAP_NO_RESULTS_RETURNED',
1235: 0x5f => 'LDAP_MORE_RESULTS_TO_RETURN',
1236: 0x60 => 'LDAP_CLIENT_LOOP',
1237: 0x61 => 'LDAP_REFERRAL_LIMIT_EXCEEDED',
1238: 1000 => 'Unknown Error');
1239:
1240: return isset($errorMessages[$errorcode]) ?
1241: $errorMessages[$errorcode] :
1242: 'Unknown Error (' . $errorcode . ')';
1243: }
1244:
1245: /**
1246: * Returns a rootDSE object
1247: *
1248: * This either fetches a fresh rootDSE object or returns it from
1249: * the internal cache for performance reasons, if possible.
1250: *
1251: * @param array $attrs Array of attributes to search for.
1252: *
1253: * @return Horde_Ldap_RootDse Horde_Ldap_RootDse object
1254: * @throws Horde_Ldap_Exception
1255: */
1256: public function rootDSE(array $attrs = array())
1257: {
1258: $attrs_signature = serialize($attrs);
1259:
1260: /* See if we need to fetch a fresh object, or if we already
1261: * requested this object with the same attributes. */
1262: if (!isset($this->_rootDSECache[$attrs_signature])) {
1263: $this->_rootDSECache[$attrs_signature] = new Horde_Ldap_RootDse($this, $attrs);
1264: }
1265:
1266: return $this->_rootDSECache[$attrs_signature];
1267: }
1268:
1269: /**
1270: * Returns a schema object
1271: *
1272: * @param string $dn Subschema entry dn.
1273: *
1274: * @return Horde_Ldap_Schema Horde_Ldap_Schema object
1275: * @throws Horde_Ldap_Exception
1276: */
1277: public function schema($dn = null)
1278: {
1279: /* If a schema caching object is registered, we use that to fetch a
1280: * schema object. */
1281: $key = 'Horde_Ldap_Schema_' . md5(serialize(array($this->_config['hostspec'], $this->_config['port'], $dn)));
1282: if (!$this->_schema && $this->_config['cache']) {
1283: $schema = $this->_config['cache']->get($key, $this->_config['cachettl']);
1284: if ($schema) {
1285: $this->_schema = @unserialize($schema);
1286: }
1287: }
1288:
1289: /* Fetch schema, if not tried before and no cached version available.
1290: * If we are already fetching the schema, we will skip fetching. */
1291: if (!$this->_schema) {
1292: /* Store a temporary error message so subsequent calls to schema()
1293: * can detect that we are fetching the schema already. Otherwise we
1294: * will get an infinite loop at Horde_Ldap_Schema. */
1295: $this->_schema = new Horde_Ldap_Exception('Schema not initialized');
1296: $this->_schema = new Horde_Ldap_Schema($this, $dn);
1297:
1298: /* If schema caching is active, advise the cache to store the
1299: * schema. */
1300: if ($this->_config['cache']) {
1301: $this->_config['cache']->set($key, serialize($this->_schema), $this->_config['cachettl']);
1302: }
1303: }
1304:
1305: if ($this->_schema instanceof Horde_Ldap_Exception) {
1306: throw $this->_schema;
1307: }
1308:
1309: return $this->_schema;
1310: }
1311:
1312: /**
1313: * Checks if PHP's LDAP extension is loaded.
1314: *
1315: * If it is not loaded, it tries to load it manually using PHP's dl().
1316: * It knows both windows-dll and *nix-so.
1317: *
1318: * @throws Horde_Ldap_Exception
1319: */
1320: public static function checkLDAPExtension()
1321: {
1322: if (!extension_loaded('ldap') && !@dl('ldap.' . PHP_SHLIB_SUFFIX)) {
1323: throw new Horde_Ldap_Exception('Unable to locate PHP LDAP extension. Please install it before using the Horde_Ldap package.');
1324: }
1325: }
1326:
1327: /**
1328: * @todo Remove this and expect all data to be UTF-8.
1329: *
1330: * Encodes given attributes to UTF8 if needed by schema.
1331: *
1332: * This function takes attributes in an array and then checks
1333: * against the schema if they need UTF8 encoding. If that is the
1334: * case, they will be encoded. An encoded array will be returned
1335: * and can be used for adding or modifying.
1336: *
1337: * $attributes is expected to be an array with keys describing
1338: * the attribute names and the values as the value of this attribute:
1339: * <code>$attributes = array('cn' => 'foo', 'attr2' => array('mv1', 'mv2'));</code>
1340: *
1341: * @param array $attributes An array of attributes.
1342: *
1343: * @return array|Horde_Ldap_Error An array of UTF8 encoded attributes or an error.
1344: */
1345: public function utf8Encode($attributes)
1346: {
1347: return $this->utf8($attributes, 'utf8_encode');
1348: }
1349:
1350: /**
1351: * @todo Remove this and expect all data to be UTF-8.
1352: *
1353: * Decodes the given attribute values if needed by schema
1354: *
1355: * $attributes is expected to be an array with keys describing
1356: * the attribute names and the values as the value of this attribute:
1357: * <code>$attributes = array('cn' => 'foo', 'attr2' => array('mv1', 'mv2'));</code>
1358: *
1359: * @param array $attributes Array of attributes
1360: *
1361: * @access public
1362: * @see utf8Encode()
1363: * @return array|Horde_Ldap_Error Array with decoded attribute values or Error
1364: */
1365: public function utf8Decode($attributes)
1366: {
1367: return $this->utf8($attributes, 'utf8_decode');
1368: }
1369:
1370: /**
1371: * @todo Remove this and expect all data to be UTF-8.
1372: *
1373: * Encodes or decodes attribute values if needed
1374: *
1375: * @param array $attributes Array of attributes
1376: * @param array $function Function to apply to attribute values
1377: *
1378: * @access protected
1379: * @return array Array of attributes with function applied to values.
1380: */
1381: protected function utf8($attributes, $function)
1382: {
1383: if (!is_array($attributes) || array_key_exists(0, $attributes)) {
1384: throw new Horde_Ldap_Exception('Parameter $attributes is expected to be an associative array');
1385: }
1386:
1387: if (!$this->_schema) {
1388: $this->_schema = $this->schema();
1389: }
1390:
1391: if (!$this->_link || !function_exists($function)) {
1392: return $attributes;
1393: }
1394:
1395: if (is_array($attributes) && count($attributes) > 0) {
1396:
1397: foreach ($attributes as $k => $v) {
1398:
1399: if (!isset($this->_schemaAttrs[$k])) {
1400:
1401: try {
1402: $attr = $this->_schema->get('attribute', $k);
1403: } catch (Exception $e) {
1404: continue;
1405: }
1406:
1407: if (false !== strpos($attr['syntax'], '1.3.6.1.4.1.1466.115.121.1.15')) {
1408: $encode = true;
1409: } else {
1410: $encode = false;
1411: }
1412: $this->_schemaAttrs[$k] = $encode;
1413:
1414: } else {
1415: $encode = $this->_schemaAttrs[$k];
1416: }
1417:
1418: if ($encode) {
1419: if (is_array($v)) {
1420: foreach ($v as $ak => $av) {
1421: $v[$ak] = call_user_func($function, $av);
1422: }
1423: } else {
1424: $v = call_user_func($function, $v);
1425: }
1426: }
1427: $attributes[$k] = $v;
1428: }
1429: }
1430: return $attributes;
1431: }
1432:
1433: /**
1434: * Returns the LDAP link resource.
1435: *
1436: * It will loop attempting to re-establish the connection if the
1437: * connection attempt fails and auto_reconnect has been turned on
1438: * (see the _config array documentation).
1439: *
1440: * @return resource LDAP link.
1441: */
1442: public function getLink()
1443: {
1444: if ($this->_config['auto_reconnect']) {
1445: while (true) {
1446: /* Return the link handle if we are already connected.
1447: * Otherwise try to reconnect. */
1448: if ($this->_link) {
1449: return $this->_link;
1450: }
1451: $this->_reconnect();
1452: }
1453: }
1454: return $this->_link;
1455: }
1456:
1457: /**
1458: * Builds an LDAP search filter fragment.
1459: *
1460: * @param string $lhs The attribute to test.
1461: * @param string $op The operator.
1462: * @param string $rhs The comparison value.
1463: * @param array $params Any additional parameters for the operator.
1464: *
1465: * @return string The LDAP search fragment.
1466: */
1467: public static function buildClause($lhs, $op, $rhs, $params = array())
1468: {
1469: switch ($op) {
1470: case 'LIKE':
1471: if (empty($rhs)) {
1472: return '(' . $lhs . '=*)';
1473: }
1474: if (!empty($params['begin'])) {
1475: return sprintf('(|(%s=%s*)(%s=* %s*))', $lhs, self::quote($rhs), $lhs, self::quote($rhs));
1476: }
1477: if (!empty($params['approximate'])) {
1478: return sprintf('(%s=~%s)', $lhs, self::quote($rhs));
1479: }
1480: return sprintf('(%s=*%s*)', $lhs, self::quote($rhs));
1481:
1482: default:
1483: return sprintf('(%s%s%s)', $lhs, $op, self::quote($rhs));
1484: }
1485: }
1486:
1487:
1488: /**
1489: * Escapes characters with special meaning in LDAP searches.
1490: *
1491: * @param string $clause The string to escape.
1492: *
1493: * @return string The escaped string.
1494: */
1495: public static function quote($clause)
1496: {
1497: return str_replace(array('\\', '(', ')', '*', "\0"),
1498: array('\\5c', '\(', '\)', '\*', "\\00"),
1499: $clause);
1500: }
1501:
1502: /**
1503: * Takes an array of DN elements and properly quotes it according to RFC
1504: * 1485.
1505: *
1506: * @param array $parts An array of tuples containing the attribute
1507: * name and that attribute's value which make
1508: * up the DN. Example:
1509: * <code>
1510: * $parts = array(0 => array('cn', 'John Smith'),
1511: * 1 => array('dc', 'example'),
1512: * 2 => array('dc', 'com'));
1513: * </code>
1514: *
1515: * @return string The properly quoted string DN.
1516: */
1517: public static function quoteDN($parts)
1518: {
1519: $dn = '';
1520: $count = count($parts);
1521: for ($i = 0; $i < $count; $i++) {
1522: if ($i > 0) {
1523: $dn .= ',';
1524: }
1525: $dn .= $parts[$i][0] . '=';
1526:
1527: // See if we need to quote the value.
1528: if (preg_match('/^\s|\s$|\s\s|[,+="\r\n<>#;]/', $parts[$i][1])) {
1529: $dn .= '"' . str_replace('"', '\\"', $parts[$i][1]) . '"';
1530: } else {
1531: $dn .= $parts[$i][1];
1532: }
1533: }
1534:
1535: return $dn;
1536: }
1537: }
1538: