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 * $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-2017 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 */ 40class 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