Subversion-Projekte lars-tiefland.php_share

Revision

Details | Letzte Änderung | Log anzeigen | RSS feed

Revision Autor Zeilennr. Zeile
1 lars 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
?>