822 lines
28 KiB
PHP
822 lines
28 KiB
PHP
<?php
|
|
/**
|
|
* LDIF capabilities for Horde_Ldap.
|
|
*
|
|
* This class provides a means to convert between Horde_Ldap_Entry objects and
|
|
* LDAP entries represented in LDIF format files. Reading and writing are
|
|
* supported and manipulating of single entries or lists of entries.
|
|
*
|
|
* Usage example:
|
|
* <code>
|
|
* // Read and parse an LDIF file into Horde_Ldap_Entry objects
|
|
* // and print out the DNs. Store the entries for later use.
|
|
* $entries = array();
|
|
* $ldif = new Horde_Ldap_Ldif('test.ldif', 'r', $options);
|
|
* do {
|
|
* $entry = $ldif->readEntry();
|
|
* $dn = $entry->dn();
|
|
* echo " done building entry: $dn\n";
|
|
* $entries[] = $entry;
|
|
* } while (!$ldif->eof());
|
|
* $ldif->done();
|
|
*
|
|
* // Write those entries to another file
|
|
* $ldif = new Horde_Ldap_Ldif('test.out.ldif', 'w', $options);
|
|
* $ldif->writeEntry($entries);
|
|
* $ldif->done();
|
|
* </code>
|
|
*
|
|
* Copyright 2009 Benedikt Hallinger
|
|
* Copyright 2010-2017 Horde LLC (http://www.horde.org/)
|
|
*
|
|
* @category Horde
|
|
* @package Ldap
|
|
* @author Benedikt Hallinger <beni@php.net>
|
|
* @author Jan Schneider <jan@horde.org>
|
|
* @license http://www.gnu.org/licenses/lgpl-3.0.html LGPL-3.0
|
|
* @see http://www.ietf.org/rfc/rfc2849.txt
|
|
* @todo LDAPv3 controls are not implemented yet
|
|
*/
|
|
class Horde_Ldap_Ldif
|
|
{
|
|
/**
|
|
* Options.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_options = array('encode' => 'base64',
|
|
'change' => false,
|
|
'lowercase' => false,
|
|
'sort' => false,
|
|
'version' => null,
|
|
'wrap' => 78,
|
|
'raw' => '');
|
|
|
|
/**
|
|
* File handle for read/write.
|
|
*
|
|
* @var resource
|
|
*/
|
|
protected $_fh;
|
|
|
|
/**
|
|
* Whether we opened the file handle ourselves.
|
|
*
|
|
* @var boolean
|
|
*/
|
|
protected $_fhOpened = false;
|
|
|
|
/**
|
|
* Line counter for input file handle.
|
|
*
|
|
* @var integer
|
|
*/
|
|
protected $_inputLine = 0;
|
|
|
|
/**
|
|
* Counter for processed entries.
|
|
*
|
|
* @var integer
|
|
*/
|
|
protected $_entrynum = 0;
|
|
|
|
/**
|
|
* Mode we are working in.
|
|
*
|
|
* Either 'r', 'a' or 'w'
|
|
*
|
|
* @var string
|
|
*/
|
|
protected $_mode;
|
|
|
|
/**
|
|
* Whether the LDIF version string was already written.
|
|
*
|
|
* @var boolean
|
|
*/
|
|
protected $_versionWritten = false;
|
|
|
|
/**
|
|
* Cache for lines that have built the current entry.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_linesCur = array();
|
|
|
|
/**
|
|
* Cache for lines that will build the next entry.
|
|
*
|
|
* @var array
|
|
*/
|
|
protected $_linesNext = array();
|
|
|
|
/**
|
|
* Constructor.
|
|
*
|
|
* Opens an LDIF file for reading or writing.
|
|
*
|
|
* $options is an associative array and may contain:
|
|
* - 'encode' (string): Some DN values in LDIF cannot be written verbatim
|
|
* and have to be encoded in some way. Possible
|
|
* values:
|
|
* - 'none': No encoding.
|
|
* - 'canonical': See {@link
|
|
* Horde_Ldap_Util::canonicalDN()}.
|
|
* - 'base64': Use base64 (default).
|
|
* - 'change' (boolean): Write entry changes to the LDIF file instead of
|
|
* the entries itself. I.e. write LDAP operations
|
|
* acting on the entries to the file instead of the
|
|
* entries contents. This writes the changes usually
|
|
* carried out by an update() to the LDIF
|
|
* file. Defaults to false.
|
|
* - 'lowercase' (boolean): Convert attribute names to lowercase when
|
|
* writing. Defaults to false.
|
|
* - 'sort' (boolean): Sort attribute names when writing entries according
|
|
* to the rule: objectclass first then all other
|
|
* attributes alphabetically sorted by attribute
|
|
* name. Defaults to false.
|
|
* - 'version' (integer): Set the LDIF version to write to the resulting
|
|
* LDIF file. According to RFC 2849 currently the
|
|
* only legal value for this option is 1. When this
|
|
* option is set Horde_Ldap_Ldif tries to adhere
|
|
* more strictly to the LDIF specification in
|
|
* RFC2489 in a few places. The default is null
|
|
* meaning no version information is written to the
|
|
* LDIF file.
|
|
* - 'wrap' (integer): Number of columns where output line wrapping shall
|
|
* occur. Default is 78. Setting it to 40 or lower
|
|
* inhibits wrapping.
|
|
* - 'raw' (string): Regular expression to denote the names of attributes
|
|
* that are to be considered binary in search results if
|
|
* writing entries. Example: 'raw' =>
|
|
* '/(?i:^jpegPhoto|;binary)/i'
|
|
*
|
|
* @param string|ressource $file Filename or file handle.
|
|
* @param string $mode Mode to open the file, either 'r', 'w'
|
|
* or 'a'.
|
|
* @param array $options Options like described above.
|
|
*
|
|
* @throws Horde_Ldap_Exception
|
|
*/
|
|
public function __construct($file, $mode = 'r', $options = array())
|
|
{
|
|
// Parse options.
|
|
foreach ($options as $option => $value) {
|
|
if (!array_key_exists($option, $this->_options)) {
|
|
throw new Horde_Ldap_Exception('Option ' . $option . ' not known');
|
|
}
|
|
$this->_options[$option] = Horde_String::lower($value);
|
|
}
|
|
|
|
// Set version.
|
|
$this->version($this->_options['version']);
|
|
|
|
// Setup file mode.
|
|
if (!preg_match('/^[rwa]$/', $mode)) {
|
|
throw new Horde_Ldap_Exception('File mode ' . $mode . ' not supported');
|
|
}
|
|
$this->_mode = $mode;
|
|
|
|
// Setup file handle.
|
|
if (is_resource($file)) {
|
|
// TODO: checks on mode possible?
|
|
$this->_fh = $file;
|
|
return;
|
|
}
|
|
|
|
switch ($mode) {
|
|
case 'r':
|
|
if (!file_exists($file)) {
|
|
throw new Horde_Ldap_Exception('Unable to open ' . $file . ' for reading: file not found');
|
|
}
|
|
if (!is_readable($file)) {
|
|
throw new Horde_Ldap_Exception('Unable to open ' . $file . ' for reading: permission denied');
|
|
}
|
|
break;
|
|
|
|
case 'w':
|
|
case 'a':
|
|
if (file_exists($file)) {
|
|
if (!is_writable($file)) {
|
|
throw new Horde_Ldap_Exception('Unable to open ' . $file . ' for writing: permission denied');
|
|
}
|
|
} else {
|
|
if (!@touch($file)) {
|
|
throw new Horde_Ldap_Exception('Unable to create ' . $file . ' for writing: permission denied');
|
|
}
|
|
}
|
|
break;
|
|
}
|
|
|
|
$this->_fh = @fopen($file, $this->_mode);
|
|
if (!$this->_fh) {
|
|
throw new Horde_Ldap_Exception('Could not open file ' . $file);
|
|
}
|
|
|
|
$this->_fhOpened = true;
|
|
}
|
|
|
|
/**
|
|
* Reads one entry from the file and return it as a Horde_Ldap_Entry
|
|
* object.
|
|
*
|
|
* @return Horde_Ldap_Entry
|
|
* @throws Horde_Ldap_Exception
|
|
*/
|
|
public function readEntry()
|
|
{
|
|
// Read fresh lines, set them as current lines and create the entry.
|
|
$attrs = $this->nextLines(true);
|
|
if (count($attrs)) {
|
|
$this->_linesCur = $attrs;
|
|
}
|
|
return $this->currentEntry();
|
|
}
|
|
|
|
/**
|
|
* Returns true when the end of the file is reached.
|
|
*
|
|
* @return boolean
|
|
*/
|
|
public function eof()
|
|
{
|
|
return feof($this->_fh);
|
|
}
|
|
|
|
/**
|
|
* Writes the entry or entries to the LDIF file.
|
|
*
|
|
* If you want to build an LDIF file containing several entries AND you
|
|
* want to call writeEntry() several times, you must open the file handle
|
|
* in append mode ('a'), otherwise you will always get the last entry only.
|
|
*
|
|
* @todo Implement operations on whole entries (adding a whole entry).
|
|
*
|
|
* @param Horde_Ldap_Entry|array $entries Entry or array of entries.
|
|
*
|
|
* @throws Horde_Ldap_Exception
|
|
*/
|
|
public function writeEntry($entries)
|
|
{
|
|
if (!is_array($entries)) {
|
|
$entries = array($entries);
|
|
}
|
|
|
|
foreach ($entries as $entry) {
|
|
$this->_entrynum++;
|
|
if (!($entry instanceof Horde_Ldap_Entry)) {
|
|
throw new Horde_Ldap_Exception('Entry ' . $this->_entrynum . ' is not an Horde_Ldap_Entry object');
|
|
}
|
|
|
|
if ($this->_options['change']) {
|
|
$this->_changeEntry($entry);
|
|
} else {
|
|
$this->_writeEntry($entry);
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Writes an LDIF file that describes an entry change.
|
|
*
|
|
* @param Horde_Ldap_Entry $entry
|
|
*
|
|
* @throws Horde_Ldap_Exception
|
|
*/
|
|
protected function _changeEntry($entry)
|
|
{
|
|
// Fetch change information from entry.
|
|
$entry_attrs_changes = $entry->getChanges();
|
|
$num_of_changes = count($entry_attrs_changes['add'])
|
|
+ count($entry_attrs_changes['replace'])
|
|
+ count($entry_attrs_changes['delete']);
|
|
|
|
$is_changed = $num_of_changes > 0 || $entry->willBeDeleted() || $entry->willBeMoved();
|
|
|
|
// Write version if not done yet, also write DN of entry.
|
|
if ($is_changed) {
|
|
if (!$this->_versionWritten) {
|
|
$this->writeVersion();
|
|
}
|
|
$this->_writeDN($entry->currentDN());
|
|
}
|
|
|
|
// Process changes.
|
|
// TODO: consider DN add!
|
|
if ($entry->willBeDeleted()) {
|
|
$this->_writeLine('changetype: delete');
|
|
} elseif ($entry->willBeMoved()) {
|
|
$this->_writeLine('changetype: modrdn');
|
|
$olddn = Horde_Ldap_Util::explodeDN($entry->currentDN(), array('casefold' => 'none'));
|
|
array_shift($olddn);
|
|
$oldparent = implode(',', $olddn);
|
|
$newdn = Horde_Ldap_Util::explodeDN($entry->dn(), array('casefold' => 'none'));
|
|
$rdn = array_shift($newdn);
|
|
$parent = implode(',', $newdn);
|
|
$this->_writeLine('newrdn: ' . $rdn);
|
|
$this->_writeLine('deleteoldrdn: 1');
|
|
if ($parent !== $oldparent) {
|
|
$this->_writeLine('newsuperior: ' . $parent);
|
|
}
|
|
// TODO: What if the entry has attribute changes as well?
|
|
// I think we should check for that and make a dummy
|
|
// entry with the changes that is written to the LDIF file.
|
|
} elseif ($num_of_changes > 0) {
|
|
// Write attribute change data.
|
|
$this->_writeLine('changetype: modify');
|
|
foreach ($entry_attrs_changes as $changetype => $entry_attrs) {
|
|
foreach ($entry_attrs as $attr_name => $attr_values) {
|
|
$this->_writeLine("$changetype: $attr_name");
|
|
if ($attr_values !== null) {
|
|
$this->_writeAttribute($attr_name, $attr_values, $changetype);
|
|
}
|
|
$this->_writeLine('-');
|
|
}
|
|
}
|
|
}
|
|
|
|
// Finish this entry's data if we had changes.
|
|
if ($is_changed) {
|
|
$this->_finishEntry();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Writes an LDIF file that describes an entry.
|
|
*
|
|
* @param Horde_Ldap_Entry $entry
|
|
*
|
|
* @throws Horde_Ldap_Exception
|
|
*/
|
|
protected function _writeEntry($entry)
|
|
{
|
|
// Fetch attributes for further processing.
|
|
$entry_attrs = $entry->getValues();
|
|
|
|
// Sort and put objectclass attributes to first position.
|
|
if ($this->_options['sort']) {
|
|
ksort($entry_attrs);
|
|
if (isset($entry_attrs['objectclass'])) {
|
|
$oc = $entry_attrs['objectclass'];
|
|
unset($entry_attrs['objectclass']);
|
|
$entry_attrs = array_merge(array('objectclass' => $oc), $entry_attrs);
|
|
}
|
|
}
|
|
|
|
// Write data.
|
|
if (!$this->_versionWritten) {
|
|
$this->writeVersion();
|
|
}
|
|
$this->_writeDN($entry->dn());
|
|
foreach ($entry_attrs as $attr_name => $attr_values) {
|
|
$this->_writeAttribute($attr_name, $attr_values);
|
|
}
|
|
$this->_finishEntry();
|
|
}
|
|
|
|
/**
|
|
* Writes the version to LDIF.
|
|
*
|
|
* If the object's version is defined, this method allows to explicitely
|
|
* write the version before an entry is written.
|
|
*
|
|
* If not called explicitely, it gets called automatically when writing the
|
|
* first entry.
|
|
*
|
|
* @throws Horde_Ldap_Exception
|
|
*/
|
|
public function writeVersion()
|
|
{
|
|
if (!is_null($this->version())) {
|
|
$this->_writeLine('version: ' . $this->version(), 'Unable to write version');
|
|
}
|
|
$this->_versionWritten = true;
|
|
}
|
|
|
|
/**
|
|
* Returns or sets the LDIF version.
|
|
*
|
|
* If called with an argument it sets the LDIF version. According to RFC
|
|
* 2849 currently the only legal value for the version is 1.
|
|
*
|
|
* @param integer $version LDIF version to set.
|
|
*
|
|
* @return integer The current or new version.
|
|
* @throws Horde_Ldap_Exception
|
|
*/
|
|
public function version($version = null)
|
|
{
|
|
if ($version !== null) {
|
|
if ($version != 1) {
|
|
throw new Horde_Ldap_Exception('Illegal LDIF version set');
|
|
}
|
|
$this->_options['version'] = $version;
|
|
}
|
|
return $this->_options['version'];
|
|
}
|
|
|
|
/**
|
|
* Returns the file handle the Horde_Ldap_Ldif object reads from or writes
|
|
* to.
|
|
*
|
|
* You can, for example, use this to fetch the content of the LDIF file
|
|
* manually.
|
|
*
|
|
* @return resource
|
|
* @throws Horde_Ldap_Exception
|
|
*/
|
|
public function handle()
|
|
{
|
|
if (!is_resource($this->_fh)) {
|
|
throw new Horde_Ldap_Exception('Invalid file resource');
|
|
}
|
|
return $this->_fh;
|
|
}
|
|
|
|
/**
|
|
* Cleans up.
|
|
*
|
|
* This method signals that the LDIF object is no longer needed. You can
|
|
* use this to free up some memory and close the file handle. The file
|
|
* handle is only closed, if it was opened from Horde_Ldap_Ldif.
|
|
*
|
|
* @throws Horde_Ldap_Exception
|
|
*/
|
|
public function done()
|
|
{
|
|
// Close file handle if we opened it.
|
|
if ($this->_fhOpened) {
|
|
fclose($this->handle());
|
|
}
|
|
|
|
// Free variables.
|
|
foreach (array_keys(get_object_vars($this)) as $name) {
|
|
unset($this->$name);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the current Horde_Ldap_Entry object.
|
|
*
|
|
* @return Horde_Ldap_Entry
|
|
* @throws Horde_Ldap_Exception
|
|
*/
|
|
public function currentEntry()
|
|
{
|
|
return $this->parseLines($this->currentLines());
|
|
}
|
|
|
|
/**
|
|
* Parse LDIF lines of one entry into an Horde_Ldap_Entry object.
|
|
*
|
|
* @todo what about file inclusions and urls?
|
|
* "jpegphoto:< file:///usr/local/directory/photos/fiona.jpg"
|
|
*
|
|
* @param array $lines LDIF lines for one entry.
|
|
*
|
|
* @return Horde_Ldap_Entry Horde_Ldap_Entry object for those lines.
|
|
* @throws Horde_Ldap_Exception
|
|
*/
|
|
public function parseLines($lines)
|
|
{
|
|
// Parse lines into an array of attributes and build the entry.
|
|
$attributes = array();
|
|
$dn = false;
|
|
foreach ($lines as $line) {
|
|
if (!preg_match('/^(\w+)(:|::|:<)\s(.+)$/', $line, $matches)) {
|
|
// Line not in "attr: value" format -> ignore. Maybe we should
|
|
// rise an error here, but this should be covered by
|
|
// nextLines() already. A problem arises, if users try to feed
|
|
// data of several entries to this method - the resulting entry
|
|
// will get wrong attributes. However, this is already
|
|
// mentioned in the method documentation above.
|
|
continue;
|
|
}
|
|
|
|
$attr = $matches[1];
|
|
$delim = $matches[2];
|
|
$data = $matches[3];
|
|
|
|
switch ($delim) {
|
|
case ':':
|
|
// Normal data.
|
|
$attributes[$attr][] = $data;
|
|
break;
|
|
case '::':
|
|
// Base64 data.
|
|
$attributes[$attr][] = base64_decode($data);
|
|
break;
|
|
case ':<':
|
|
// File inclusion
|
|
// TODO: Is this the job of the LDAP-client or the server?
|
|
throw new Horde_Ldap_Exception('File inclusions are currently not supported');
|
|
default:
|
|
throw new Horde_Ldap_Exception('Parsing error: invalid syntax at parsing entry line: ' . $line);
|
|
}
|
|
|
|
if (Horde_String::lower($attr) == 'dn') {
|
|
// DN line detected. Save possibly decoded DN.
|
|
$dn = $attributes[$attr][0];
|
|
// Remove wrongly added "dn: " attribute.
|
|
unset($attributes[$attr]);
|
|
}
|
|
}
|
|
|
|
if (!$dn) {
|
|
throw new Horde_Ldap_Exception('Parsing error: unable to detect DN for entry');
|
|
}
|
|
|
|
return Horde_Ldap_Entry::createFresh($dn, $attributes);
|
|
}
|
|
|
|
/**
|
|
* Returns the lines that generated the current Horde_Ldap_Entry object.
|
|
*
|
|
* Returns an empty array if no lines have been read so far.
|
|
*
|
|
* @return array Array of lines.
|
|
*/
|
|
public function currentLines()
|
|
{
|
|
return $this->_linesCur;
|
|
}
|
|
|
|
/**
|
|
* Returns the lines that will generate the next Horde_Ldap_Entry object.
|
|
*
|
|
* If you set $force to true you can iterate over the lines that build up
|
|
* entries manually. Otherwise, iterating is done using {@link
|
|
* readEntry()}. $force will move the file pointer forward, thus returning
|
|
* the next entry lines.
|
|
*
|
|
* Wrapped lines will be unwrapped. Comments are stripped.
|
|
*
|
|
* @param boolean $force Set this to true if you want to iterate over the
|
|
* lines manually
|
|
*
|
|
* @return array
|
|
* @throws Horde_Ldap_Exception
|
|
*/
|
|
public function nextLines($force = false)
|
|
{
|
|
// If we already have those lines, just return them, otherwise read.
|
|
if (count($this->_linesNext) == 0 || $force) {
|
|
// Empty in case something was left (if used $force).
|
|
$this->_linesNext = array();
|
|
$entry_done = false;
|
|
$fh = $this->handle();
|
|
// Are we in an comment? For wrapping purposes.
|
|
$commentmode = false;
|
|
// How many lines with data we have read?
|
|
$datalines_read = 0;
|
|
|
|
while (!$entry_done && !$this->eof()) {
|
|
$this->_inputLine++;
|
|
// Read line. Remove line endings, we want only data; this is
|
|
// okay since ending spaces should be encoded.
|
|
$data = rtrim(fgets($fh));
|
|
if ($data === false) {
|
|
// Error only, if EOF not reached after fgets() call.
|
|
if (!$this->eof()) {
|
|
throw new Horde_Ldap_Exception('Error reading from file at input line ' . $this->_inputLine);
|
|
}
|
|
break;
|
|
}
|
|
|
|
if (count($this->_linesNext) > 0 && preg_match('/^$/', $data)) {
|
|
// Entry is finished if we have an empty line after we had
|
|
// data.
|
|
$entry_done = true;
|
|
|
|
// Look ahead if the next EOF is nearby. Comments and empty
|
|
// lines at the file end may cause problems otherwise.
|
|
$current_pos = ftell($fh);
|
|
$data = fgets($fh);
|
|
while (!feof($fh)) {
|
|
if (preg_match('/^\s*$/', $data) ||
|
|
preg_match('/^#/', $data)) {
|
|
// Only empty lines or comments, continue to seek.
|
|
// TODO: Known bug: Wrappings for comments are okay
|
|
// but are treaten as error, since we do not
|
|
// honor comment mode here. This should be a
|
|
// very theoretically case, however I am
|
|
// willing to fix this if really necessary.
|
|
$this->_inputLine++;
|
|
$current_pos = ftell($fh);
|
|
$data = fgets($fh);
|
|
} else {
|
|
// Data found if non emtpy line and not a comment!!
|
|
// Rewind to position prior last read and stop
|
|
// lookahead.
|
|
fseek($fh, $current_pos);
|
|
break;
|
|
}
|
|
}
|
|
// Now we have either the file pointer at the beginning of
|
|
// a new data position or at the end of file causing feof()
|
|
// to return true.
|
|
continue;
|
|
}
|
|
|
|
// Build lines.
|
|
if (preg_match('/^version:\s(.+)$/', $data, $match)) {
|
|
// Version statement, set version.
|
|
$this->version($match[1]);
|
|
} elseif (preg_match('/^\w+::?\s.+$/', $data)) {
|
|
// Normal attribute: add line.
|
|
$commentmode = false;
|
|
$this->_linesNext[] = trim($data);
|
|
$datalines_read++;
|
|
} elseif (preg_match('/^\s(.+)$/', $data, $matches)) {
|
|
// Wrapped data: unwrap if not in comment mode.
|
|
if (!$commentmode) {
|
|
if ($datalines_read == 0) {
|
|
// First line of entry: wrapped data is illegal.
|
|
throw new Horde_Ldap_Exception('Illegal wrapping at input line ' . $this->_inputLine);
|
|
}
|
|
$this->_linesNext[] = array_pop($this->_linesNext) . trim($matches[1]);
|
|
$datalines_read++;
|
|
}
|
|
} elseif (preg_match('/^#/', $data)) {
|
|
// LDIF comments.
|
|
$commentmode = true;
|
|
} elseif (preg_match('/^\s*$/', $data)) {
|
|
// Empty line but we had no data for this entry, so just
|
|
// ignore this line.
|
|
$commentmode = false;
|
|
} else {
|
|
throw new Horde_Ldap_Exception('Invalid syntax at input line ' . $this->_inputLine);
|
|
}
|
|
}
|
|
}
|
|
|
|
return $this->_linesNext;
|
|
}
|
|
|
|
/**
|
|
* Converts an attribute and value to LDIF string representation.
|
|
*
|
|
* It honors correct encoding of values according to RFC 2849. Line
|
|
* wrapping will occur at the configured maximum but only if the value is
|
|
* greater than 40 chars.
|
|
*
|
|
* @param string $attr_name Name of the attribute.
|
|
* @param string $attr_value Value of the attribute.
|
|
*
|
|
* @return string LDIF string for that attribute and value.
|
|
*/
|
|
protected function _convertAttribute($attr_name, $attr_value)
|
|
{
|
|
// Handle empty attribute or process.
|
|
if (!strlen($attr_value)) {
|
|
return $attr_name.': ';
|
|
}
|
|
|
|
// If converting is needed, do it.
|
|
// Either we have some special chars or a matching "raw" regex
|
|
if ($this->_isBinary($attr_value) ||
|
|
($this->_options['raw'] &&
|
|
preg_match($this->_options['raw'], $attr_name))) {
|
|
$attr_name .= ':';
|
|
$attr_value = base64_encode($attr_value);
|
|
}
|
|
|
|
// Lowercase attribute names if requested.
|
|
if ($this->_options['lowercase']) {
|
|
$attr_name = Horde_String::lower($attr_name);
|
|
}
|
|
|
|
// Handle line wrapping.
|
|
if ($this->_options['wrap'] > 40 &&
|
|
strlen($attr_value) > $this->_options['wrap']) {
|
|
$attr_value = wordwrap($attr_value, $this->_options['wrap'], PHP_EOL . ' ', true);
|
|
}
|
|
|
|
return $attr_name . ': ' . $attr_value;
|
|
}
|
|
|
|
/**
|
|
* Converts an entry's DN to LDIF string representation.
|
|
*
|
|
* It honors correct encoding of values according to RFC 2849.
|
|
*
|
|
* @todo I am not sure, if the UTF8 stuff is correctly handled right now
|
|
*
|
|
* @param string $dn UTF8 encoded DN.
|
|
*
|
|
* @return string LDIF string for that DN.
|
|
*/
|
|
protected function _convertDN($dn)
|
|
{
|
|
// If converting is needed, do it.
|
|
return $this->_isBinary($dn)
|
|
? 'dn:: ' . base64_encode($dn)
|
|
: 'dn: ' . $dn;
|
|
}
|
|
|
|
/**
|
|
* Returns whether some data is considered binary and must be
|
|
* base64-encoded.
|
|
*
|
|
* @param string $value Some data.
|
|
*
|
|
* @return boolean True if the data should be encoded.
|
|
*/
|
|
protected function _isBinary($value)
|
|
{
|
|
$binary = false;
|
|
|
|
// ASCII-chars that are NOT safe for the start and for being inside the
|
|
// value. These are the integer values of those chars.
|
|
$unsafe_init = array(0, 10, 13, 32, 58, 60);
|
|
$unsafe = array(0, 10, 13);
|
|
|
|
// Test for illegal init char.
|
|
$init_ord = ord(substr($value, 0, 1));
|
|
if ($init_ord > 127 || in_array($init_ord, $unsafe_init)) {
|
|
$binary = true;
|
|
}
|
|
|
|
// Test for illegal content char.
|
|
for ($i = 0, $len = strlen($value); $i < $len; $i++) {
|
|
$char_ord = ord(substr($value, $i, 1));
|
|
if ($char_ord >= 127 || in_array($char_ord, $unsafe)) {
|
|
$binary = true;
|
|
}
|
|
}
|
|
|
|
// Test for ending space
|
|
if (substr($value, -1) == ' ') {
|
|
$binary = true;
|
|
}
|
|
|
|
return $binary;
|
|
}
|
|
|
|
/**
|
|
* Writes an attribute to the file handle.
|
|
*
|
|
* @param string $attr_name Name of the attribute.
|
|
* @param string|array $attr_values Single attribute value or array with
|
|
* attribute values.
|
|
*
|
|
* @throws Horde_Ldap_Exception
|
|
*/
|
|
protected function _writeAttribute($attr_name, $attr_values)
|
|
{
|
|
// Write out attribute content.
|
|
if (!is_array($attr_values)) {
|
|
$attr_values = array($attr_values);
|
|
}
|
|
foreach ($attr_values as $attr_val) {
|
|
$line = $this->_convertAttribute($attr_name, $attr_val);
|
|
$this->_writeLine($line, 'Unable to write attribute ' . $attr_name . ' of entry ' . $this->_entrynum);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Writes a DN to the file handle.
|
|
*
|
|
* @param string $dn DN to write.
|
|
*
|
|
* @throws Horde_Ldap_Exception
|
|
*/
|
|
protected function _writeDN($dn)
|
|
{
|
|
// Prepare DN.
|
|
if ($this->_options['encode'] == 'base64') {
|
|
$dn = $this->_convertDN($dn);
|
|
} elseif ($this->_options['encode'] == 'canonical') {
|
|
$dn = Horde_Ldap_Util::canonicalDN($dn, array('casefold' => 'none'));
|
|
}
|
|
$this->_writeLine($dn, 'Unable to write DN of entry ' . $this->_entrynum);
|
|
}
|
|
|
|
/**
|
|
* Finishes an LDIF entry.
|
|
*
|
|
* @throws Horde_Ldap_Exception
|
|
*/
|
|
protected function _finishEntry()
|
|
{
|
|
$this->_writeLine('', 'Unable to close entry ' . $this->_entrynum);
|
|
}
|
|
|
|
/**
|
|
* Writes an arbitary line to the file handle.
|
|
*
|
|
* @param string $line Content to write.
|
|
* @param string $error If error occurs, throw this exception message.
|
|
*
|
|
* @throws Horde_Ldap_Exception
|
|
*/
|
|
protected function _writeLine($line, $error = 'Unable to write to file handle')
|
|
{
|
|
$line .= PHP_EOL;
|
|
if (is_resource($this->handle()) &&
|
|
fwrite($this->handle(), $line, strlen($line)) === false) {
|
|
throw new Horde_Ldap_Exception($error);
|
|
}
|
|
}
|
|
}
|