]> git.mxchange.org Git - quix0rs-gnu-social.git/blob - extlib/Net/LDAP2/Util.php
Merge branch 'master' of git@gitorious.org:statusnet/mainline
[quix0rs-gnu-social.git] / extlib / Net / LDAP2 / Util.php
1 <?php
2 /* vim: set expandtab tabstop=4 shiftwidth=4: */
3 /**
4 * File containing the Net_LDAP2_Util interface class.
5 *
6 * PHP version 5
7 *
8 * @category  Net
9 * @package   Net_LDAP2
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/
15 */
16
17 /**
18 * Includes
19 */
20 require_once 'PEAR.php';
21
22 /**
23 * Utility Class for Net_LDAP2
24 *
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.
27 *
28 * @category Net
29 * @package  Net_LDAP2
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/
33 */
34 class Net_LDAP2_Util extends PEAR
35 {
36     /**
37      * Constructor
38      *
39      * @access public
40      */
41     public function __construct()
42     {
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.
48     }
49
50     /**
51     * Explodes the given DN into its elements
52     *
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.
56     *
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>
59     *
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.
66     *
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.
72     *
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')
82     *
83
84     * @param string $dn      The DN that should be exploded
85     * @param array  $options Options to use
86     *
87     * @static
88     * @return array   Parts of the exploded DN
89     * @todo implement BER
90     */
91     public static function ldap_explode_dn($dn, $options = array('casefold' => 'upper'))
92     {
93         if (!isset($options['onlyvalues'])) $options['onlyvalues']  = false;
94         if (!isset($options['reverse']))    $options['reverse']     = false;
95         if (!isset($options['casefold']))   $options['casefold']    = 'upper';
96
97         // Escaping of DN and stripping of "OID."
98         $dn = self::canonical_dn($dn, array('casefold' => $options['casefold']));
99
100         // splitting the DN
101         $dn_array = preg_split('/(?<=[^\\\\]),/', $dn);
102
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, ',');
106
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) {
113                 // MV RDN!
114                 foreach ($rdns as $subrdn_k => $subrdn_v) {
115                     // Casefolding
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);
118
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];
125                     } else {
126                         $unescaped = self::unescape_dn_value($subrdn_v);
127                         $rdns[$subrdn_k] = $unescaped[0];
128                     }
129                 }
130
131                 $dn_array[$key] = $rdns;
132             } else {
133                 // normal RDN
134
135                 // Casefolding
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);
138
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];
145                 } else {
146                     $unescaped = self::unescape_dn_value($value);
147                     $dn_array[$key] = $unescaped[0];
148                 }
149             }
150         }
151
152         if ($options['reverse']) {
153             return array_reverse($dn_array);
154         } else {
155             return $dn_array;
156         }
157     }
158
159     /**
160     * Escapes a DN value according to RFC 2253
161     *
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.
166     *
167     * @param array $values An array containing the DN values that should be escaped
168     *
169     * @static
170     * @return array The array $values, but escaped
171     */
172     public static function escape_dn_value($values = array())
173     {
174         // Parameter validation
175         if (!is_array($values)) {
176             $values = array($values);
177         }
178
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);
190
191             // ASCII < 32 escaping
192             $val = self::asc2hex32($val);
193
194             // Convert all leading and trailing spaces to sequences of \20.
195             if (preg_match('/^(\s*)(.+?)(\s*)$/', $val, $matches)) {
196                 $val = $matches[2];
197                 for ($i = 0; $i < strlen($matches[1]); $i++) {
198                     $val = '\20'.$val;
199                 }
200                 for ($i = 0; $i < strlen($matches[3]); $i++) {
201                     $val = $val.'\20';
202                 }
203             }
204
205             if (null === $val) $val = '\0';  // apply escaped "null" if string is empty
206
207             $values[$key] = $val;
208         }
209
210         return $values;
211     }
212
213     /**
214     * Undoes the conversion done by escape_dn_value().
215     *
216     * Any escape sequence starting with a baskslash - hexpair or special character -
217     * will be transformed back to the corresponding character.
218     *
219     * @param array $values Array of DN Values
220     *
221     * @return array Same as $values, but unescaped
222     * @static
223     */
224     public static function unescape_dn_value($values = array())
225     {
226         // Parameter validation
227         if (!is_array($values)) {
228             $values = array($values);
229         }
230
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);
242
243             // Translate hex code into ascii
244             $values[$key] = self::hex2asc($val);
245         }
246
247         return $values;
248     }
249
250     /**
251     * Returns the given DN in a canonical form
252     *
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).
258     *
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.
264     *
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.
268     *                 Valid values are:
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 (',').
275     *
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.
278     *
279     * @param array|string $dn      The DN
280     * @param array        $options Options to use
281     *
282     * @static
283     * @return false|string The canonical DN or FALSE
284     * @todo implement option mbcescape
285     */
286     public static function canonical_dn($dn, $options = array('casefold' => 'upper', 'separator' => ','))
287     {
288         if ($dn === '') return $dn;  // empty DN is valid!
289
290         // options check
291         if (!isset($options['reverse'])) {
292             $options['reverse'] = false;
293         } else {
294             $options['reverse'] = true;
295         }
296         if (!isset($options['casefold']))  $options['casefold'] = 'upper';
297         if (!isset($options['separator'])) $options['separator'] = ',';
298
299
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);
304
305             // clear wrong splitting (possibly we have split too much)
306             $dn = self::correct_dn_splitting($dn, $options['separator']);
307         } else {
308             // Is array, check, if the array is indexed or associative
309             $assoc = false;
310             foreach ($dn as $dn_key => $dn_part) {
311                 if (!is_int($dn_key)) {
312                     $assoc = true;
313                 }
314             }
315             // convert to indexed, if associative array detected
316             if ($assoc) {
317                 $newdn = array();
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
322                     } else {
323                         $newdn[] = $dn_key.'='.$dn_part;
324                     }
325                 }
326                 $dn =& $newdn;
327             }
328         }
329
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
335                 $dnval_new = '';
336                 foreach ($dnval as $subkey => $subval) {
337                     // build RDN part
338                     if (!is_int($subkey)) {
339                         $subval = $subkey.'='.$subval;
340                     }
341                     $subval_processed = self::canonical_dn($subval);
342                     if (false === $subval_processed) return false;
343                     $dnval_new .= $subval_processed.'+';
344                 }
345                 $dn[$pos] = substr($dnval_new, 0, -1); // store RDN part, strip last plus
346             } else {
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!
353                     $rdn_string = '';
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.'+';
359                     }
360
361                     $dn[$pos] = substr($rdn_string, 0, -1); // store RDN part, strip last plus
362
363                 } else {
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)
368                     $val     = $dn_comp[1];
369
370                     // strip 'OID.', otherwise apply casefolding and escaping
371                     if (substr(strtolower($ocl), 0, 4) == 'oid.') {
372                         $ocl = substr($ocl, 4);
373                     } else {
374                         if ($options['casefold'] == 'upper') $ocl = strtoupper($ocl);
375                         if ($options['casefold'] == 'lower') $ocl = strtolower($ocl);
376                         $ocl = self::escape_dn_value(array($ocl));
377                         $ocl = $ocl[0];
378                     }
379
380                     // escaping of dn-value
381                     $val = self::escape_dn_value(array($val));
382                     $val = str_replace('/', '\/', $val[0]);
383
384                     $dn[$pos] = $ocl.'='.$val;
385                 }
386             }
387         }
388
389         if ($options['reverse']) $dn = array_reverse($dn);
390         return implode($options['separator'], $dn);
391     }
392
393     /**
394     * Escapes the given VALUES according to RFC 2254 so that they can be safely used in LDAP filters.
395     *
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.
399     *
400     * @param array $values Array of values to escape
401     *
402     * @static
403     * @return array Array $values, but escaped
404     */
405     public static function escape_filter_value($values = array())
406     {
407         // Parameter validation
408         if (!is_array($values)) {
409             $values = array($values);
410         }
411
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);
418
419             // ASCII < 32 escaping
420             $val = self::asc2hex32($val);
421
422             if (null === $val) $val = '\0';  // apply escaped "null" if string is empty
423
424             $values[$key] = $val;
425         }
426
427         return $values;
428     }
429
430     /**
431     * Undoes the conversion done by {@link escape_filter_value()}.
432     *
433     * Converts any sequences of a backslash followed by two hex digits into the corresponding character.
434     *
435     * @param array $values Array of values to escape
436     *
437     * @static
438     * @return array Array $values, but unescaped
439     */
440     public static function unescape_filter_value($values = array())
441     {
442         // Parameter validation
443         if (!is_array($values)) {
444             $values = array($values);
445         }
446
447         foreach ($values as $key => $value) {
448             // Translate hex code into ascii
449             $values[$key] = self::hex2asc($value);
450         }
451
452         return $values;
453     }
454
455     /**
456     * Converts all ASCII chars < 32 to "\HEX"
457     *
458     * @param string $string String to convert
459     *
460     * @static
461     * @return string
462     */
463     public static function asc2hex32($string)
464     {
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);
471             }
472         }
473         return $string;
474     }
475
476     /**
477     * Converts all Hex expressions ("\HEX") to their original ASCII characters
478     *
479     * @param string $string String to convert
480     *
481     * @static
482     * @author beni@php.net, heavily based on work from DavidSmith@byu.net
483     * @return string
484     */
485     public static function hex2asc($string)
486     {
487         $string = preg_replace("/\\\([0-9A-Fa-f]{2})/e", "''.chr(hexdec('\\1')).''", $string);
488         return $string;
489     }
490
491     /**
492     * Split an multivalued RDN value into an Array
493     *
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.
496     *
497     * If no multivalued RDN is detected, an array containing only
498     * the original rdn part is returned.
499     *
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>
502     *
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.
505     *
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';
510     *       returns:
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.
514     *
515     * @param string $rdn Part of an (multivalued) escaped RDN (eg. ou=foo OR ou=foo+cn=bar)
516     *
517     * @static
518     * @return array Array with the components of the multivalued RDN or Error
519     */
520     public static function split_rdn_multival($rdn)
521     {
522         $rdns = preg_split('/(?<!\\\\)\+/', $rdn);
523         $rdns = self::correct_dn_splitting($rdns, '+');
524         return array_values($rdns);
525     }
526
527     /**
528     * Splits a attribute=value syntax into an array
529     *
530     * The split will occur at the first unescaped '=' character.
531     *
532     * @param string $attr Attribute and Value Syntax
533     *
534     * @return array Indexed array: 0=attribute name, 1=attribute value
535     */
536     public static function split_attribute_string($attr)
537     {
538         return preg_split('/(?<!\\\\)=/', $attr, 2);
539     }
540
541     /**
542     * Corrects splitting of dn parts
543     *
544     * @param array $dn        Raw DN array
545     * @param array $separator Separator that was used when splitting
546     *
547     * @return array Corrected array
548     * @access protected
549     */
550     protected static function correct_dn_splitting($dn = array(), $separator = ',')
551     {
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)) {
560                 unset($dn[$key]);
561                 if (array_key_exists($key-1, $dn)) {
562                     $dn[$key-1] = $dn[$key-1].$separator.$dn_value; // append to previous attr value
563                 } else {
564                     $dn[$key+1] = $dn_value.$separator.$dn[$key+1]; // first element: prepend to next attr name
565                 }
566             }
567         }
568         return array_values($dn);
569     }
570 }
571
572 ?>