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: