diff options
Diffstat (limited to 'extlib/Net/LDAP2/Util.php')
-rw-r--r-- | extlib/Net/LDAP2/Util.php | 572 |
1 files changed, 572 insertions, 0 deletions
diff --git a/extlib/Net/LDAP2/Util.php b/extlib/Net/LDAP2/Util.php new file mode 100644 index 000000000..48b03f9f9 --- /dev/null +++ b/extlib/Net/LDAP2/Util.php @@ -0,0 +1,572 @@ +<?php +/* vim: set expandtab tabstop=4 shiftwidth=4: */ +/** +* File containing the Net_LDAP2_Util interface class. +* +* PHP version 5 +* +* @category Net +* @package Net_LDAP2 +* @author Benedikt Hallinger <beni@php.net> +* @copyright 2009 Benedikt Hallinger +* @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3 +* @version SVN: $Id: Util.php 286718 2009-08-03 07:30:49Z beni $ +* @link http://pear.php.net/package/Net_LDAP2/ +*/ + +/** +* Includes +*/ +require_once 'PEAR.php'; + +/** +* Utility Class for Net_LDAP2 +* +* This class servers some functionality to the other classes of Net_LDAP2 but most of +* the methods can be used separately as well. +* +* @category Net +* @package Net_LDAP2 +* @author Benedikt Hallinger <beni@php.net> +* @license http://www.gnu.org/copyleft/lesser.html LGPL +* @link http://pear.php.net/package/Net_LDAP22/ +*/ +class Net_LDAP2_Util extends PEAR +{ + /** + * Constructor + * + * @access public + */ + public function __construct() + { + // We do nothing here, since all methods can be called statically. + // In Net_LDAP <= 0.7, we needed a instance of Util, because + // it was possible to do utf8 encoding and decoding, but this + // has been moved to the LDAP class. The constructor remains only + // here to document the downward compatibility of creating an instance. + } + + /** + * 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: + * <kbd>array( [0] => array([0] => 'OU=Sales', [1] => 'CN=J. Smith'), [2] => 'DC=example', [3] => 'DC=net' )</kbd> + * + * [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, ldap_explode_dn uses references to + * the actual values, e.g. '1.3.6.1.4.1.1466.0=#04024869,DC=example,DC=com' is exploded to: + * [ { '1.3.6.1.4.1.1466.0' => "\004\002Hi" }, { 'DC' => 'example' }, { 'DC' => 'com' } ]; + * 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') + * + + * @param string $dn The DN that should be exploded + * @param array $options Options to use + * + * @static + * @return array Parts of the exploded DN + * @todo implement BER + */ + public static function ldap_explode_dn($dn, $options = array('casefold' => 'upper')) + { + if (!isset($options['onlyvalues'])) $options['onlyvalues'] = false; + if (!isset($options['reverse'])) $options['reverse'] = false; + if (!isset($options['casefold'])) $options['casefold'] = 'upper'; + + // Escaping of DN and stripping of "OID." + $dn = self::canonical_dn($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::correct_dn_splitting($dn_array, ','); + + // 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::unescape_dn_value($value); + $rdns = self::split_rdn_multival($value_u[0]); + if (count($rdns) > 1) { + // MV RDN! + foreach ($rdns as $subrdn_k => $subrdn_v) { + // Casefolding + if ($options['casefold'] == 'upper') $subrdn_v = preg_replace("/^(\w+=)/e", "''.strtoupper('\\1').''", $subrdn_v); + if ($options['casefold'] == 'lower') $subrdn_v = preg_replace("/^(\w+=)/e", "''.strtolower('\\1').''", $subrdn_v); + + if ($options['onlyvalues']) { + preg_match('/(.+?)(?<!\\\\)=(.+)/', $subrdn_v, $matches); + $rdn_ocl = $matches[1]; + $rdn_val = $matches[2]; + $unescaped = self::unescape_dn_value($rdn_val); + $rdns[$subrdn_k] = $unescaped[0]; + } else { + $unescaped = self::unescape_dn_value($subrdn_v); + $rdns[$subrdn_k] = $unescaped[0]; + } + } + + $dn_array[$key] = $rdns; + } else { + // normal RDN + + // Casefolding + if ($options['casefold'] == 'upper') $value = preg_replace("/^(\w+=)/e", "''.strtoupper('\\1').''", $value); + if ($options['casefold'] == 'lower') $value = preg_replace("/^(\w+=)/e", "''.strtolower('\\1').''", $value); + + if ($options['onlyvalues']) { + preg_match('/(.+?)(?<!\\\\)=(.+)/', $value, $matches); + $dn_ocl = $matches[1]; + $dn_val = $matches[2]; + $unescaped = self::unescape_dn_value($dn_val); + $dn_array[$key] = $unescaped[0]; + } else { + $unescaped = self::unescape_dn_value($value); + $dn_array[$key] = $unescaped[0]; + } + } + } + + if ($options['reverse']) { + return array_reverse($dn_array); + } else { + return $dn_array; + } + } + + /** + * Escapes a DN value 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 array $values An array containing the DN values that should be escaped + * + * @static + * @return array The array $values, but escaped + */ + public static function escape_dn_value($values = array()) + { + // Parameter validation + if (!is_array($values)) { + $values = array($values); + } + + foreach ($values as $key => $val) { + // Escaping of filter meta characters + $val = str_replace('\\', '\\\\', $val); + $val = str_replace(',', '\,', $val); + $val = str_replace('+', '\+', $val); + $val = str_replace('"', '\"', $val); + $val = str_replace('<', '\<', $val); + $val = str_replace('>', '\>', $val); + $val = str_replace(';', '\;', $val); + $val = str_replace('#', '\#', $val); + $val = str_replace('=', '\=', $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 = $matches[2]; + for ($i = 0; $i < strlen($matches[1]); $i++) { + $val = '\20'.$val; + } + for ($i = 0; $i < strlen($matches[3]); $i++) { + $val = $val.'\20'; + } + } + + if (null === $val) $val = '\0'; // apply escaped "null" if string is empty + + $values[$key] = $val; + } + + return $values; + } + + /** + * Undoes the conversion done by escape_dn_value(). + * + * Any escape sequence starting with a baskslash - hexpair or special character - + * will be transformed back to the corresponding character. + * + * @param array $values Array of DN Values + * + * @return array Same as $values, but unescaped + * @static + */ + public static function unescape_dn_value($values = array()) + { + // Parameter validation + if (!is_array($values)) { + $values = array($values); + } + + foreach ($values as $key => $val) { + // strip slashes from special chars + $val = str_replace('\\\\', '\\', $val); + $val = str_replace('\,', ',', $val); + $val = str_replace('\+', '+', $val); + $val = str_replace('\"', '"', $val); + $val = str_replace('\<', '<', $val); + $val = str_replace('\>', '>', $val); + $val = str_replace('\;', ';', $val); + $val = str_replace('\#', '#', $val); + $val = str_replace('\=', '=', $val); + + // Translate hex code into ascii + $values[$key] = self::hex2asc($val); + } + + return $values; + } + + /** + * Returns the given DN in a canonical form + * + * Returns false if DN is not a valid Distinguished Name. + * DN can either be a string or an array + * as returned by ldap_explode_dn, 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. This is the default. + * none Do not change attribute type names. + * [NOT IMPLEMENTED] mbcescape If TRUE, characters that are encoded as a multi-octet UTF-8 sequence will be escaped as \(hexpair){2,*}. + * reverse If TRUE, the RDN sequence is reversed. + * separator Separator to use between RDNs. Defaults to comma (','). + * + * Note: 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 + * + * @static + * @return false|string The canonical DN or FALSE + * @todo implement option mbcescape + */ + public static function canonical_dn($dn, $options = array('casefold' => 'upper', 'separator' => ',')) + { + if ($dn === '') return $dn; // empty DN is valid! + + // options check + if (!isset($options['reverse'])) { + $options['reverse'] = false; + } else { + $options['reverse'] = true; + } + if (!isset($options['casefold'])) $options['casefold'] = 'upper'; + if (!isset($options['separator'])) $options['separator'] = ','; + + + 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('/(?<=[^\\\\])'.$options['separator'].'/', $dn); + + // clear wrong splitting (possibly we have split too much) + $dn = self::correct_dn_splitting($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; + } + } + // convert to indexed, if associative array detected + if ($assoc) { + $newdn = array(); + foreach ($dn as $dn_key => $dn_part) { + if (is_array($dn_part)) { + ksort($dn_part, SORT_STRING); // we assume here, that the rdn parts are also associative + $newdn[] = $dn_part; // copy array as-is, so we can resolve it later + } else { + $newdn[] = $dn_key.'='.$dn_part; + } + } + $dn =& $newdn; + } + } + + // Escaping and casefolding + foreach ($dn as $pos => $dnval) { + if (is_array($dnval)) { + // subarray detected, this means very surely, 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::canonical_dn($subval); + if (false === $subval_processed) return false; + $dnval_new .= $subval_processed.'+'; + } + $dn[$pos] = substr($dnval_new, 0, -1); // store RDN part, strip last plus + } else { + // try to split multivalued RDNS into array + $rdns = self::split_rdn_multival($dnval); + if (count($rdns) > 1) { + // Multivalued RDN was detected! + // The RDN value is expected to be correctly split by split_rdn_multival(). + // It's time to sort the RDN and build the DN! + $rdn_string = ''; + sort($rdns, SORT_STRING); // Sort RDN keys alphabetically + foreach ($rdns as $rdn) { + $subval_processed = self::canonical_dn($rdn); + if (false === $subval_processed) return false; + $rdn_string .= $subval_processed.'+'; + } + + $dn[$pos] = substr($rdn_string, 0, -1); // store RDN part, strip last plus + + } else { + // no multivalued RDN! + // split at first unescaped "=" + $dn_comp = preg_split('/(?<=[^\\\\])=/', $rdns[0], 2); + $ocl = ltrim($dn_comp[0]); // trim left whitespaces 'cause of "cn=foo, l=bar" syntax (whitespace after comma) + $val = $dn_comp[1]; + + // strip 'OID.', otherwise apply casefolding and escaping + if (substr(strtolower($ocl), 0, 4) == 'oid.') { + $ocl = substr($ocl, 4); + } else { + if ($options['casefold'] == 'upper') $ocl = strtoupper($ocl); + if ($options['casefold'] == 'lower') $ocl = strtolower($ocl); + $ocl = self::escape_dn_value(array($ocl)); + $ocl = $ocl[0]; + } + + // escaping of dn-value + $val = self::escape_dn_value(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 Array of values to escape + * + * @static + * @return array Array $values, but escaped + */ + public static function escape_filter_value($values = array()) + { + // Parameter validation + if (!is_array($values)) { + $values = array($values); + } + + foreach ($values as $key => $val) { + // Escaping of filter meta characters + $val = str_replace('\\', '\5c', $val); + $val = str_replace('*', '\2a', $val); + $val = str_replace('(', '\28', $val); + $val = str_replace(')', '\29', $val); + + // ASCII < 32 escaping + $val = self::asc2hex32($val); + + if (null === $val) $val = '\0'; // apply escaped "null" if string is empty + + $values[$key] = $val; + } + + return $values; + } + + /** + * Undoes the conversion done by {@link escape_filter_value()}. + * + * Converts any sequences of a backslash followed by two hex digits into the corresponding character. + * + * @param array $values Array of values to escape + * + * @static + * @return array Array $values, but unescaped + */ + public static function unescape_filter_value($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 + * + * @static + * @return string + */ + public static function asc2hex32($string) + { + for ($i = 0; $i < strlen($string); $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 Hex expressions ("\HEX") to their original ASCII characters + * + * @param string $string String to convert + * + * @static + * @author beni@php.net, heavily based on work from DavidSmith@byu.net + * @return string + */ + public static function hex2asc($string) + { + $string = preg_replace("/\\\([0-9A-Fa-f]{2})/e", "''.chr(hexdec('\\1')).''", $string); + return $string; + } + + /** + * Split an multivalued RDN value into an Array + * + * A RDN can contain multiple values, spearated by a plus sign. + * This function 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 trys to be smart if it encounters unescaped "+" characters, but may fail, + * so ensure escaped "+"es in attr names and attr 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 value of the first pair instead as attr name of the second pair. + * To prevent this, escape correctly. + * + * @param string $rdn Part of an (multivalued) escaped RDN (eg. ou=foo OR ou=foo+cn=bar) + * + * @static + * @return array Array with the components of the multivalued RDN or Error + */ + public static function split_rdn_multival($rdn) + { + $rdns = preg_split('/(?<!\\\\)\+/', $rdn); + $rdns = self::correct_dn_splitting($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 Attribute and Value Syntax + * + * @return array Indexed array: 0=attribute name, 1=attribute value + */ + public static function split_attribute_string($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 + * @access protected + */ + protected static function correct_dn_splitting($dn = array(), $separator = ',') + { + foreach ($dn as $key => $dn_value) { + $dn_value = $dn[$key]; // refresh value (foreach caches!) + // if the dn_value is not in attr=value format, then 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. Keep in mind, that UTIL class + // must remain independent from the other classes or connections. + if (!preg_match('/.+(?<!\\\\)=.+/', $dn_value)) { + unset($dn[$key]); + if (array_key_exists($key-1, $dn)) { + $dn[$key-1] = $dn[$key-1].$separator.$dn_value; // append to previous attr value + } else { + $dn[$key+1] = $dn_value.$separator.$dn[$key+1]; // first element: prepend to next attr name + } + } + } + return array_values($dn); + } +} + +?> |