Overview

Packages

  • Ldap

Classes

  • Horde_Ldap
  • Horde_Ldap_Entry
  • Horde_Ldap_Exception
  • Horde_Ldap_Filter
  • Horde_Ldap_Ldif
  • Horde_Ldap_RootDse
  • Horde_Ldap_Schema
  • Horde_Ldap_Search
  • Horde_Ldap_Util
  • Overview
  • Package
  • Class
  • Tree
  1: <?php
  2: /**
  3:  * LDIF capabilities for Horde_Ldap.
  4:  *
  5:  * This class provides a means to convert between Horde_Ldap_Entry objects and
  6:  * LDAP entries represented in LDIF format files. Reading and writing are
  7:  * supported and manipulating of single entries or lists of entries.
  8:  *
  9:  * Usage example:
 10:  * <code>
 11:  * // Read and parse an LDIF file into Horde_Ldap_Entry objects
 12:  * // and print out the DNs. Store the entries for later use.
 13:  * $entries = array();
 14:  * $ldif = new Horde_Ldap_Ldif('test.ldif', 'r', $options);
 15:  * do {
 16:  *     $entry = $ldif->readEntry();
 17:  *     $dn    = $entry->dn();
 18:  *     echo " done building entry: $dn\n";
 19:  *     array_push($entries, $entry);
 20:  * } while (!$ldif->eof());
 21:  * $ldif->done();
 22:  *
 23:  * // Write those entries to another file
 24:  * $ldif = new Horde_Ldap_Ldif('test.out.ldif', 'w', $options);
 25:  * $ldif->writeEntry($entries);
 26:  * $ldif->done();
 27:  * </code>
 28:  *
 29:  * Copyright 2009 Benedikt Hallinger
 30:  * Copyright 2010-2012 Horde LLC (http://www.horde.org/)
 31:  *
 32:  * @category  Horde
 33:  * @package   Ldap
 34:  * @author    Benedikt Hallinger <beni@php.net>
 35:  * @author    Jan Schneider <jan@horde.org>
 36:  * @license   http://www.gnu.org/licenses/lgpl-3.0.html LGPL-3.0
 37:  * @see       http://www.ietf.org/rfc/rfc2849.txt
 38:  * @todo      LDAPv3 controls are not implemented yet
 39:  */
 40: class Horde_Ldap_Ldif
 41: {
 42:     /**
 43:      * Options.
 44:      *
 45:      * @var array
 46:      */
 47:     protected $_options = array('encode'    => 'base64',
 48:                                 'change'    => false,
 49:                                 'lowercase' => false,
 50:                                 'sort'      => false,
 51:                                 'version'   => null,
 52:                                 'wrap'      => 78,
 53:                                 'raw'       => '');
 54: 
 55:     /**
 56:      * File handle for read/write.
 57:      *
 58:      * @var resource
 59:      */
 60:     protected $_fh;
 61: 
 62:     /**
 63:      * Whether we opened the file handle ourselves.
 64:      *
 65:      * @var boolean
 66:      */
 67:     protected $_fhOpened = false;
 68: 
 69:     /**
 70:      * Line counter for input file handle.
 71:      *
 72:      * @var integer
 73:      */
 74:     protected $_inputLine = 0;
 75: 
 76:     /**
 77:      * Counter for processed entries.
 78:      *
 79:      * @var integer
 80:      */
 81:     protected $_entrynum = 0;
 82: 
 83:     /**
 84:      * Mode we are working in.
 85:      *
 86:      * Either 'r', 'a' or 'w'
 87:      *
 88:      * @var string
 89:      */
 90:     protected $_mode;
 91: 
 92:     /**
 93:      * Whether the LDIF version string was already written.
 94:      *
 95:      * @var boolean
 96:      */
 97:     protected $_versionWritten = false;
 98: 
 99:     /**
100:      * Cache for lines that have built the current entry.
101:      *
102:      * @var array
103:      */
104:     protected $_linesCur = array();
105: 
106:     /**
107:      * Cache for lines that will build the next entry.
108:      *
109:      * @var array
110:      */
111:     protected $_linesNext = array();
112: 
113:     /**
114:      * Constructor.
115:      *
116:      * Opens an LDIF file for reading or writing.
117:      *
118:      * $options is an associative array and may contain:
119:      * - 'encode' (string): Some DN values in LDIF cannot be written verbatim
120:      *                      and have to be encoded in some way. Possible
121:      *                      values:
122:      *                      - 'none':      No encoding.
123:      *                      - 'canonical': See {@link
124:      *                                     Horde_Ldap_Util::canonicalDN()}.
125:      *                      - 'base64':    Use base64 (default).
126:      * - 'change' (boolean): Write entry changes to the LDIF file instead of
127:      *                       the entries itself. I.e. write LDAP operations
128:      *                       acting on the entries to the file instead of the
129:      *                       entries contents.  This writes the changes usually
130:      *                       carried out by an update() to the LDIF
131:      *                       file. Defaults to false.
132:      * - 'lowercase' (boolean): Convert attribute names to lowercase when
133:      *                          writing. Defaults to false.
134:      * - 'sort' (boolean): Sort attribute names when writing entries according
135:      *                     to the rule: objectclass first then all other
136:      *                     attributes alphabetically sorted by attribute
137:      *                     name. Defaults to false.
138:      * - 'version' (integer): Set the LDIF version to write to the resulting
139:      *                        LDIF file. According to RFC 2849 currently the
140:      *                        only legal value for this option is 1. When this
141:      *                        option is set Horde_Ldap_Ldif tries to adhere
142:      *                        more strictly to the LDIF specification in
143:      *                        RFC2489 in a few places. The default is null
144:      *                        meaning no version information is written to the
145:      *                        LDIF file.
146:      * - 'wrap' (integer): Number of columns where output line wrapping shall
147:      *                     occur.  Default is 78. Setting it to 40 or lower
148:      *                     inhibits wrapping.
149:      * - 'raw' (string): Regular expression to denote the names of attributes
150:      *                   that are to be considered binary in search results if
151:      *                   writing entries.  Example: 'raw' =>
152:      *                   '/(?i:^jpegPhoto|;binary)/i'
153:      *
154:      * @param string|ressource $file    Filename or file handle.
155:      * @param string           $mode    Mode to open the file, either 'r', 'w'
156:      *                                  or 'a'.
157:      * @param array            $options Options like described above.
158:      *
159:      * @throws Horde_Ldap_Exception
160:      */
161:     public function __construct($file, $mode = 'r', $options = array())
162:     {
163:         // Parse options.
164:         foreach ($options as $option => $value) {
165:             if (!array_key_exists($option, $this->_options)) {
166:                 throw new Horde_Ldap_Exception('Option ' . $option . ' not known');
167:             }
168:             $this->_options[$option] = Horde_String::lower($value);
169:         }
170: 
171:         // Set version.
172:         $this->version($this->_options['version']);
173: 
174:         // Setup file mode.
175:         if (!preg_match('/^[rwa]$/', $mode)) {
176:             throw new Horde_Ldap_Exception('File mode ' . $mode . ' not supported');
177:         }
178:         $this->_mode = $mode;
179: 
180:         // Setup file handle.
181:         if (is_resource($file)) {
182:             // TODO: checks on mode possible?
183:             $this->_fh = $file;
184:             return;
185:         }
186: 
187:         switch ($mode) {
188:         case 'r':
189:             if (!file_exists($file)) {
190:                 throw new Horde_Ldap_Exception('Unable to open ' . $file . ' for reading: file not found');
191:             }
192:             if (!is_readable($file)) {
193:                 throw new Horde_Ldap_Exception('Unable to open ' . $file . ' for reading: permission denied');
194:             }
195:             break;
196: 
197:         case 'w':
198:         case 'a':
199:             if (file_exists($file)) {
200:                 if (!is_writable($file)) {
201:                     throw new Horde_Ldap_Exception('Unable to open ' . $file . ' for writing: permission denied');
202:                 }
203:             } else {
204:                 if (!@touch($file)) {
205:                     throw new Horde_Ldap_Exception('Unable to create ' . $file . ' for writing: permission denied');
206:                 }
207:             }
208:             break;
209:         }
210: 
211:         $this->_fh = @fopen($file, $this->_mode);
212:         if (!$this->_fh) {
213:             throw new Horde_Ldap_Exception('Could not open file ' . $file);
214:         }
215: 
216:         $this->_fhOpened = true;
217:     }
218: 
219:     /**
220:      * Reads one entry from the file and return it as a Horde_Ldap_Entry
221:      * object.
222:      *
223:      * @return Horde_Ldap_Entry
224:      * @throws Horde_Ldap_Exception
225:      */
226:     public function readEntry()
227:     {
228:         // Read fresh lines, set them as current lines and create the entry.
229:         $attrs = $this->nextLines(true);
230:         if (count($attrs)) {
231:             $this->_linesCur = $attrs;
232:         }
233:         return $this->currentEntry();
234:     }
235: 
236:     /**
237:      * Returns true when the end of the file is reached.
238:      *
239:      * @return boolean
240:      */
241:     public function eof()
242:     {
243:         return feof($this->_fh);
244:     }
245: 
246:     /**
247:      * Writes the entry or entries to the LDIF file.
248:      *
249:      * If you want to build an LDIF file containing several entries AND you
250:      * want to call writeEntry() several times, you must open the file handle
251:      * in append mode ('a'), otherwise you will always get the last entry only.
252:      *
253:      * @todo Implement operations on whole entries (adding a whole entry).
254:      *
255:      * @param Horde_Ldap_Entry|array $entries Entry or array of entries.
256:      *
257:      * @throws Horde_Ldap_Exception
258:      */
259:     public function writeEntry($entries)
260:     {
261:         if (!is_array($entries)) {
262:             $entries = array($entries);
263:         }
264: 
265:         foreach ($entries as $entry) {
266:             $this->_entrynum++;
267:             if (!($entry instanceof Horde_Ldap_Entry)) {
268:                 throw new Horde_Ldap_Exception('Entry ' . $this->_entrynum . ' is not an Horde_Ldap_Entry object');
269:             }
270: 
271:             if ($this->_options['change']) {
272:                 $this->_changeEntry($entry);
273:             } else {
274:                 $this->_writeEntry($entry);
275:             }
276:         }
277:     }
278: 
279:     /**
280:      * Writes an LDIF file that describes an entry change.
281:      *
282:      * @param Horde_Ldap_Entry $entry
283:      *
284:      * @throws Horde_Ldap_Exception
285:      */
286:     protected function _changeEntry($entry)
287:     {
288:         // Fetch change information from entry.
289:         $entry_attrs_changes = $entry->getChanges();
290:         $num_of_changes = count($entry_attrs_changes['add'])
291:                         + count($entry_attrs_changes['replace'])
292:                         + count($entry_attrs_changes['delete']);
293: 
294:         $is_changed = $num_of_changes > 0 || $entry->willBeDeleted() || $entry->willBeMoved();
295: 
296:         // Write version if not done yet, also write DN of entry.
297:         if ($is_changed) {
298:             if (!$this->_versionWritten) {
299:                 $this->writeVersion();
300:             }
301:             $this->_writeDN($entry->currentDN());
302:         }
303: 
304:         // Process changes.
305:         // TODO: consider DN add!
306:         if ($entry->willBeDeleted()) {
307:             $this->_writeLine('changetype: delete');
308:         } elseif ($entry->willBeMoved()) {
309:             $this->_writeLine('changetype: modrdn');
310:             $olddn     = Horde_Ldap_Util::explodeDN($entry->currentDN(), array('casefold' => 'none'));
311:             array_shift($olddn);
312:             $oldparent = implode(',', $olddn);
313:             $newdn     = Horde_Ldap_Util::explodeDN($entry->dn(), array('casefold' => 'none'));
314:             $rdn       = array_shift($newdn);
315:             $parent    = implode(',', $newdn);
316:             $this->_writeLine('newrdn: ' . $rdn);
317:             $this->_writeLine('deleteoldrdn: 1');
318:             if ($parent !== $oldparent) {
319:                 $this->_writeLine('newsuperior: ' . $parent);
320:             }
321:             // TODO: What if the entry has attribute changes as well?
322:             //       I think we should check for that and make a dummy
323:             //       entry with the changes that is written to the LDIF file.
324:         } elseif ($num_of_changes > 0) {
325:             // Write attribute change data.
326:             $this->_writeLine('changetype: modify');
327:             foreach ($entry_attrs_changes as $changetype => $entry_attrs) {
328:                 foreach ($entry_attrs as $attr_name => $attr_values) {
329:                     $this->_writeLine("$changetype: $attr_name");
330:                     if ($attr_values !== null) {
331:                         $this->_writeAttribute($attr_name, $attr_values, $changetype);
332:                     }
333:                     $this->_writeLine('-');
334:                 }
335:             }
336:         }
337: 
338:         // Finish this entry's data if we had changes.
339:         if ($is_changed) {
340:             $this->_finishEntry();
341:         }
342:     }
343: 
344:     /**
345:      * Writes an LDIF file that describes an entry.
346:      *
347:      * @param Horde_Ldap_Entry $entry
348:      *
349:      * @throws Horde_Ldap_Exception
350:      */
351:     protected function _writeEntry($entry)
352:     {
353:         // Fetch attributes for further processing.
354:         $entry_attrs = $entry->getValues();
355: 
356:         // Sort and put objectclass attributes to first position.
357:         if ($this->_options['sort']) {
358:             ksort($entry_attrs);
359:             if (isset($entry_attrs['objectclass'])) {
360:                 $oc = $entry_attrs['objectclass'];
361:                 unset($entry_attrs['objectclass']);
362:                 $entry_attrs = array_merge(array('objectclass' => $oc), $entry_attrs);
363:             }
364:         }
365: 
366:         // Write data.
367:         if (!$this->_versionWritten) {
368:             $this->writeVersion();
369:         }
370:         $this->_writeDN($entry->dn());
371:         foreach ($entry_attrs as $attr_name => $attr_values) {
372:             $this->_writeAttribute($attr_name, $attr_values);
373:         }
374:         $this->_finishEntry();
375:     }
376: 
377:     /**
378:      * Writes the version to LDIF.
379:      *
380:      * If the object's version is defined, this method allows to explicitely
381:      * write the version before an entry is written.
382:      *
383:      * If not called explicitely, it gets called automatically when writing the
384:      * first entry.
385:      *
386:      * @throws Horde_Ldap_Exception
387:      */
388:     public function writeVersion()
389:     {
390:         if (!is_null($this->version())) {
391:             $this->_writeLine('version: ' . $this->version(), 'Unable to write version');
392:         }
393:         $this->_versionWritten = true;
394:     }
395: 
396:     /**
397:      * Returns or sets the LDIF version.
398:      *
399:      * If called with an argument it sets the LDIF version. According to RFC
400:      * 2849 currently the only legal value for the version is 1.
401:      *
402:      * @param integer $version LDIF version to set.
403:      *
404:      * @return integer The current or new version.
405:      * @throws Horde_Ldap_Exception
406:      */
407:     public function version($version = null)
408:     {
409:         if ($version !== null) {
410:             if ($version != 1) {
411:                 throw new Horde_Ldap_Exception('Illegal LDIF version set');
412:             }
413:             $this->_options['version'] = $version;
414:         }
415:         return $this->_options['version'];
416:     }
417: 
418:     /**
419:      * Returns the file handle the Horde_Ldap_Ldif object reads from or writes
420:      * to.
421:      *
422:      * You can, for example, use this to fetch the content of the LDIF file
423:      * manually.
424:      *
425:      * @return resource
426:      * @throws Horde_Ldap_Exception
427:      */
428:     public function handle()
429:     {
430:         if (!is_resource($this->_fh)) {
431:             throw new Horde_Ldap_Exception('Invalid file resource');
432:         }
433:         return $this->_fh;
434:     }
435: 
436:     /**
437:      * Cleans up.
438:      *
439:      * This method signals that the LDIF object is no longer needed. You can
440:      * use this to free up some memory and close the file handle. The file
441:      * handle is only closed, if it was opened from Horde_Ldap_Ldif.
442:      *
443:      * @throws Horde_Ldap_Exception
444:      */
445:     public function done()
446:     {
447:         // Close file handle if we opened it.
448:         if ($this->_fhOpened) {
449:             fclose($this->handle());
450:         }
451: 
452:         // Free variables.
453:         foreach (array_keys(get_object_vars($this)) as $name) {
454:             unset($this->$name);
455:         }
456:     }
457: 
458:     /**
459:      * Returns the current Horde_Ldap_Entry object.
460:      *
461:      * @return Horde_Ldap_Entry
462:      * @throws Horde_Ldap_Exception
463:      */
464:     public function currentEntry()
465:     {
466:         return $this->parseLines($this->currentLines());
467:     }
468: 
469:     /**
470:      * Parse LDIF lines of one entry into an Horde_Ldap_Entry object.
471:      *
472:      * @todo what about file inclusions and urls?
473:      *       "jpegphoto:< file:///usr/local/directory/photos/fiona.jpg"
474:      *
475:      * @param array $lines LDIF lines for one entry.
476:      *
477:      * @return Horde_Ldap_Entry Horde_Ldap_Entry object for those lines.
478:      * @throws Horde_Ldap_Exception
479:      */
480:     public function parseLines($lines)
481:     {
482:         // Parse lines into an array of attributes and build the entry.
483:         $attributes = array();
484:         $dn = false;
485:         foreach ($lines as $line) {
486:             if (!preg_match('/^(\w+)(:|::|:<)\s(.+)$/', $line, $matches)) {
487:                 // Line not in "attr: value" format -> ignore.  Maybe we should
488:                 // rise an error here, but this should be covered by
489:                 // nextLines() already. A problem arises, if users try to feed
490:                 // data of several entries to this method - the resulting entry
491:                 // will get wrong attributes. However, this is already
492:                 // mentioned in the method documentation above.
493:                 continue;
494:             }
495: 
496:             $attr  = $matches[1];
497:             $delim = $matches[2];
498:             $data  = $matches[3];
499: 
500:             switch ($delim) {
501:             case ':':
502:                 // Normal data.
503:                 $attributes[$attr][] = $data;
504:                 break;
505:             case '::':
506:                 // Base64 data.
507:                 $attributes[$attr][] = base64_decode($data);
508:                 break;
509:             case ':<':
510:                 // File inclusion
511:                 // TODO: Is this the job of the LDAP-client or the server?
512:                 throw new Horde_Ldap_Exception('File inclusions are currently not supported');
513:             default:
514:                 throw new Horde_Ldap_Exception('Parsing error: invalid syntax at parsing entry line: ' . $line);
515:             }
516: 
517:             if (Horde_String::lower($attr) == 'dn') {
518:                 // DN line detected. Save possibly decoded DN.
519:                 $dn = $attributes[$attr][0];
520:                 // Remove wrongly added "dn: " attribute.
521:                 unset($attributes[$attr]);
522:             }
523:         }
524: 
525:         if (!$dn) {
526:             throw new Horde_Ldap_Exception('Parsing error: unable to detect DN for entry');
527:         }
528: 
529:         return Horde_Ldap_Entry::createFresh($dn, $attributes);
530:     }
531: 
532:     /**
533:      * Returns the lines that generated the current Horde_Ldap_Entry object.
534:      *
535:      * Returns an empty array if no lines have been read so far.
536:      *
537:      * @return array Array of lines.
538:      */
539:     public function currentLines()
540:     {
541:         return $this->_linesCur;
542:     }
543: 
544:     /**
545:      * Returns the lines that will generate the next Horde_Ldap_Entry object.
546:      *
547:      * If you set $force to true you can iterate over the lines that build up
548:      * entries manually. Otherwise, iterating is done using {@link
549:      * readEntry()}. $force will move the file pointer forward, thus returning
550:      * the next entry lines.
551:      *
552:      * Wrapped lines will be unwrapped. Comments are stripped.
553:      *
554:      * @param boolean $force Set this to true if you want to iterate over the
555:      *                       lines manually
556:      *
557:      * @return array
558:      * @throws Horde_Ldap_Exception
559:      */
560:     public function nextLines($force = false)
561:     {
562:         // If we already have those lines, just return them, otherwise read.
563:         if (count($this->_linesNext) == 0 || $force) {
564:             // Empty in case something was left (if used $force).
565:             $this->_linesNext = array();
566:             $entry_done       = false;
567:             $fh               = $this->handle();
568:             // Are we in an comment? For wrapping purposes.
569:             $commentmode      = false;
570:             // How many lines with data we have read?
571:             $datalines_read   = 0;
572: 
573:             while (!$entry_done && !$this->eof()) {
574:                 $this->_inputLine++;
575:                 // Read line. Remove line endings, we want only data; this is
576:                 // okay since ending spaces should be encoded.
577:                 $data = rtrim(fgets($fh));
578:                 if ($data === false) {
579:                     // Error only, if EOF not reached after fgets() call.
580:                     if (!$this->eof()) {
581:                         throw new Horde_Ldap_Exception('Error reading from file at input line ' . $this->_inputLine);
582:                     }
583:                     break;
584:                 }
585: 
586:                 if (count($this->_linesNext) > 0 && preg_match('/^$/', $data)) {
587:                     // Entry is finished if we have an empty line after we had
588:                     // data.
589:                     $entry_done = true;
590: 
591:                     // Look ahead if the next EOF is nearby. Comments and empty
592:                     // lines at the file end may cause problems otherwise.
593:                     $current_pos = ftell($fh);
594:                     $data        = fgets($fh);
595:                     while (!feof($fh)) {
596:                         if (preg_match('/^\s*$/', $data) ||
597:                             preg_match('/^#/', $data)) {
598:                             // Only empty lines or comments, continue to seek.
599:                             // TODO: Known bug: Wrappings for comments are okay
600:                             //       but are treaten as error, since we do not
601:                             //       honor comment mode here.  This should be a
602:                             //       very theoretically case, however I am
603:                             //       willing to fix this if really necessary.
604:                             $this->_inputLine++;
605:                             $current_pos = ftell($fh);
606:                             $data        = fgets($fh);
607:                         } else {
608:                             // Data found if non emtpy line and not a comment!!
609:                             // Rewind to position prior last read and stop
610:                             // lookahead.
611:                             fseek($fh, $current_pos);
612:                             break;
613:                         }
614:                     }
615:                     // Now we have either the file pointer at the beginning of
616:                     // a new data position or at the end of file causing feof()
617:                     // to return true.
618:                     continue;
619:                 }
620: 
621:                 // Build lines.
622:                 if (preg_match('/^version:\s(.+)$/', $data, $match)) {
623:                     // Version statement, set version.
624:                     $this->version($match[1]);
625:                 } elseif (preg_match('/^\w+::?\s.+$/', $data)) {
626:                     // Normal attribute: add line.
627:                     $commentmode        = false;
628:                     $this->_linesNext[] = trim($data);
629:                     $datalines_read++;
630:                 } elseif (preg_match('/^\s(.+)$/', $data, $matches)) {
631:                     // Wrapped data: unwrap if not in comment mode.
632:                     if (!$commentmode) {
633:                         if ($datalines_read == 0) {
634:                             // First line of entry: wrapped data is illegal.
635:                             throw new Horde_Ldap_Exception('Illegal wrapping at input line ' . $this->_inputLine);
636:                         }
637:                         $this->_linesNext[] = array_pop($this->_linesNext) . trim($matches[1]);
638:                         $datalines_read++;
639:                     }
640:                 } elseif (preg_match('/^#/', $data)) {
641:                     // LDIF comments.
642:                     $commentmode = true;
643:                 } elseif (preg_match('/^\s*$/', $data)) {
644:                     // Empty line but we had no data for this entry, so just
645:                     // ignore this line.
646:                     $commentmode = false;
647:                 } else {
648:                     throw new Horde_Ldap_Exception('Invalid syntax at input line ' . $this->_inputLine);
649:                 }
650:             }
651:         }
652: 
653:         return $this->_linesNext;
654:     }
655: 
656:     /**
657:      * Converts an attribute and value to LDIF string representation.
658:      *
659:      * It honors correct encoding of values according to RFC 2849. Line
660:      * wrapping will occur at the configured maximum but only if the value is
661:      * greater than 40 chars.
662:      *
663:      * @param string $attr_name  Name of the attribute.
664:      * @param string $attr_value Value of the attribute.
665:      *
666:      * @return string LDIF string for that attribute and value.
667:      */
668:     protected function _convertAttribute($attr_name, $attr_value)
669:     {
670:         // Handle empty attribute or process.
671:         if (!strlen($attr_value)) {
672:             return $attr_name.':  ';
673:         }
674: 
675:         // If converting is needed, do it.
676:         // Either we have some special chars or a matching "raw" regex
677:         if ($this->_isBinary($attr_value) ||
678:             ($this->_options['raw'] &&
679:              preg_match($this->_options['raw'], $attr_name))) {
680:             $attr_name .= ':';
681:             $attr_value = base64_encode($attr_value);
682:         }
683: 
684:         // Lowercase attribute names if requested.
685:         if ($this->_options['lowercase']) {
686:             $attr_name = Horde_String::lower($attr_name);
687:         }
688: 
689:         // Handle line wrapping.
690:         if ($this->_options['wrap'] > 40 &&
691:             strlen($attr_value) > $this->_options['wrap']) {
692:             $attr_value = wordwrap($attr_value, $this->_options['wrap'], PHP_EOL . ' ', true);
693:         }
694: 
695:         return $attr_name . ': ' . $attr_value;
696:     }
697: 
698:     /**
699:      * Converts an entry's DN to LDIF string representation.
700:      *
701:      * It honors correct encoding of values according to RFC 2849.
702:      *
703:      * @todo I am not sure, if the UTF8 stuff is correctly handled right now
704:      *
705:      * @param string $dn UTF8 encoded DN.
706:      *
707:      * @return string LDIF string for that DN.
708:      */
709:     protected function _convertDN($dn)
710:     {
711:         // If converting is needed, do it.
712:         return $this->_isBinary($dn)
713:             ? 'dn:: ' . base64_encode($dn)
714:             : 'dn: ' . $dn;
715:     }
716: 
717:     /**
718:      * Returns whether some data is considered binary and must be
719:      * base64-encoded.
720:      *
721:      * @param string $value  Some data.
722:      *
723:      * @return boolean  True if the data should be encoded.
724:      */
725:     protected function _isBinary($value)
726:     {
727:         $binary = false;
728: 
729:         // ASCII-chars that are NOT safe for the start and for being inside the
730:         // value. These are the integer values of those chars.
731:         $unsafe_init = array(0, 10, 13, 32, 58, 60);
732:         $unsafe      = array(0, 10, 13);
733: 
734:         // Test for illegal init char.
735:         $init_ord = ord(substr($value, 0, 1));
736:         if ($init_ord > 127 || in_array($init_ord, $unsafe_init)) {
737:             $binary = true;
738:         }
739: 
740:         // Test for illegal content char.
741:         for ($i = 0, $len = strlen($value); $i < $len; $i++) {
742:             $char_ord = ord(substr($value, $i, 1));
743:             if ($char_ord >= 127 || in_array($char_ord, $unsafe)) {
744:                 $binary = true;
745:             }
746:         }
747: 
748:         // Test for ending space
749:         if (substr($value, -1) == ' ') {
750:             $binary = true;
751:         }
752: 
753:         return $binary;
754:     }
755: 
756:     /**
757:      * Writes an attribute to the file handle.
758:      *
759:      * @param string       $attr_name   Name of the attribute.
760:      * @param string|array $attr_values Single attribute value or array with
761:      *                                  attribute values.
762:      *
763:      * @throws Horde_Ldap_Exception
764:      */
765:     protected function _writeAttribute($attr_name, $attr_values)
766:     {
767:         // Write out attribute content.
768:         if (!is_array($attr_values)) {
769:             $attr_values = array($attr_values);
770:         }
771:         foreach ($attr_values as $attr_val) {
772:             $line = $this->_convertAttribute($attr_name, $attr_val);
773:             $this->_writeLine($line, 'Unable to write attribute ' . $attr_name . ' of entry ' . $this->_entrynum);
774:         }
775:     }
776: 
777:     /**
778:      * Writes a DN to the file handle.
779:      *
780:      * @param string $dn DN to write.
781:      *
782:      * @throws Horde_Ldap_Exception
783:      */
784:     protected function _writeDN($dn)
785:     {
786:         // Prepare DN.
787:         if ($this->_options['encode'] == 'base64') {
788:             $dn = $this->_convertDN($dn);
789:         } elseif ($this->_options['encode'] == 'canonical') {
790:             $dn = Horde_Ldap_Util::canonicalDN($dn, array('casefold' => 'none'));
791:         }
792:         $this->_writeLine($dn, 'Unable to write DN of entry ' . $this->_entrynum);
793:     }
794: 
795:     /**
796:      * Finishes an LDIF entry.
797:      *
798:      * @throws Horde_Ldap_Exception
799:      */
800:     protected function _finishEntry()
801:     {
802:         $this->_writeLine('', 'Unable to close entry ' . $this->_entrynum);
803:     }
804: 
805:     /**
806:      * Writes an arbitary line to the file handle.
807:      *
808:      * @param string $line  Content to write.
809:      * @param string $error If error occurs, throw this exception message.
810:      *
811:      * @throws Horde_Ldap_Exception
812:      */
813:     protected function _writeLine($line, $error = 'Unable to write to file handle')
814:     {
815:         $line .= PHP_EOL;
816:         if (is_resource($this->handle()) &&
817:             fwrite($this->handle(), $line, strlen($line)) === false) {
818:             throw new Horde_Ldap_Exception($error);
819:         }
820:     }
821: }
822: 
API documentation generated by ApiGen