Overview

Packages

  • Horde
    • Data
  • None
  • Turba

Classes

  • Turba
  • Turba_Api
  • Turba_Driver
  • Turba_Driver_Facebook
  • Turba_Driver_Favourites
  • Turba_Driver_Group
  • Turba_Driver_Imsp
  • Turba_Driver_Kolab
  • Turba_Driver_Ldap
  • Turba_Driver_Prefs
  • Turba_Driver_Share
  • Turba_Driver_Sql
  • Turba_Driver_Vbook
  • Turba_Exception
  • Turba_Factory_Driver
  • Turba_Form_AddContact
  • Turba_Form_Contact
  • Turba_Form_ContactBase
  • Turba_Form_CreateAddressBook
  • Turba_Form_DeleteAddressBook
  • Turba_Form_EditAddressBook
  • Turba_Form_EditContact
  • Turba_Form_EditContactGroup
  • Turba_List
  • Turba_LoginTasks_SystemTask_Upgrade
  • Turba_Object
  • Turba_Object_Group
  • Turba_Test
  • Turba_View_Browse
  • Turba_View_Contact
  • Turba_View_DeleteContact
  • Turba_View_Duplicates
  • Turba_View_EditContact
  • Turba_View_List
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: /**
  3:  * Turba directory driver implementation for PHP's LDAP extension.
  4:  *
  5:  * Copyright 2010-2012 Horde LLC (http://www.horde.org/)
  6:  *
  7:  * See the enclosed file LICENSE for license information (ASL).  If you did
  8:  * did not receive this file, see http://www.horde.org/licenses/apache.
  9:  *
 10:  * @author   Chuck Hagenbuch <chuck@horde.org>
 11:  * @author   Jon Parise <jon@csh.rit.edu>
 12:  * @category Horde
 13:  * @license  http://www.horde.org/licenses/apache ASL
 14:  * @package  Turba
 15:  */
 16: class Turba_Driver_Ldap extends Turba_Driver
 17: {
 18:     /**
 19:      * Handle for the current LDAP connection.
 20:      *
 21:      * @var resource
 22:      */
 23:     protected $_ds = 0;
 24: 
 25:     /**
 26:      * Cache _getSyntax() calls.
 27:      *
 28:      * @var array
 29:      */
 30:     protected $_syntaxCache = array();
 31: 
 32:     /**
 33:      * Constructs a new Turba LDAP driver object.
 34:      *
 35:      * @param string $name   The source name
 36:      * @param array $params  Hash containing additional configuration parameters.
 37:      *
 38:      * @return Turba_Driver_Ldap
 39:      */
 40:     public function __construct($name = '', array $params = array())
 41:     {
 42:         if (!Horde_Util::extensionExists('ldap')) {
 43:             throw new Turba_Exception(_("LDAP support is required but the LDAP module is not available or not loaded."));
 44:         }
 45: 
 46:         $params = array_merge(array(
 47:             'charset' => '',
 48:             'deref' => LDAP_DEREF_NEVER,
 49:             'multiple_entry_separator' => ', ',
 50:             'port' => 389,
 51:             'root' => '',
 52:             'scope' => 'sub',
 53:             'server' => 'localhost'
 54:         ), $params);
 55: 
 56:         parent::__construct($name, $params);
 57:     }
 58: 
 59:     /**
 60:      * Initiate LDAP connection.
 61:      *
 62:      * Not done in __construct(), only when a read or write action is
 63:      * necessary.
 64:      */
 65:     protected function _connect()
 66:     {
 67:         if ($this->_ds) {
 68:             return;
 69:         }
 70: 
 71:         if (!($this->_ds = @ldap_connect($this->_params['server'], $this->_params['port']))) {
 72:             throw new Turba_Exception(_("Connection failure"));
 73:         }
 74: 
 75:         /* Set the LDAP protocol version. */
 76:         if (!empty($this->_params['version'])) {
 77:             @ldap_set_option($this->_ds, LDAP_OPT_PROTOCOL_VERSION, $this->_params['version']);
 78:         }
 79: 
 80:         /* Set the LDAP deref option for dereferencing aliases. */
 81:         if (!empty($this->_params['deref'])) {
 82:             @ldap_set_option($this->_ds, LDAP_OPT_DEREF, $this->_params['deref']);
 83:         }
 84: 
 85:         /* Set the LDAP referrals. */
 86:         if (!empty($this->_params['referrals'])) {
 87:             @ldap_set_option($this->_ds, LDAP_OPT_REFERRALS, $this->_params['referrals']);
 88:         }
 89: 
 90:         /* Start TLS if we're using it. */
 91:         if (!empty($this->_params['tls']) &&
 92:             !@ldap_start_tls($this->_ds)) {
 93:             throw new Turba_Exception(sprintf(_("STARTTLS failed: (%s) %s"), ldap_errno($this->_ds), ldap_error($this->_ds)));
 94:         }
 95: 
 96:         /* Bind to the server. */
 97:         if (isset($this->_params['bind_dn']) &&
 98:             isset($this->_params['bind_password'])) {
 99:             $error = !@ldap_bind($this->_ds, $this->_params['bind_dn'], $this->_params['bind_password']);
100:         } else {
101:             $error = !(@ldap_bind($this->_ds));
102:         }
103: 
104:         if ($error) {
105:             throw new Turba_Exception(sprintf(_("Bind failed: (%s) %s"), ldap_errno($this->_ds), ldap_error($this->_ds)));
106:         }
107:     }
108: 
109:     /**
110:      * Extends parent function to build composed fields needed for the dn
111:      * based on the contents of $this->map.
112:      *
113:      * @param array $hash  Hash using Turba keys.
114:      *
115:      * @return array  Translated version of $hash.
116:      */
117:     public function toDriverKeys(array $hash)
118:     {
119:         // First check for combined fields in the dn-fields and add them.
120:         if (is_array($this->_params['dn'])) {
121:             foreach ($this->_params['dn'] as $param) {
122:                 foreach ($this->map as $turbaname => $ldapname) {
123:                     if ((is_array($this->map[$turbaname])) &&
124:                         (isset($this->map[$turbaname]['attribute'])) &&
125:                         ($this->map[$turbaname]['attribute'] == $param)) {
126:                         $fieldarray = array();
127:                         foreach ($this->map[$turbaname]['fields'] as $mapfield) {
128:                             $fieldarray[] = isset($hash[$mapfield])
129:                                 ? $hash[$mapfield]
130:                                 : '';
131:                         }
132:                         $hash[$turbaname] = Turba::formatCompositeField($this->map[$turbaname]['format'], $fieldarray);
133:                     }
134:                 }
135:             }
136:         }
137: 
138:         // Now convert the turba-fieldnames to ldap-fieldnames
139:         return parent::toDriverKeys($hash);
140:     }
141: 
142:     /**
143:      * Searches the LDAP directory with the given criteria and returns
144:      * a filtered list of results. If no criteria are specified, all
145:      * records are returned.
146:      *
147:      * @param array $criteria    Array containing the search criteria.
148:      * @param array $fields      List of fields to return.
149:      * @param array $blobFields  Fields that contain binary data.
150:      *
151:      * @return array  Hash containing the search results.
152:      * @throws Turba_Exception
153:      */
154:     protected function _search(array $criteria, array $fields, array $blobFields = array(), $count_only = false)
155:     {
156:         $this->_connect();
157: 
158:         /* Build the LDAP filter. */
159:         $filter = '';
160:         if (count($criteria)) {
161:             foreach ($criteria as $key => $vals) {
162:                 if ($key == 'OR') {
163:                     $filter .= '(|' . $this->_buildSearchQuery($vals) . ')';
164:                 } elseif ($key == 'AND') {
165:                     $filter .= '(&' . $this->_buildSearchQuery($vals) . ')';
166:                 }
167:             }
168:         } elseif (!empty($this->_params['objectclass'])) {
169:             /* Filter on objectclass. */
170:             $filter = Horde_Ldap_Filter::build(array('objectclass' => $this->_params['objectclass']), 'or');
171:         }
172: 
173:         /* Add source-wide filters, which are _always_ AND-ed. */
174:         if (!empty($this->_params['filter'])) {
175:             $filter = '(&' . '(' . $this->_params['filter'] . ')' . $filter . ')';
176:         }
177: 
178:         /* Four11 (at least) doesn't seem to return 'cn' if you don't
179:          * ask for 'sn' as well. Add 'sn' implicitly. */
180:         $attr = $fields;
181:         if (!in_array('sn', $attr)) {
182:             $attr[] = 'sn';
183:         }
184: 
185:         /* Add a sizelimit, if specified. Default is 0, which means no
186:          * limit.  Note: You cannot override a server-side limit with
187:          * this. */
188:         $sizelimit = 0;
189:         if (!empty($this->_params['sizelimit'])) {
190:             $sizelimit = $this->_params['sizelimit'];
191:         }
192: 
193:         /* Log the query at a DEBUG log level. */
194:         Horde::logMessage(sprintf('LDAP query by Turba_Driver_ldap::_search(): user = %s, root = %s (%s); filter = "%s"; attributes = "%s"; deref = "%s"  ; sizelimit = %d',
195:                                   $GLOBALS['registry']->getAuth(), $this->_params['root'], $this->_params['server'], $filter, implode(', ', $attr), $this->_params['deref'], $sizelimit), 'DEBUG');
196: 
197:         /* Send the query to the LDAP server and fetch the matching
198:          * entries. */
199:         $func = ($this->_params['scope'] == 'one')
200:             ? 'ldap_list'
201:             : 'ldap_search';
202: 
203:         if (!($res = @$func($this->_ds, $this->_params['root'], $filter, $attr, 0, $sizelimit))) {
204:             throw new Turba_Exception(sprintf(_("Query failed: (%s) %s"), ldap_errno($this->_ds), ldap_error($this->_ds)));
205:         }
206: 
207:         return $count_only ? count($this->_getResults($fields, $res)) : $this->_getResults($fields, $res);
208:     }
209: 
210:     /**
211:      * Reads the LDAP directory for a given element and returns the results.
212:      *
213:      * @param string $key        The primary key field to use.
214:      * @param mixed $ids         The ids of the contacts to load.
215:      * @param string $owner      Only return contacts owned by this user.
216:      * @param array $fields      List of fields to return.
217:      * @param array $blobFields  Array of fields containing binary data.
218:      *
219:      * @return array  Hash containing the search results.
220:      * @throws Turba_Exception
221:      */
222:     protected function _read($key, $ids, $owner, array $fields,
223:                              array $blobFields = array())
224:     {
225:         /* Only DN. */
226:         if ($key != 'dn') {
227:             return array();
228:         }
229: 
230:         $this->_connect();
231: 
232:         if (empty($this->_params['objectclass'])) {
233:             $filter = null;
234:         } else {
235:             $filter = (string)Horde_Ldap_Filter::build(array('objectclass' => $this->_params['objectclass']), 'or');
236:         }
237: 
238:         /* Four11 (at least) doesn't seem to return 'cn' if you don't
239:          * ask for 'sn' as well. Add 'sn' implicitly. */
240:         $attr = $fields;
241:         if (!in_array('sn', $attr)) {
242:             $attr[] = 'sn';
243:         }
244: 
245:         /* Handle a request for multiple records. */
246:         if (is_array($ids) && !empty($ids)) {
247:             $results = array();
248:             foreach ($ids as $d) {
249:                 $res = @ldap_read($this->_ds, Horde_String::convertCharset($d, 'UTF-8', $this->_params['charset']), $filter, $attr);
250:                 if ($res) {
251:                     $results = array_merge($results, $this->_getResults($fields, $res));
252:                 } else {
253:                     throw new Turba_Exception(sprintf(_("Read failed: (%s) %s"), ldap_errno($this->_ds), ldap_error($this->_ds)));
254:                 }
255:             }
256: 
257:             return $results;
258:         }
259: 
260:         $res = @ldap_read($this->_ds, Horde_String::convertCharset($this->_params['root'], 'UTF-8', $this->_params['charset']), $filter, $attr);
261:         if (!$res) {
262:             throw new Turba_Exception(sprintf(_("Read failed: (%s) %s"), ldap_errno($this->_ds), ldap_error($this->_ds)));
263:         }
264: 
265:         return $this->_getResults($fields, $res);
266:     }
267: 
268:     /**
269:      * Adds the specified contact to the addressbook.
270:      *
271:      * @param array $attributes  The attribute values of the contact.
272:      * @param array $blob_fields TODO
273:      *
274:      * @throws Turba_Exception
275:      */
276:     protected function _add(array $attributes, array $blob_fields = array())
277:     {
278:         if (empty($attributes['dn'])) {
279:             throw new Turba_Exception('Tried to add an object with no dn: [' . serialize($attributes) . '].');
280:         }
281:         if (empty($this->_params['objectclass'])) {
282:             throw new Turba_Exception('Tried to add an object with no objectclass: [' . serialize($attributes) . '].');
283:         }
284: 
285:         $this->_connect();
286: 
287:         /* Take the DN out of the attributes array. */
288:         $dn = $attributes['dn'];
289:         unset($attributes['dn']);
290: 
291:         /* Put the objectClass into the attributes array. */
292:         if (!is_array($this->_params['objectclass'])) {
293:             $attributes['objectclass'] = $this->_params['objectclass'];
294:         } else {
295:             $i = 0;
296:             foreach ($this->_params['objectclass'] as $objectclass) {
297:                 $attributes['objectclass'][$i++] = $objectclass;
298:             }
299:         }
300: 
301:         /* Don't add empty attributes. */
302:         $attributes = array_filter($attributes, array($this, '_emptyAttributeFilter'));
303: 
304:         /* If a required attribute doesn't exist, add a dummy
305:          * value. */
306:         if (!empty($this->_params['checkrequired'])) {
307:             $required = $this->_checkRequiredAttributes($this->_params['objectclass']);
308: 
309:             foreach ($required as $k => $v) {
310:                 if (!isset($attributes[$v])) {
311:                     $attributes[$v] = $this->_params['checkrequired_string'];
312:                 }
313:             }
314:         }
315: 
316:         $this->_encodeAttributes($attributes);
317: 
318:         if (!@ldap_add($this->_ds, Horde_String::convertCharset($dn, 'UTF-8', $this->_params['charset']), $attributes)) {
319:             throw new Turba_Exception('Failed to add an object: [' . ldap_errno($this->_ds) . '] "' . ldap_error($this->_ds) . '" DN: ' . $dn . ' (attributes: [' . serialize($attributes) . '])');
320:         }
321:     }
322: 
323:     /**
324:      * TODO
325:      *
326:      * @return boolean  TODO
327:      */
328:     protected function _canAdd()
329:     {
330:         return true;
331:     }
332: 
333:     /**
334:      * Deletes the specified entry from the LDAP directory.
335:      *
336:      * @param string $object_key
337:      * @param string $object_id
338:      *
339:      * @throws Turba_Exception
340:      */
341:     protected function _delete($object_key, $object_id)
342:     {
343:         if ($object_key != 'dn') {
344:             throw new Turba_Exception(_("Invalid key specified."));
345:         }
346: 
347:         $this->_connect();
348: 
349:         if (!@ldap_delete($this->_ds, Horde_String::convertCharset($object_id, 'UTF-8', $this->_params['charset']))) {
350:             throw new Turba_Exception(sprintf(_("Delete failed: (%s) %s"), ldap_errno($this->_ds), ldap_error($this->_ds)));
351:         }
352:     }
353: 
354:     /**
355:      * Modifies the specified entry in the LDAP directory.
356:      *
357:      * @param Turba_Object $object  The object we wish to save.
358:      *
359:      * @return string  The object id, possibly updated.
360:      * @throw Turba_Exception
361:      */
362:     protected function _save(Turba_Object $object)
363:     {
364:         $this->_connect();
365: 
366:         list($object_key, $object_id) = each($this->toDriverKeys(array('__key' => $object->getValue('__key'))));
367:         $attributes = $this->toDriverKeys($object->getAttributes());
368: 
369:         /* Get the old entry so that we can access the old
370:          * values. These are needed so that we can delete any
371:          * attributes that have been removed by using ldap_mod_del. */
372:         if (empty($this->_params['objectclass'])) {
373:             $filter = null;
374:         } else {
375:             $filter = (string)Horde_Ldap_Filter::build(array('objectclass' => $this->_params['objectclass']), 'or');
376:         }
377:         $oldres = @ldap_read($this->_ds, Horde_String::convertCharset($object_id, 'UTF-8', $this->_params['charset']), $filter, array_merge(array_keys($attributes), array('objectclass')));
378:         $info = ldap_get_attributes($this->_ds, ldap_first_entry($this->_ds, $oldres));
379: 
380:         if ($this->_params['version'] == 3 &&
381:             Horde_String::lower(str_replace(array(',', '"'), array('\\2C', ''), $this->_makeKey($attributes))) !=
382:             Horde_String::lower(str_replace(',', '\\2C', $object_id))) {
383:             /* Need to rename the object. */
384:             $newrdn = $this->_makeRDN($attributes);
385:             if ($newrdn == '') {
386:                 throw new Turba_Exception(_("Missing DN in LDAP source configuration."));
387:             }
388: 
389:             if (ldap_rename($this->_ds, Horde_String::convertCharset($object_id, 'UTF-8', $this->_params['charset']),
390:                             Horde_String::convertCharset($newrdn, 'UTF-8', $this->_params['charset']), $this->_params['root'], true)) {
391:                 $object_id = $newrdn . ',' . $this->_params['root'];
392:             } else {
393:                 throw new Turba_Exception(sprintf(_("Failed to change name: (%s) %s; Old DN = %s, New DN = %s, Root = %s"), ldap_errno($this->_ds), ldap_error($this->_ds), $object_id, $newrdn, $this->_params['root']));
394:             }
395:         }
396: 
397:         /* Work only with lowercase keys. */
398:         $info = array_change_key_case($info, CASE_LOWER);
399:         $attributes = array_change_key_case($attributes, CASE_LOWER);
400: 
401:         foreach ($info as $key => $value) {
402:             $var = $info[$key];
403:             $oldval = null;
404: 
405:             /* Check to see if the old value and the new value are
406:              * different and that the new value is empty. If so then
407:              * we use ldap_mod_del to delete the attribute. */
408:             if (isset($attributes[$key]) &&
409:                 ($var[0] != $attributes[$key]) &&
410:                 $attributes[$key] == '') {
411: 
412:                 $oldval[$key] = $var[0];
413:                 if (!@ldap_mod_del($this->_ds, Horde_String::convertCharset($object_id, 'UTF-8', $this->_params['charset']), $oldval)) {
414:                     throw new Turba_Exception(sprintf(_("Modify failed: (%s) %s"), ldap_errno($this->_ds), ldap_error($this->_ds)));
415:                 }
416:                 unset($attributes[$key]);
417:             } elseif (isset($attributes[$key]) &&
418:                       $var[0] == $attributes[$key]) {
419:                 /* Drop unchanged elements from list of attributes to write. */
420:                 unset($attributes[$key]);
421:             }
422:         }
423: 
424:         unset($attributes[Horde_String::lower($object_key)]);
425:         $this->_encodeAttributes($attributes);
426:         $attributes = array_filter($attributes, array($this, '_emptyAttributeFilter'));
427: 
428:         /* Modify objectclasses only if they really changed. */
429:         $oldClasses = array_map(array('Horde_String', 'lower'), $info['objectclass']);
430:         array_shift($oldClasses);
431:         $attributes['objectclass'] = array_unique(array_map('strtolower', array_merge($info['objectclass'], $this->_params['objectclass'])));
432:         unset($attributes['objectclass']['count']);
433:         $attributes['objectclass'] = array_values($attributes['objectclass']);
434: 
435:         /* Do not handle object classes unless they have changed. */
436:         if ((!array_diff($oldClasses, $attributes['objectclass']))) {
437:             unset($attributes['objectclass']);
438:         }
439:         if (!@ldap_modify($this->_ds, Horde_String::convertCharset($object_id, 'UTF-8', $this->_params['charset']), $attributes)) {
440:             throw new Turba_Exception(sprintf(_("Modify failed: (%s) %s"), ldap_errno($this->_ds), ldap_error($this->_ds)));
441:         }
442: 
443:         return $object_id;
444:     }
445: 
446:     /**
447:      * Build a RDN based on a set of attributes and what attributes
448:      * make a RDN for the current source.
449:      *
450:      * @param array $attributes The attributes (in driver keys) of the
451:      *                          object being added.
452:      *
453:      * @return string  The RDN for the new object.
454:      */
455:     protected function _makeRDN(array $attributes)
456:     {
457:         if (!is_array($this->_params['dn'])) {
458:             return '';
459:         }
460: 
461:         $pairs = array();
462:         foreach ($this->_params['dn'] as $param) {
463:             if (isset($attributes[$param])) {
464:                 $pairs[] = array($param, $attributes[$param]);
465:             }
466:         }
467: 
468:         return Horde_Ldap::quoteDN($pairs);
469:     }
470: 
471:     /**
472:      * Build a DN based on a set of attributes and what attributes
473:      * make a DN for the current source.
474:      *
475:      * @param array $attributes The attributes (in driver keys) of the
476:      *                          object being added.
477:      *
478:      * @return string  The DN for the new object.
479:      */
480:     protected function _makeKey(array $attributes)
481:     {
482:         return $this->_makeRDN($attributes) . ',' . $this->_params['root'];
483:     }
484: 
485:     /**
486:      * Build a piece of a search query.
487:      *
488:      * @param array  $criteria  The array of criteria.
489:      *
490:      * @return string  An LDAP query fragment.
491:      */
492:     protected function _buildSearchQuery(array $criteria)
493:     {
494:         $clause = '';
495: 
496:         foreach ($criteria as $key => $vals) {
497:             if (!empty($vals['OR'])) {
498:                 $clause .= '(|' . $this->_buildSearchQuery($vals) . ')';
499:             } elseif (!empty($vals['AND'])) {
500:                 $clause .= '(&' . $this->_buildSearchQuery($vals) . ')';
501:             } else {
502:                 if (isset($vals['field'])) {
503:                     $rhs = Horde_String::convertCharset($vals['test'], 'UTF-8', $this->_params['charset']);
504:                     $clause .= Horde_Ldap::buildClause($vals['field'], $vals['op'], $rhs, array('begin' => !empty($vals['begin'])));
505:                 } else {
506:                     foreach ($vals as $test) {
507:                         if (!empty($test['OR'])) {
508:                             $clause .= '(|' . $this->_buildSearchQuery($test) . ')';
509:                         } elseif (!empty($test['AND'])) {
510:                             $clause .= '(&' . $this->_buildSearchQuery($test) . ')';
511:                         } else {
512:                             $rhs = Horde_String::convertCharset($test['test'], 'UTF-8', $this->_params['charset']);
513:                             $clause .= Horde_Ldap::buildClause($test['field'], $test['op'], $rhs, array('begin' => !empty($vals['begin'])));
514:                         }
515:                     }
516:                 }
517:             }
518:         }
519: 
520:         return $clause;
521:     }
522: 
523:     /**
524:      * Get some results from a result identifier and clean them up.
525:      *
526:      * @param array    $fields  List of fields to return.
527:      * @param resource $res     Result identifier.
528:      *
529:      * @return array  Hash containing the results.
530:      * @throws Turba_Exception
531:      */
532:     protected function _getResults(array $fields, $res)
533:     {
534:         $entries = @ldap_get_entries($this->_ds, $res);
535:         if ($entries === false) {
536:             throw new Turba_Exception(sprintf(_("Read failed: (%s) %s"), ldap_errno($this->_ds), ldap_error($this->_ds)));
537:         }
538: 
539:         /* Return only the requested fields (from $fields, above). */
540:         $results = array();
541:         for ($i = 0; $i < $entries['count']; ++$i) {
542:             $entry = $entries[$i];
543:             $result = array();
544: 
545:             foreach ($fields as $field) {
546:                 $field_l = Horde_String::lower($field);
547:                 if ($field == 'dn') {
548:                     $result[$field] = Horde_String::convertCharset($entry[$field_l], $this->_params['charset'], 'UTF-8');
549:                 } else {
550:                     $result[$field] = '';
551:                     if (!empty($entry[$field_l])) {
552:                         for ($j = 0; $j < $entry[$field_l]['count']; $j++) {
553:                             if (!empty($result[$field])) {
554:                                 $result[$field] .= $this->_params['multiple_entry_separator'];
555:                             }
556:                             $result[$field] .= Horde_String::convertCharset($entry[$field_l][$j], $this->_params['charset'], 'UTF-8');
557:                         }
558: 
559:                         /* If schema checking is enabled check the
560:                          * backend syntax. */
561:                         if (!empty($this->_params['checksyntax'])) {
562:                             $postal = $this->_isPostalAddress($field_l);
563:                         } else {
564:                             /* Otherwise rely on the attribute mapping
565:                              * in attributes.php. */
566:                             $attr = array_search($field_l, $this->map);
567:                             $postal = (!empty($attr) && !empty($GLOBALS['attributes'][$attr]) &&
568:                                        $GLOBALS['attributes'][$attr]['type'] == 'address');
569:                         }
570:                         if ($postal) {
571:                             $result[$field] = str_replace('$', "\r\n", $result[$field]);
572:                         }
573:                     }
574:                 }
575:             }
576: 
577:             $results[] = $result;
578:         }
579: 
580:         return $results;
581:     }
582: 
583:     /**
584:      * Remove empty attributes from attributes array.
585:      *
586:      * @param mixed $val  Value from attributes array.
587:      *
588:      * @return boolean  Boolean used by array_filter.
589:      */
590:     protected function _emptyAttributeFilter($var)
591:     {
592:         if (!is_array($var)) {
593:             return ($var != '');
594:         }
595: 
596:         if (!count($var)) {
597:             return false;
598:         }
599: 
600:         foreach ($var as $v) {
601:             if ($v == '') {
602:                 return false;
603:             }
604:         }
605: 
606:         return true;
607:     }
608: 
609:     /**
610:      * Format and encode attributes including postal addresses,
611:      * character set encoding, etc.
612:      *
613:      * @param array $attributes  The attributes array.
614:      */
615:     protected function _encodeAttributes(&$attributes)
616:     {
617:         foreach ($attributes as $key => $val) {
618:             /* If schema checking is enabled check the backend syntax. */
619:             if (!empty($this->_params['checksyntax'])) {
620:                 $postal = $this->_isPostalAddress($key);
621:             } else {
622:                 /* Otherwise rely on the attribute mapping in
623:                  * attributes.php. */
624:                 $attr = array_search($key, $this->map);
625:                 $postal = (!empty($attr) && !empty($val) && !empty($GLOBALS['attributes'][$attr]) &&
626:                            $GLOBALS['attributes'][$attr]['type'] == 'address');
627:             }
628:             if ($postal) {
629:                 /* Correctly store postal addresses. */
630:                 $val = str_replace(array("\r\n", "\r", "\n"), '$', $val);
631:             }
632: 
633:             if (!is_array($val)) {
634:                 $attributes[$key] = Horde_String::convertCharset($val, 'UTF-8', $this->_params['charset']);
635:             }
636:         }
637:     }
638: 
639:     /**
640:      * Returns a list of required attributes.
641:      *
642:      * @param array $objectclasses  List of objectclasses that should be
643:      *                              checked for required attributes.
644:      *
645:      * @return array  List of attribute names of the specified objectclasses
646:      *                that have been configured as being required.
647:      * @throws Turba_Exception
648:      */
649:     protected function _checkRequiredAttributes(array $objectclasses)
650:     {
651:         $ldap = new Horde_Ldap($this->_convertParameters($this->_params));
652:         $schema = $ldap->schema();
653: 
654:         $retval = array();
655:         foreach ($objectclasses as $oc) {
656:             if (Horde_String::lower($oc) == 'top') {
657:                 continue;
658:             }
659: 
660:             $required = $schema->must($oc, true);
661:             if (is_array($required)) {
662:                 foreach ($required as $v) {
663:                     if ($this->_isString($v)) {
664:                         $retval[] = Horde_String::lower($v);
665:                     }
666:                 }
667:             }
668:         }
669: 
670:         return $retval;
671:     }
672: 
673:     /**
674:      * Checks if an attribute refers to a string.
675:      *
676:      * @param string $attribute  An attribute name.
677:      *
678:      * @return boolean  True if the specified attribute refers to a string.
679:      */
680:     protected function _isString($attribute)
681:     {
682:         $syntax = $this->_getSyntax($attribute);
683: 
684:         /* Syntaxes we want to allow, i.e. no integers.
685:          * Syntaxes have the form:
686:          * 1.3.6.1.4.1.1466.115.121.1.$n{$y}
687:          * ... where $n is the integer used below and $y is a sizelimit. */
688:         $okSyntax = array(
689:             44 => 1, /* Printable string. */
690:             41 => 1, /* Postal address. */
691:             39 => 1, /* Other mailbox. */
692:             34 => 1, /* Name and optional UID. */
693:             26 => 1, /* IA5 string. */
694:             15 => 1, /* Directory string. */
695:         );
696: 
697:         return (preg_match('/^(.*)\.(\d+)\{\d+\}$/', $syntax, $matches) &&
698:                 ($matches[1] == "1.3.6.1.4.1.1466.115.121.1") &&
699:                 isset($okSyntax[$matches[2]]));
700:     }
701: 
702:     /**
703:      * Checks if an attribute refers to a Postal Address.
704:      *
705:      * @param string $attribute  An attribute name.
706:      *
707:      * @return boolean  True if the specified attribute refers to a Postal
708:      *                  Address.
709:      */
710:     protected function _isPostalAddress($attribute)
711:     {
712:         /* LDAP postal address syntax is
713:          * 1.3.6.1.4.1.1466.115.121.1.41 */
714:         return ($this->_getSyntax($attribute) == '1.3.6.1.4.1.1466.115.121.1.41');
715:     }
716: 
717:     /**
718:      * Returns the syntax of an attribute, if necessary recursively.
719:      *
720:      * @param string $att  Attribute name.
721:      *
722:      * @return string  Attribute syntax.
723:      * @throws Turba_Exception
724:      */
725:     protected function _getSyntax($att)
726:     {
727:         $ldap = new Horde_Ldap($this->_convertParameters($this->_params));
728:         $schema = $ldap->schema();
729: 
730:         if (!isset($this->_syntaxCache[$att])) {
731:             $attv = $schema->get('attribute', $att);
732:             $this->_syntaxCache[$att] = isset($attv['syntax'])
733:                 ? $attv['syntax']
734:                 : $this->_getSyntax($attv['sup'][0]);
735:         }
736: 
737:         return $this->_syntaxCache[$att];
738:     }
739: 
740:     /**
741:      * Converts Turba connection parameter so Horde_Ldap parameters.
742:      *
743:      * @param array $in  Turba parameters.
744:      *
745:      * @return array  Horde_Ldap parameters.
746:      */
747:     protected function _convertParameters(array $in)
748:     {
749:         $map = array(
750:             'server' => 'hostspec',
751:             'port' => 'port',
752:             'tls' => 'tls',
753:             'version' => 'version',
754:             'root' => 'basedn',
755:             'bind_dn' => 'binddn',
756:             'bind_password' => 'bindpw',
757:             // can both be specified in Turba but only one in Horde_Ldap.
758:             //'objectclass',
759:             //'filter' => 'filter',
760:             'scope' => 'scope',
761:             // charset is always utf-8
762:             //'charset',
763:             // Not yet implemented.
764:             //'deref',
765:             //'referrals',
766:             //'sizelimit',
767:             //'dn',
768:         );
769:         $out = array();
770:         foreach ($in as $key => $value) {
771:             if (isset($map[$key])) {
772:                 $out[$map[$key]] = $value;
773:             }
774:         }
775:         return $out;
776:     }
777: }
778: 
API documentation generated by ApiGen