620 lines
23 KiB
PHP
620 lines
23 KiB
PHP
<?php
|
|
/**
|
|
* Utility Class for Horde_Ldap
|
|
*
|
|
* This class servers some functionality to the other classes of Horde_Ldap but
|
|
* most of the methods can be used separately as well.
|
|
*
|
|
* 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
|
|
*/
|
|
class Horde_Ldap_Util
|
|
{
|
|
/**
|
|
* Explodes the given DN into its elements
|
|
*
|
|
* {@link http://www.ietf.org/rfc/rfc2253.txt RFC 2253} says, a
|
|
* Distinguished Name is a sequence of Relative Distinguished Names (RDNs),
|
|
* which themselves are sets of Attributes. For each RDN a array is
|
|
* constructed where the RDN part is stored.
|
|
*
|
|
* For example, the DN 'OU=Sales+CN=J. Smith,DC=example,DC=net' is exploded
|
|
* to:
|
|
* <code>
|
|
* array(array('OU=Sales', 'CN=J. Smith'),
|
|
* 'DC=example',
|
|
* 'DC=net')
|
|
* </code>
|
|
*
|
|
* [NOT IMPLEMENTED] DNs might also contain values, which are the bytes of
|
|
* the BER encoding of the X.500 AttributeValue rather than some LDAP
|
|
* string syntax. These values are hex-encoded and prefixed with a #. To
|
|
* distinguish such BER values, explodeDN uses references to the
|
|
* actual values, e.g. '1.3.6.1.4.1.1466.0=#04024869,DC=example,DC=com' is
|
|
* exploded to:
|
|
* <code>
|
|
* array(array('1.3.6.1.4.1.1466.0' => "\004\002Hi"),
|
|
* array('DC' => 'example',
|
|
* array('DC' => 'com'))
|
|
* <code>
|
|
* See {@link http://www.vijaymukhi.com/vmis/berldap.htm} for more
|
|
* information on BER.
|
|
*
|
|
* It also performs the following operations on the given DN:
|
|
* - Unescape "\" followed by ",", "+", """, "\", "<", ">", ";", "#", "=",
|
|
* " ", or a hexpair and strings beginning with "#".
|
|
* - Removes the leading 'OID.' characters if the type is an OID instead of
|
|
* a name.
|
|
* - If an RDN contains multiple parts, the parts are re-ordered so that
|
|
* the attribute type names are in alphabetical order.
|
|
*
|
|
* $options is a list of name/value pairs, valid options are:
|
|
* - casefold: Controls case folding of attribute types names.
|
|
* Attribute values are not affected by this option.
|
|
* The default is to uppercase. Valid values are:
|
|
* - lower: Lowercase attribute types names.
|
|
* - upper: Uppercase attribute type names. This is the
|
|
* default.
|
|
* - none: Do not change attribute type names.
|
|
* - reverse: If true, the RDN sequence is reversed.
|
|
* - onlyvalues: If true, then only attributes values are returned ('foo'
|
|
* instead of 'cn=foo')
|
|
*
|
|
* @todo implement BER
|
|
* @todo replace preg_replace() callbacks.
|
|
*
|
|
* @param string $dn The DN that should be exploded.
|
|
* @param array $options Options to use.
|
|
*
|
|
* @return array Parts of the exploded DN.
|
|
*/
|
|
public static function explodeDN($dn, array $options = array())
|
|
{
|
|
$options = array_merge(
|
|
array(
|
|
'casefold' => 'upper',
|
|
'onlyvalues' => false,
|
|
'reverse' => false,
|
|
),
|
|
$options
|
|
);
|
|
|
|
// Escaping of DN and stripping of "OID.".
|
|
$dn = self::canonicalDN($dn, array('casefold' => $options['casefold']));
|
|
|
|
// Splitting the DN.
|
|
$dn_array = preg_split('/(?<!\\\\),/', $dn);
|
|
|
|
// Clear wrong splitting (possibly we have split too much).
|
|
// Not clear, if this is neccessary here:
|
|
//$dn_array = self::_correctDNSplitting($dn_array, ',');
|
|
|
|
$callback_upper = function($value) {
|
|
return Horde_String::upper($value[1]);
|
|
};
|
|
$callback_lower = function($value) {
|
|
return Horde_String::lower($value[1]);
|
|
};
|
|
|
|
// Construct subarrays for multivalued RDNs and unescape DN value, also
|
|
// convert to output format and apply casefolding.
|
|
foreach ($dn_array as $key => $value) {
|
|
$value_u = self::unescapeDNValue($value);
|
|
$rdns = self::splitRDNMultivalue($value_u[0]);
|
|
// TODO: nuke code duplication
|
|
if (count($rdns) > 1) {
|
|
// Multivalued RDN!
|
|
foreach ($rdns as $subrdn_k => $subrdn_v) {
|
|
// Casefolding.
|
|
if ($options['casefold'] == 'upper') {
|
|
$subrdn_v = preg_replace_callback('/^(\w+=)/',
|
|
$callback_upper,
|
|
$subrdn_v);
|
|
}
|
|
if ($options['casefold'] == 'lower') {
|
|
$subrdn_v = preg_replace_callback('/^(\w+=)/',
|
|
$callback_lower,
|
|
$subrdn_v);
|
|
}
|
|
|
|
if ($options['onlyvalues']) {
|
|
preg_match('/(.+?)(?<!\\\\)=(.+)/', $subrdn_v, $matches);
|
|
$rdn_val = $matches[2];
|
|
$unescaped = self::unescapeDNValue($rdn_val);
|
|
$rdns[$subrdn_k] = $unescaped[0];
|
|
} else {
|
|
$unescaped = self::unescapeDNValue($subrdn_v);
|
|
$rdns[$subrdn_k] = $unescaped[0];
|
|
}
|
|
}
|
|
|
|
$dn_array[$key] = $rdns;
|
|
} else {
|
|
// Singlevalued RDN.
|
|
// Casefolding.
|
|
if ($options['casefold'] == 'upper') {
|
|
$value = preg_replace_callback('/^(\w+=)/',
|
|
$callback_upper,
|
|
$value);
|
|
}
|
|
if ($options['casefold'] == 'lower') {
|
|
$value = preg_replace_callback('/^(\w+=)/',
|
|
$callback_lower,
|
|
$value);
|
|
}
|
|
|
|
if ($options['onlyvalues']) {
|
|
preg_match('/(.+?)(?<!\\\\)=(.+)/', $value, $matches);
|
|
$dn_val = $matches[2];
|
|
$unescaped = self::unescapeDNValue($dn_val);
|
|
$dn_array[$key] = $unescaped[0];
|
|
} else {
|
|
$unescaped = self::unescapeDNValue($value);
|
|
$dn_array[$key] = $unescaped[0];
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($options['reverse']) {
|
|
return array_reverse($dn_array);
|
|
}
|
|
|
|
return $dn_array;
|
|
}
|
|
|
|
/**
|
|
* Escapes DN values according to RFC 2253.
|
|
*
|
|
* Escapes the given VALUES according to RFC 2253 so that they can be
|
|
* safely used in LDAP DNs. The characters ",", "+", """, "\", "<", ">",
|
|
* ";", "#", "=" with a special meaning in RFC 2252 are preceeded by ba
|
|
* backslash. Control characters with an ASCII code < 32 are represented as
|
|
* \hexpair. Finally all leading and trailing spaces are converted to
|
|
* sequences of \20.
|
|
*
|
|
* @param string|array $values DN values that should be escaped.
|
|
*
|
|
* @return array The escaped values.
|
|
*/
|
|
public static function escapeDNValue($values)
|
|
{
|
|
// Parameter validation.
|
|
if (!is_array($values)) {
|
|
$values = array($values);
|
|
}
|
|
|
|
foreach ($values as $key => $val) {
|
|
// Escaping of filter meta characters.
|
|
$val = addcslashes($val, '\\,+"<>;#=');
|
|
|
|
// ASCII < 32 escaping.
|
|
$val = self::asc2hex32($val);
|
|
|
|
// Convert all leading and trailing spaces to sequences of \20.
|
|
if (preg_match('/^(\s*)(.+?)(\s*)$/', $val, $matches)) {
|
|
$val = str_repeat('\20', strlen($matches[1])) . $matches[2] . str_repeat('\20', strlen($matches[3]));
|
|
}
|
|
|
|
if (null === $val) {
|
|
// Apply escaped "null" if string is empty.
|
|
$val = '\0';
|
|
}
|
|
|
|
$values[$key] = $val;
|
|
}
|
|
|
|
return $values;
|
|
}
|
|
|
|
/**
|
|
* Unescapes DN values according to RFC 2253.
|
|
*
|
|
* Reverts the conversion done by escapeDNValue().
|
|
*
|
|
* Any escape sequence starting with a baskslash - hexpair or special
|
|
* character - will be transformed back to the corresponding character.
|
|
*
|
|
* @param array $values DN values.
|
|
*
|
|
* @return array Unescaped DN values.
|
|
*/
|
|
public static function unescapeDNValue($values)
|
|
{
|
|
// Parameter validation.
|
|
if (!is_array($values)) {
|
|
$values = array($values);
|
|
}
|
|
|
|
foreach ($values as $key => $val) {
|
|
// Strip slashes from special chars.
|
|
$val = str_replace(
|
|
array('\\\\', '\,', '\+', '\"', '\<', '\>', '\;', '\#', '\='),
|
|
array('\\', ',', '+', '"', '<', '>', ';', '#', '='),
|
|
$val);
|
|
|
|
// Translate hex code into ascii.
|
|
$values[$key] = self::hex2asc($val);
|
|
}
|
|
|
|
return $values;
|
|
}
|
|
|
|
/**
|
|
* Converts a DN into a canonical form.
|
|
*
|
|
* DN can either be a string or an array as returned by explodeDN(),
|
|
* which is useful when constructing a DN. The DN array may have be
|
|
* indexed (each array value is a OCL=VALUE pair) or associative (array key
|
|
* is OCL and value is VALUE).
|
|
*
|
|
* It performs the following operations on the given DN:
|
|
* - Removes the leading 'OID.' characters if the type is an OID instead of
|
|
* a name.
|
|
* - Escapes all RFC 2253 special characters (",", "+", """, "\", "<", ">",
|
|
* ";", "#", "="), slashes ("/"), and any other character where the ASCII
|
|
* code is < 32 as \hexpair.
|
|
* - Converts all leading and trailing spaces in values to be \20.
|
|
* - If an RDN contains multiple parts, the parts are re-ordered so that
|
|
* the attribute type names are in alphabetical order.
|
|
*
|
|
* $options is a list of name/value pairs, valid options are:
|
|
*
|
|
* - casefold: Controls case folding of attribute type names. Attribute
|
|
* values are not affected by this option. The default is to
|
|
* uppercase. Valid values are:
|
|
* - lower: Lowercase attribute type names.
|
|
* - upper: Uppercase attribute type names.
|
|
* - none: Do not change attribute type names.
|
|
* - reverse: If true, the RDN sequence is reversed.
|
|
* - separator: Separator to use between RDNs. Defaults to comma (',').
|
|
*
|
|
* The empty string "" is a valid DN, so be sure not to do a "$can_dn ==
|
|
* false" test, because an empty string evaluates to false. Use the "==="
|
|
* operator instead.
|
|
*
|
|
* @param array|string $dn The DN.
|
|
* @param array $options Options to use.
|
|
*
|
|
* @return boolean|string The canonical DN or false if the DN is not valid.
|
|
*/
|
|
public static function canonicalDN($dn, array $options = array())
|
|
{
|
|
if ($dn === '') {
|
|
// Empty DN is valid.
|
|
return $dn;
|
|
}
|
|
|
|
// Options check.
|
|
$options = array_merge(
|
|
array(
|
|
'casefold' => 'upper',
|
|
'reverse' => false,
|
|
'separator' => ',',
|
|
),
|
|
$options
|
|
);
|
|
|
|
if (!is_array($dn)) {
|
|
// It is not clear to me if the perl implementation splits by the
|
|
// user defined separator or if it just uses this separator to
|
|
// construct the new DN.
|
|
$dn = preg_split('/(?<!\\\\)' . preg_quote($options['separator']) . '/', $dn);
|
|
|
|
// Clear wrong splitting (possibly we have split too much).
|
|
$dn = self::_correctDNSplitting($dn, $options['separator']);
|
|
} else {
|
|
// Is array, check if the array is indexed or associative.
|
|
$assoc = false;
|
|
foreach ($dn as $dn_key => $dn_part) {
|
|
if (!is_int($dn_key)) {
|
|
$assoc = true;
|
|
break;
|
|
}
|
|
}
|
|
|
|
// Convert to indexed, if associative array detected.
|
|
if ($assoc) {
|
|
$newdn = array();
|
|
foreach ($dn as $dn_key => $dn_part) {
|
|
if (is_array($dn_part)) {
|
|
// We assume here that the RDN parts are also
|
|
// associative.
|
|
ksort($dn_part, SORT_STRING);
|
|
// Copy array as-is, so we can resolve it later.
|
|
$newdn[] = $dn_part;
|
|
} else {
|
|
$newdn[] = $dn_key . '=' . $dn_part;
|
|
}
|
|
}
|
|
$dn =& $newdn;
|
|
}
|
|
}
|
|
|
|
// Escaping and casefolding.
|
|
foreach ($dn as $pos => $dnval) {
|
|
if (is_array($dnval)) {
|
|
// Subarray detected, this means most probably that we had a
|
|
// multivalued DN part, which must be resolved.
|
|
$dnval_new = '';
|
|
foreach ($dnval as $subkey => $subval) {
|
|
// Build RDN part.
|
|
if (!is_int($subkey)) {
|
|
$subval = $subkey . '=' . $subval;
|
|
}
|
|
$subval_processed = self::canonicalDN($subval, $options);
|
|
if (false === $subval_processed) {
|
|
return false;
|
|
}
|
|
$dnval_new .= $subval_processed . '+';
|
|
}
|
|
// Store RDN part, strip last plus.
|
|
$dn[$pos] = substr($dnval_new, 0, -1);
|
|
} else {
|
|
// Try to split multivalued RDNs into array.
|
|
$rdns = self::splitRDNMultivalue($dnval);
|
|
if (count($rdns) > 1) {
|
|
// Multivalued RDN was detected. The RDN value is expected
|
|
// to be correctly split by splitRDNMultivalue(). It's time
|
|
// to sort the RDN and build the DN.
|
|
$rdn_string = '';
|
|
// Sort RDN keys alphabetically.
|
|
sort($rdns, SORT_STRING);
|
|
foreach ($rdns as $rdn) {
|
|
$subval_processed = self::canonicalDN($rdn, $options);
|
|
if (false === $subval_processed) {
|
|
return false;
|
|
}
|
|
$rdn_string .= $subval_processed . '+';
|
|
}
|
|
|
|
// Store RDN part, strip last plus.
|
|
$dn[$pos] = substr($rdn_string, 0, -1);
|
|
} else {
|
|
// No multivalued RDN. Split at first unescaped "=".
|
|
$dn_comp = self::splitAttributeString($rdns[0]);
|
|
if (count($dn_comp) != 2) {
|
|
throw new Horde_Ldap_Exception('Invalid RDN: ' . $rdns[0]);
|
|
}
|
|
// Trim left whitespaces because of "cn=foo, l=bar" syntax
|
|
// (whitespace after comma).
|
|
$ocl = ltrim($dn_comp[0]);
|
|
$val = $dn_comp[1];
|
|
|
|
// Strip 'OID.', otherwise apply casefolding and escaping.
|
|
if (substr(Horde_String::lower($ocl), 0, 4) == 'oid.') {
|
|
$ocl = substr($ocl, 4);
|
|
} else {
|
|
if ($options['casefold'] == 'upper') {
|
|
$ocl = Horde_String::upper($ocl);
|
|
}
|
|
if ($options['casefold'] == 'lower') {
|
|
$ocl = Horde_String::lower($ocl);
|
|
}
|
|
$ocl = self::escapeDNValue(array($ocl));
|
|
$ocl = $ocl[0];
|
|
}
|
|
|
|
// Escaping of DN value.
|
|
// TODO: if the value is already correctly escaped, we get
|
|
// double escaping.
|
|
$val = self::escapeDNValue(array($val));
|
|
$val = str_replace('/', '\/', $val[0]);
|
|
|
|
$dn[$pos] = $ocl . '=' . $val;
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($options['reverse']) {
|
|
$dn = array_reverse($dn);
|
|
}
|
|
|
|
return implode($options['separator'], $dn);
|
|
}
|
|
|
|
/**
|
|
* Escapes the given values according to RFC 2254 so that they can be
|
|
* safely used in LDAP filters.
|
|
*
|
|
* Any control characters with an ACII code < 32 as well as the characters
|
|
* with special meaning in LDAP filters "*", "(", ")", and "\" (the
|
|
* backslash) are converted into the representation of a backslash followed
|
|
* by two hex digits representing the hexadecimal value of the character.
|
|
*
|
|
* @param array $values Values to escape.
|
|
*
|
|
* @return array Escaped values.
|
|
*/
|
|
public static function escapeFilterValue($values)
|
|
{
|
|
// Parameter validation.
|
|
if (!is_array($values)) {
|
|
$values = array($values);
|
|
}
|
|
|
|
foreach ($values as $key => $val) {
|
|
// Escaping of filter meta characters.
|
|
$val = str_replace(array('\\', '*', '(', ')'),
|
|
array('\5c', '\2a', '\28', '\29'),
|
|
$val);
|
|
|
|
// ASCII < 32 escaping.
|
|
$val = self::asc2hex32($val);
|
|
|
|
if (null === $val) {
|
|
// Apply escaped "null" if string is empty.
|
|
$val = '\0';
|
|
}
|
|
|
|
$values[$key] = $val;
|
|
}
|
|
|
|
return $values;
|
|
}
|
|
|
|
/**
|
|
* Unescapes the given values according to RFC 2254.
|
|
*
|
|
* Reverses the conversion done by {@link escapeFilterValue()}.
|
|
*
|
|
* Converts any sequences of a backslash followed by two hex digits into
|
|
* the corresponding character.
|
|
*
|
|
* @param array $values Values to unescape.
|
|
*
|
|
* @return array Unescaped values.
|
|
*/
|
|
public static function unescapeFilterValue($values = array())
|
|
{
|
|
// Parameter validation.
|
|
if (!is_array($values)) {
|
|
$values = array($values);
|
|
}
|
|
|
|
foreach ($values as $key => $value) {
|
|
// Translate hex code into ascii.
|
|
$values[$key] = self::hex2asc($value);
|
|
}
|
|
|
|
return $values;
|
|
}
|
|
|
|
/**
|
|
* Converts all ASCII chars < 32 to "\HEX".
|
|
*
|
|
* @param string $string String to convert.
|
|
*
|
|
* @return string Hexadecimal representation of $string.
|
|
*/
|
|
public static function asc2hex32($string)
|
|
{
|
|
for ($i = 0, $len = strlen($string); $i < $len; $i++) {
|
|
$char = substr($string, $i, 1);
|
|
if (ord($char) < 32) {
|
|
$hex = dechex(ord($char));
|
|
if (strlen($hex) == 1) {
|
|
$hex = '0' . $hex;
|
|
}
|
|
$string = str_replace($char, '\\' . $hex, $string);
|
|
}
|
|
}
|
|
return $string;
|
|
}
|
|
|
|
/**
|
|
* Converts all hexadecimal expressions ("\HEX") to their original ASCII
|
|
* characters.
|
|
*
|
|
* @author beni@php.net, heavily based on work from DavidSmith@byu.net
|
|
*
|
|
* @param string $string String to convert.
|
|
*
|
|
* @return string ASCII representation of $string.
|
|
*/
|
|
public static function hex2asc($string)
|
|
{
|
|
return preg_replace_callback(
|
|
'/\\\([0-9A-Fa-f]{2})/',
|
|
function($hex) {
|
|
return chr(hexdec($hex[1]));
|
|
},
|
|
$string);
|
|
}
|
|
|
|
/**
|
|
* Splits a multivalued RDN value into an array.
|
|
*
|
|
* A RDN can contain multiple values, spearated by a plus sign. This method
|
|
* returns each separate ocl=value pair of the RDN part.
|
|
*
|
|
* If no multivalued RDN is detected, an array containing only the original
|
|
* RDN part is returned.
|
|
*
|
|
* For example, the multivalued RDN 'OU=Sales+CN=J. Smith' is exploded to:
|
|
* <kbd>array([0] => 'OU=Sales', [1] => 'CN=J. Smith')</kbd>
|
|
*
|
|
* The method tries to be smart if it encounters unescaped "+" characters,
|
|
* but may fail, so better ensure escaped "+" in attribute names and
|
|
* values.
|
|
*
|
|
* [BUG] If you have a multivalued RDN with unescaped plus characters and
|
|
* there is a unescaped plus sign at the end of an value followed by
|
|
* an attribute name containing an unescaped plus, then you will get
|
|
* wrong splitting:
|
|
* $rdn = 'OU=Sales+C+N=J. Smith';
|
|
* returns:
|
|
* array('OU=Sales+C', 'N=J. Smith');
|
|
* The "C+" is treaten as the value of the first pair instead of as
|
|
* the attribute name of the second pair. To prevent this, escape
|
|
* correctly.
|
|
*
|
|
* @param string $rdn Part of a (multivalued) escaped RDN (e.g. ou=foo or
|
|
* ou=foo+cn=bar)
|
|
*
|
|
* @return array The components of the multivalued RDN.
|
|
*/
|
|
public static function splitRDNMultivalue($rdn)
|
|
{
|
|
$rdns = preg_split('/(?<!\\\\)\+/', $rdn);
|
|
$rdns = self::_correctDNSplitting($rdns, '+');
|
|
return array_values($rdns);
|
|
}
|
|
|
|
/**
|
|
* Splits a attribute=value syntax into an array.
|
|
*
|
|
* The split will occur at the first unescaped '=' character.
|
|
*
|
|
* @param string $attr An attribute-value string.
|
|
*
|
|
* @return array Indexed array: 0=attribute name, 1=attribute value.
|
|
*/
|
|
public static function splitAttributeString($attr)
|
|
{
|
|
return preg_split('/(?<!\\\\)=/', $attr, 2);
|
|
}
|
|
|
|
/**
|
|
* Corrects splitting of DN parts.
|
|
*
|
|
* @param array $dn Raw DN array.
|
|
* @param array $separator Separator that was used when splitting.
|
|
*
|
|
* @return array Corrected array.
|
|
*/
|
|
protected static function _correctDNSplitting($dn = array(),
|
|
$separator = ',')
|
|
{
|
|
foreach ($dn as $key => $dn_value) {
|
|
// Refresh value (foreach caches!)
|
|
$dn_value = $dn[$key];
|
|
// If $dn_value is not in attr=value format, we had an unescaped
|
|
// separator character inside the attr name or the value. We assume
|
|
// that it was the attribute value.
|
|
|
|
// TODO: To solve this, we might ask the schema. The
|
|
// Horde_Ldap_Util class must remain independent from the
|
|
// other classes or connections though.
|
|
if (!preg_match('/.+(?<!\\\\)=.+/', $dn_value)) {
|
|
unset($dn[$key]);
|
|
if (array_key_exists($key - 1, $dn)) {
|
|
// Append to previous attribute value.
|
|
$dn[$key - 1] = $dn[$key - 1] . $separator . $dn_value;
|
|
} elseif (array_key_exists($key + 1, $dn)) {
|
|
// First element: prepend to next attribute name.
|
|
$dn[$key + 1] = $dn_value . $separator . $dn[$key + 1];
|
|
} else {
|
|
$dn[$key] = $dn_value;
|
|
}
|
|
}
|
|
}
|
|
return array_values($dn);
|
|
}
|
|
}
|