Blame | Letzte Änderung | Log anzeigen | RSS feed
<?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 casefoldingforeach ($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) {// Casefoldingif ($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// Casefoldingif ($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 validationif (!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 validationif (!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 checkif (!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 detectedif ($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 casefoldingforeach ($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 partif (!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 alphabeticallyforeach ($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 escapingif (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 validationif (!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 validationif (!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);}}?>