2 /* vim: set expandtab tabstop=4 shiftwidth=4: */
4 * File containing the Net_LDAP2_Util interface class.
10 * @author Benedikt Hallinger <beni@php.net>
11 * @copyright 2009 Benedikt Hallinger
12 * @license http://www.gnu.org/licenses/lgpl-3.0.txt LGPLv3
13 * @version SVN: $Id: Util.php 286718 2009-08-03 07:30:49Z beni $
14 * @link http://pear.php.net/package/Net_LDAP2/
20 require_once 'PEAR.php';
23 * Utility Class for Net_LDAP2
25 * This class servers some functionality to the other classes of Net_LDAP2 but most of
26 * the methods can be used separately as well.
30 * @author Benedikt Hallinger <beni@php.net>
31 * @license http://www.gnu.org/copyleft/lesser.html LGPL
32 * @link http://pear.php.net/package/Net_LDAP22/
34 class Net_LDAP2_Util extends PEAR
41 public function __construct()
43 // We do nothing here, since all methods can be called statically.
44 // In Net_LDAP <= 0.7, we needed a instance of Util, because
45 // it was possible to do utf8 encoding and decoding, but this
46 // has been moved to the LDAP class. The constructor remains only
47 // here to document the downward compatibility of creating an instance.
51 * Explodes the given DN into its elements
53 * {@link http://www.ietf.org/rfc/rfc2253.txt RFC 2253} says, a Distinguished Name is a sequence
54 * of Relative Distinguished Names (RDNs), which themselves
55 * are sets of Attributes. For each RDN a array is constructed where the RDN part is stored.
57 * For example, the DN 'OU=Sales+CN=J. Smith,DC=example,DC=net' is exploded to:
58 * <kbd>array( [0] => array([0] => 'OU=Sales', [1] => 'CN=J. Smith'), [2] => 'DC=example', [3] => 'DC=net' )</kbd>
60 * [NOT IMPLEMENTED] DNs might also contain values, which are the bytes of the BER encoding of
61 * the X.500 AttributeValue rather than some LDAP string syntax. These values are hex-encoded
62 * and prefixed with a #. To distinguish such BER values, ldap_explode_dn uses references to
63 * the actual values, e.g. '1.3.6.1.4.1.1466.0=#04024869,DC=example,DC=com' is exploded to:
64 * [ { '1.3.6.1.4.1.1466.0' => "\004\002Hi" }, { 'DC' => 'example' }, { 'DC' => 'com' } ];
65 * See {@link http://www.vijaymukhi.com/vmis/berldap.htm} for more information on BER.
67 * It also performs the following operations on the given DN:
68 * - Unescape "\" followed by ",", "+", """, "\", "<", ">", ";", "#", "=", " ", or a hexpair
69 * and strings beginning with "#".
70 * - Removes the leading 'OID.' characters if the type is an OID instead of a name.
71 * - If an RDN contains multiple parts, the parts are re-ordered so that the attribute type names are in alphabetical order.
73 * OPTIONS is a list of name/value pairs, valid options are:
74 * casefold Controls case folding of attribute types names.
75 * Attribute values are not affected by this option.
76 * The default is to uppercase. Valid values are:
77 * lower Lowercase attribute types names.
78 * upper Uppercase attribute type names. This is the default.
79 * none Do not change attribute type names.
80 * reverse If TRUE, the RDN sequence is reversed.
81 * onlyvalues If TRUE, then only attributes values are returned ('foo' instead of 'cn=foo')
84 * @param string $dn The DN that should be exploded
85 * @param array $options Options to use
88 * @return array Parts of the exploded DN
91 public static function ldap_explode_dn($dn, $options = array('casefold' => 'upper'))
93 if (!isset($options['onlyvalues'])) $options['onlyvalues'] = false;
94 if (!isset($options['reverse'])) $options['reverse'] = false;
95 if (!isset($options['casefold'])) $options['casefold'] = 'upper';
97 // Escaping of DN and stripping of "OID."
98 $dn = self::canonical_dn($dn, array('casefold' => $options['casefold']));
101 $dn_array = preg_split('/(?<=[^\\\\]),/', $dn);
103 // clear wrong splitting (possibly we have split too much)
104 // /!\ Not clear, if this is neccessary here
105 //$dn_array = self::correct_dn_splitting($dn_array, ',');
107 // construct subarrays for multivalued RDNs and unescape DN value
108 // also convert to output format and apply casefolding
109 foreach ($dn_array as $key => $value) {
110 $value_u = self::unescape_dn_value($value);
111 $rdns = self::split_rdn_multival($value_u[0]);
112 if (count($rdns) > 1) {
114 foreach ($rdns as $subrdn_k => $subrdn_v) {
116 if ($options['casefold'] == 'upper') $subrdn_v = preg_replace("/^(\w+=)/e", "''.strtoupper('\\1').''", $subrdn_v);
117 if ($options['casefold'] == 'lower') $subrdn_v = preg_replace("/^(\w+=)/e", "''.strtolower('\\1').''", $subrdn_v);
119 if ($options['onlyvalues']) {
120 preg_match('/(.+?)(?<!\\\\)=(.+)/', $subrdn_v, $matches);
121 $rdn_ocl = $matches[1];
122 $rdn_val = $matches[2];
123 $unescaped = self::unescape_dn_value($rdn_val);
124 $rdns[$subrdn_k] = $unescaped[0];
126 $unescaped = self::unescape_dn_value($subrdn_v);
127 $rdns[$subrdn_k] = $unescaped[0];
131 $dn_array[$key] = $rdns;
136 if ($options['casefold'] == 'upper') $value = preg_replace("/^(\w+=)/e", "''.strtoupper('\\1').''", $value);
137 if ($options['casefold'] == 'lower') $value = preg_replace("/^(\w+=)/e", "''.strtolower('\\1').''", $value);
139 if ($options['onlyvalues']) {
140 preg_match('/(.+?)(?<!\\\\)=(.+)/', $value, $matches);
141 $dn_ocl = $matches[1];
142 $dn_val = $matches[2];
143 $unescaped = self::unescape_dn_value($dn_val);
144 $dn_array[$key] = $unescaped[0];
146 $unescaped = self::unescape_dn_value($value);
147 $dn_array[$key] = $unescaped[0];
152 if ($options['reverse']) {
153 return array_reverse($dn_array);
160 * Escapes a DN value according to RFC 2253
162 * Escapes the given VALUES according to RFC 2253 so that they can be safely used in LDAP DNs.
163 * The characters ",", "+", """, "\", "<", ">", ";", "#", "=" with a special meaning in RFC 2252
164 * are preceeded by ba backslash. Control characters with an ASCII code < 32 are represented as \hexpair.
165 * Finally all leading and trailing spaces are converted to sequences of \20.
167 * @param array $values An array containing the DN values that should be escaped
170 * @return array The array $values, but escaped
172 public static function escape_dn_value($values = array())
174 // Parameter validation
175 if (!is_array($values)) {
176 $values = array($values);
179 foreach ($values as $key => $val) {
180 // Escaping of filter meta characters
181 $val = str_replace('\\', '\\\\', $val);
182 $val = str_replace(',', '\,', $val);
183 $val = str_replace('+', '\+', $val);
184 $val = str_replace('"', '\"', $val);
185 $val = str_replace('<', '\<', $val);
186 $val = str_replace('>', '\>', $val);
187 $val = str_replace(';', '\;', $val);
188 $val = str_replace('#', '\#', $val);
189 $val = str_replace('=', '\=', $val);
191 // ASCII < 32 escaping
192 $val = self::asc2hex32($val);
194 // Convert all leading and trailing spaces to sequences of \20.
195 if (preg_match('/^(\s*)(.+?)(\s*)$/', $val, $matches)) {
197 for ($i = 0; $i < strlen($matches[1]); $i++) {
200 for ($i = 0; $i < strlen($matches[3]); $i++) {
205 if (null === $val) $val = '\0'; // apply escaped "null" if string is empty
207 $values[$key] = $val;
214 * Undoes the conversion done by escape_dn_value().
216 * Any escape sequence starting with a baskslash - hexpair or special character -
217 * will be transformed back to the corresponding character.
219 * @param array $values Array of DN Values
221 * @return array Same as $values, but unescaped
224 public static function unescape_dn_value($values = array())
226 // Parameter validation
227 if (!is_array($values)) {
228 $values = array($values);
231 foreach ($values as $key => $val) {
232 // strip slashes from special chars
233 $val = str_replace('\\\\', '\\', $val);
234 $val = str_replace('\,', ',', $val);
235 $val = str_replace('\+', '+', $val);
236 $val = str_replace('\"', '"', $val);
237 $val = str_replace('\<', '<', $val);
238 $val = str_replace('\>', '>', $val);
239 $val = str_replace('\;', ';', $val);
240 $val = str_replace('\#', '#', $val);
241 $val = str_replace('\=', '=', $val);
243 // Translate hex code into ascii
244 $values[$key] = self::hex2asc($val);
251 * Returns the given DN in a canonical form
253 * Returns false if DN is not a valid Distinguished Name.
254 * DN can either be a string or an array
255 * as returned by ldap_explode_dn, which is useful when constructing a DN.
256 * The DN array may have be indexed (each array value is a OCL=VALUE pair)
257 * or associative (array key is OCL and value is VALUE).
259 * It performs the following operations on the given DN:
260 * - Removes the leading 'OID.' characters if the type is an OID instead of a name.
261 * - Escapes all RFC 2253 special characters (",", "+", """, "\", "<", ">", ";", "#", "="), slashes ("/"), and any other character where the ASCII code is < 32 as \hexpair.
262 * - Converts all leading and trailing spaces in values to be \20.
263 * - If an RDN contains multiple parts, the parts are re-ordered so that the attribute type names are in alphabetical order.
265 * OPTIONS is a list of name/value pairs, valid options are:
266 * casefold Controls case folding of attribute type names.
267 * Attribute values are not affected by this option. The default is to uppercase.
269 * lower Lowercase attribute type names.
270 * upper Uppercase attribute type names. This is the default.
271 * none Do not change attribute type names.
272 * [NOT IMPLEMENTED] mbcescape If TRUE, characters that are encoded as a multi-octet UTF-8 sequence will be escaped as \(hexpair){2,*}.
273 * reverse If TRUE, the RDN sequence is reversed.
274 * separator Separator to use between RDNs. Defaults to comma (',').
276 * Note: The empty string "" is a valid DN, so be sure not to do a "$can_dn == false" test,
277 * because an empty string evaluates to false. Use the "===" operator instead.
279 * @param array|string $dn The DN
280 * @param array $options Options to use
283 * @return false|string The canonical DN or FALSE
284 * @todo implement option mbcescape
286 public static function canonical_dn($dn, $options = array('casefold' => 'upper', 'separator' => ','))
288 if ($dn === '') return $dn; // empty DN is valid!
291 if (!isset($options['reverse'])) {
292 $options['reverse'] = false;
294 $options['reverse'] = true;
296 if (!isset($options['casefold'])) $options['casefold'] = 'upper';
297 if (!isset($options['separator'])) $options['separator'] = ',';
300 if (!is_array($dn)) {
301 // It is not clear to me if the perl implementation splits by the user defined
302 // separator or if it just uses this separator to construct the new DN
303 $dn = preg_split('/(?<=[^\\\\])'.$options['separator'].'/', $dn);
305 // clear wrong splitting (possibly we have split too much)
306 $dn = self::correct_dn_splitting($dn, $options['separator']);
308 // Is array, check, if the array is indexed or associative
310 foreach ($dn as $dn_key => $dn_part) {
311 if (!is_int($dn_key)) {
315 // convert to indexed, if associative array detected
318 foreach ($dn as $dn_key => $dn_part) {
319 if (is_array($dn_part)) {
320 ksort($dn_part, SORT_STRING); // we assume here, that the rdn parts are also associative
321 $newdn[] = $dn_part; // copy array as-is, so we can resolve it later
323 $newdn[] = $dn_key.'='.$dn_part;
330 // Escaping and casefolding
331 foreach ($dn as $pos => $dnval) {
332 if (is_array($dnval)) {
333 // subarray detected, this means very surely, that we had
334 // a multivalued dn part, which must be resolved
336 foreach ($dnval as $subkey => $subval) {
338 if (!is_int($subkey)) {
339 $subval = $subkey.'='.$subval;
341 $subval_processed = self::canonical_dn($subval);
342 if (false === $subval_processed) return false;
343 $dnval_new .= $subval_processed.'+';
345 $dn[$pos] = substr($dnval_new, 0, -1); // store RDN part, strip last plus
347 // try to split multivalued RDNS into array
348 $rdns = self::split_rdn_multival($dnval);
349 if (count($rdns) > 1) {
350 // Multivalued RDN was detected!
351 // The RDN value is expected to be correctly split by split_rdn_multival().
352 // It's time to sort the RDN and build the DN!
354 sort($rdns, SORT_STRING); // Sort RDN keys alphabetically
355 foreach ($rdns as $rdn) {
356 $subval_processed = self::canonical_dn($rdn);
357 if (false === $subval_processed) return false;
358 $rdn_string .= $subval_processed.'+';
361 $dn[$pos] = substr($rdn_string, 0, -1); // store RDN part, strip last plus
364 // no multivalued RDN!
365 // split at first unescaped "="
366 $dn_comp = preg_split('/(?<=[^\\\\])=/', $rdns[0], 2);
367 $ocl = ltrim($dn_comp[0]); // trim left whitespaces 'cause of "cn=foo, l=bar" syntax (whitespace after comma)
370 // strip 'OID.', otherwise apply casefolding and escaping
371 if (substr(strtolower($ocl), 0, 4) == 'oid.') {
372 $ocl = substr($ocl, 4);
374 if ($options['casefold'] == 'upper') $ocl = strtoupper($ocl);
375 if ($options['casefold'] == 'lower') $ocl = strtolower($ocl);
376 $ocl = self::escape_dn_value(array($ocl));
380 // escaping of dn-value
381 $val = self::escape_dn_value(array($val));
382 $val = str_replace('/', '\/', $val[0]);
384 $dn[$pos] = $ocl.'='.$val;
389 if ($options['reverse']) $dn = array_reverse($dn);
390 return implode($options['separator'], $dn);
394 * Escapes the given VALUES according to RFC 2254 so that they can be safely used in LDAP filters.
396 * Any control characters with an ACII code < 32 as well as the characters with special meaning in
397 * LDAP filters "*", "(", ")", and "\" (the backslash) are converted into the representation of a
398 * backslash followed by two hex digits representing the hexadecimal value of the character.
400 * @param array $values Array of values to escape
403 * @return array Array $values, but escaped
405 public static function escape_filter_value($values = array())
407 // Parameter validation
408 if (!is_array($values)) {
409 $values = array($values);
412 foreach ($values as $key => $val) {
413 // Escaping of filter meta characters
414 $val = str_replace('\\', '\5c', $val);
415 $val = str_replace('*', '\2a', $val);
416 $val = str_replace('(', '\28', $val);
417 $val = str_replace(')', '\29', $val);
419 // ASCII < 32 escaping
420 $val = self::asc2hex32($val);
422 if (null === $val) $val = '\0'; // apply escaped "null" if string is empty
424 $values[$key] = $val;
431 * Undoes the conversion done by {@link escape_filter_value()}.
433 * Converts any sequences of a backslash followed by two hex digits into the corresponding character.
435 * @param array $values Array of values to escape
438 * @return array Array $values, but unescaped
440 public static function unescape_filter_value($values = array())
442 // Parameter validation
443 if (!is_array($values)) {
444 $values = array($values);
447 foreach ($values as $key => $value) {
448 // Translate hex code into ascii
449 $values[$key] = self::hex2asc($value);
456 * Converts all ASCII chars < 32 to "\HEX"
458 * @param string $string String to convert
463 public static function asc2hex32($string)
465 for ($i = 0; $i < strlen($string); $i++) {
466 $char = substr($string, $i, 1);
467 if (ord($char) < 32) {
468 $hex = dechex(ord($char));
469 if (strlen($hex) == 1) $hex = '0'.$hex;
470 $string = str_replace($char, '\\'.$hex, $string);
477 * Converts all Hex expressions ("\HEX") to their original ASCII characters
479 * @param string $string String to convert
482 * @author beni@php.net, heavily based on work from DavidSmith@byu.net
485 public static function hex2asc($string)
487 $string = preg_replace("/\\\([0-9A-Fa-f]{2})/e", "''.chr(hexdec('\\1')).''", $string);
492 * Split an multivalued RDN value into an Array
494 * A RDN can contain multiple values, spearated by a plus sign.
495 * This function returns each separate ocl=value pair of the RDN part.
497 * If no multivalued RDN is detected, an array containing only
498 * the original rdn part is returned.
500 * For example, the multivalued RDN 'OU=Sales+CN=J. Smith' is exploded to:
501 * <kbd>array([0] => 'OU=Sales', [1] => 'CN=J. Smith')</kbd>
503 * The method trys to be smart if it encounters unescaped "+" characters, but may fail,
504 * so ensure escaped "+"es in attr names and attr values.
506 * [BUG] If you have a multivalued RDN with unescaped plus characters
507 * and there is a unescaped plus sign at the end of an value followed by an
508 * attribute name containing an unescaped plus, then you will get wrong splitting:
509 * $rdn = 'OU=Sales+C+N=J. Smith';
511 * array('OU=Sales+C', 'N=J. Smith');
512 * The "C+" is treaten as value of the first pair instead as attr name of the second pair.
513 * To prevent this, escape correctly.
515 * @param string $rdn Part of an (multivalued) escaped RDN (eg. ou=foo OR ou=foo+cn=bar)
518 * @return array Array with the components of the multivalued RDN or Error
520 public static function split_rdn_multival($rdn)
522 $rdns = preg_split('/(?<!\\\\)\+/', $rdn);
523 $rdns = self::correct_dn_splitting($rdns, '+');
524 return array_values($rdns);
528 * Splits a attribute=value syntax into an array
530 * The split will occur at the first unescaped '=' character.
532 * @param string $attr Attribute and Value Syntax
534 * @return array Indexed array: 0=attribute name, 1=attribute value
536 public static function split_attribute_string($attr)
538 return preg_split('/(?<!\\\\)=/', $attr, 2);
542 * Corrects splitting of dn parts
544 * @param array $dn Raw DN array
545 * @param array $separator Separator that was used when splitting
547 * @return array Corrected array
550 protected static function correct_dn_splitting($dn = array(), $separator = ',')
552 foreach ($dn as $key => $dn_value) {
553 $dn_value = $dn[$key]; // refresh value (foreach caches!)
554 // if the dn_value is not in attr=value format, then we had an
555 // unescaped separator character inside the attr name or the value.
556 // We assume, that it was the attribute value.
557 // [TODO] To solve this, we might ask the schema. Keep in mind, that UTIL class
558 // must remain independent from the other classes or connections.
559 if (!preg_match('/.+(?<!\\\\)=.+/', $dn_value)) {
561 if (array_key_exists($key-1, $dn)) {
562 $dn[$key-1] = $dn[$key-1].$separator.$dn_value; // append to previous attr value
564 $dn[$key+1] = $dn_value.$separator.$dn[$key+1]; // first element: prepend to next attr name
568 return array_values($dn);