Completed
Push — master ( f0fd1d...defc9d )
by Christopher
40:01 queued 02:21
created

Utilities::unescapeValue()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 8
Code Lines 4

Duplication

Lines 8
Ratio 100 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
c 1
b 0
f 0
dl 8
loc 8
rs 9.4285
cc 1
eloc 4
nc 1
nop 1
1
<?php
2
/**
3
 * @link      https://github.com/chrmorandi/yii2-ldap for the canonical source repository
4
 * @package   yii2-ldap
5
 * @author    Christopher Mota <[email protected]>
6
 * @license   MIT License - view the LICENSE file that was distributed with this source code.
7
 */
8
9
namespace chrmorandi\ldap;
10
11
use LdapTools\Exception\InvalidArgumentException;
12
13
/**
14
 * 
15
 */
16
class Utilities
17
{
18
    /**
19
     * Converts a DN string into an array of RDNs.
20
     *
21
     * This will also decode hex characters into their true
22
     * UTF-8 representation embedded inside the DN as well.
23
     *
24
     * @param string $dn
25
     * @param bool   $removeAttributePrefixes
26
     *
27
     * @return array
28
     */
29
    public static function explodeDn($dn, $removeAttributePrefixes = true)
30
    {
31
        $dn = ldap_explode_dn($dn, ($removeAttributePrefixes ? 1 : 0));
32
33
        if (is_array($dn) && array_key_exists('count', $dn)) {
34
            foreach ($dn as $rdn => $value) {
35
                $dn[$rdn] = self::unescape($value);
36
            }
37
        }
38
39
        return $dn;
40
    }
41
42
    /**
43
     * Returns true / false if the current
44
     * PHP install supports escaping values.
45
     *
46
     * @return bool
47
     */
48
    public static function isEscapingSupported()
49
    {
50
        return function_exists('ldap_escape');
51
    }
52
53
    /**
54
     * Returns an escaped string for use in an LDAP filter.
55
     *
56
     * @param string $value
57
     * @param string $ignore
58
     * @param $flags
59
     *
60
     * @return string
61
     */
62
    public static function escape($value, $ignore = '', $flags = 0)
63
    {
64
        if (!static::isEscapingSupported()) {
65
            return static::escapeManual($value, $ignore, $flags);
66
        }
67
68
        return ldap_escape($value, $ignore, $flags);
69
    }
70
71
    /**
72
     * Escapes the inserted value for LDAP.
73
     *
74
     * @param string $value
75
     * @param string $ignore
76
     * @param int    $flags
77
     *
78
     * @return string
79
     */
80
    protected static function escapeManual($value, $ignore = '', $flags = 0)
81
    {
82
        // If a flag was supplied, we'll send the value off
83
        // to be escaped using the PHP flag values
84
        // and return the result.
85
        if ($flags) {
86
            return static::escapeManualWithFlags($value, $ignore, $flags);
87
        }
88
89
        // Convert ignore string into an array.
90
        $ignores = static::ignoreStrToArray($ignore);
91
92
        // Convert the value to a hex string.
93
        $hex = bin2hex($value);
94
95
        // Separate the string, with the hex length of 2, and
96
        // place a backslash on the end of each section.
97
        $value = chunk_split($hex, 2, '\\');
98
99
        // We'll append a backslash at the front of the string
100
        // and remove the ending backslash of the string.
101
        $value = '\\'.substr($value, 0, -1);
102
103
        // Go through each character to ignore.
104
        foreach ($ignores as $charToIgnore) {
105
            // Convert the character to ignore to a hex.
106
            $hexed = bin2hex($charToIgnore);
107
108
            // Replace the hexed variant with the original character.
109
            $value = str_replace('\\'.$hexed, $charToIgnore, $value);
110
        }
111
112
        // Finally we can return the escaped value.
113
        return $value;
114
    }
115
116
    /**
117
     * Escapes the inserted value with flags. Supplying either 1
118
     * or 2 into the flags parameter will escape only certain values.
119
     *
120
     *
121
     * @param string $value
122
     * @param string $ignore
123
     * @param int    $flags
124
     *
125
     * @return string
126
     */
127
    protected static function escapeManualWithFlags($value, $ignore = '', $flags = 0)
128
    {
129
        // Convert ignore string into an array
130
        $ignores = static::ignoreStrToArray($ignore);
131
132
        // The escape characters for search filters
133
        $escapeFilter = ['\\', '*', '(', ')'];
134
135
        // The escape characters for distinguished names
136
        $escapeDn = ['\\', ',', '=', '+', '<', '>', ';', '"', '#'];
137
138
        switch ($flags) {
139
            case 1:
140
                // Int 1 equals to LDAP_ESCAPE_FILTER
141
                $escapes = $escapeFilter;
142
                break;
143
            case 2:
144
                // Int 2 equals to LDAP_ESCAPE_DN
145
                $escapes = $escapeDn;
146
                break;
147
            case 3:
148
                // If both LDAP_ESCAPE_FILTER and LDAP_ESCAPE_DN are used
149
                $escapes = array_unique(array_merge($escapeDn, $escapeFilter));
150
                break;
151
            default:
152
                // We've been given an invalid flag, we'll escape everything to be safe.
153
                return static::escapeManual($value, $ignore);
154
        }
155
156
        foreach ($escapes as $escape) {
157
            // Make sure the escaped value isn't being ignored.
158
            if (!in_array($escape, $ignores)) {
159
                $hexed = static::escape($escape);
160
161
                $value = str_replace($escape, $hexed, $value);
162
            }
163
        }
164
165
        return $value;
166
    }
167
168
    /**
169
     * Un-escapes a hexadecimal string into
170
     * its original string representation.
171
     *
172
     * @param string $value
173
     *
174
     * @return string
175
     */
176 View Code Duplication
    public static function unescape($value)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
177
    {
178
        $callback = function ($matches) {
179
            return chr(hexdec($matches[1]));
180
        };
181
182
        return preg_replace_callback('/\\\([0-9A-Fa-f]{2})/', $callback, $value);
183
    }
184
185
    /**
186
     * Convert a binary SID to a string SID.
187
     *
188
     * @param string $binSid A Binary SID
189
     *
190
     * @return string
191
     */
192
    public static function binarySidToString($binSid)
193
    {
194
        if (trim($binSid) == '' || is_null($binSid)) {
195
            return;
196
        }
197
198
        $hex = bin2hex($binSid);
199
200
        $rev = hexdec(substr($hex, 0, 2));
201
202
        $subCount = hexdec(substr($hex, 2, 2));
203
204
        $auth = hexdec(substr($hex, 4, 12));
205
206
        $result = "$rev-$auth";
207
208
        $subauth = [];
209
210
        for ($x = 0; $x < $subCount; $x++) {
211
            $subauth[$x] = hexdec(static::littleEndian(substr($hex, 16 + ($x * 8), 8)));
212
213
            $result .= '-'.$subauth[$x];
214
        }
215
216
        return 'S-'.$result;
217
    }
218
219
    /**
220
     * Convert a binary GUID to a string GUID.
221
     *
222
     * @param string $binGuid
223
     *
224
     * @return string
225
     */
226
    public static function binaryGuidToString($binGuid)
227
    {
228
        if (trim($binGuid) == '' || is_null($binGuid)) {
229
            return;
230
        }
231
232
        $hex = unpack('H*hex', $binGuid)['hex'];
233
234
        $hex1 = substr($hex, -26, 2).substr($hex, -28, 2).substr($hex, -30, 2).substr($hex, -32, 2);
235
        $hex2 = substr($hex, -22, 2).substr($hex, -24, 2);
236
        $hex3 = substr($hex, -18, 2).substr($hex, -20, 2);
237
        $hex4 = substr($hex, -16, 4);
238
        $hex5 = substr($hex, -12, 12);
239
240
        $guid = sprintf('%s-%s-%s-%s-%s', $hex1, $hex2, $hex3, $hex4, $hex5);
241
242
        return $guid;
243
    }
244
245
    /**
246
     * Converts a little-endian hex number to one that hexdec() can convert.
247
     *
248
     * @param string $hex A hex code
249
     *
250
     * @return string
251
     */
252
    public static function littleEndian($hex)
253
    {
254
        $result = '';
255
256
        for ($x = strlen($hex) - 2; $x >= 0; $x = $x - 2) {
257
            $result .= substr($hex, $x, 2);
258
        }
259
260
        return $result;
261
    }
262
263
    /**
264
     * Encode a password for transmission over LDAP.
265
     *
266
     * @param string $password The password to encode
267
     *
268
     * @return string
269
     */
270
    public static function encodePassword($password)
271
    {
272
        return iconv('UTF-8', 'UTF-16LE', '"'.$password.'"');
273
    }
274
275
    /**
276
     * Round a Windows timestamp down to seconds and remove
277
     * the seconds between 1601-01-01 and 1970-01-01.
278
     *
279
     * @param float $windowsTime
280
     *
281
     * @return float
282
     */
283
    public static function convertWindowsTimeToUnixTime($windowsTime)
284
    {
285
        return round($windowsTime / 10000000) - 11644473600;
286
    }
287
288
    /**
289
     * Convert a Unix timestamp to Windows timestamp.
290
     *
291
     * @param float $unixTime
292
     *
293
     * @return float
294
     */
295
    public static function convertUnixTimeToWindowsTime($unixTime)
296
    {
297
        return ($unixTime + 11644473600) * 10000000;
298
    }
299
300
    /**
301
     * Validates that the inserted string is an object SID.
302
     *
303
     * @param string $sid
304
     *
305
     * @return bool
306
     */
307
    public static function isValidSid($sid)
308
    {
309
        preg_match("/S-1-5-21-\d+-\d+\-\d+\-\d+/", $sid, $matches);
310
311
        if (count($matches) > 0) {
312
            return true;
313
        }
314
315
        return false;
316
    }
317
318
    /**
319
     * Converts an ignore string into an array.
320
     *
321
     * @param string $ignore
322
     *
323
     * @return array
324
     */
325
    protected static function ignoreStrToArray($ignore)
326
    {
327
        $ignore = trim($ignore);
328
329
        if (!empty($ignore)) {
330
            return str_split($ignore);
331
        }
332
333
        return [];
334
    }
335
    
336
     /**
337
     * Regex to match a GUID.
338
     */
339
    const MATCH_GUID = '/^([0-9a-fA-F]){8}(-([0-9a-fA-F]){4}){3}-([0-9a-fA-F]){12}$/';
340
341
    /**
342
     * Regex to match a Windows SID.
343
     */
344
    const MATCH_SID = '/^S-\d-(\d+-){1,14}\d+$/i';
345
346
    /**
347
     * Regex to match an OID.
348
     */
349
    const MATCH_OID = '/^[0-9]+(\.[0-9]+?)*?$/';
350
351
    /**
352
     * Regex to match an attribute descriptor.
353
     */
354
    const MATCH_DESCRIPTOR = '/^\pL[\pL\pN-]+$/iu';
355
356
    /**
357
     * The prefix for a LDAP DNS SRV record.
358
     */
359
    const SRV_PREFIX = '_ldap._tcp.';
360
361
    /**
362
     * The mask to use when sanitizing arrays with LDAP password information.
363
     */
364
    const MASK = '******';
365
366
    /**
367
     * The attributes to mask in a batch/attribute array.
368
     */
369
    const MASK_ATTRIBUTES = [
370
        'unicodepwd',
371
        'userpassword',
372
    ];
373
374
    /**
375
     * Escape any special characters for LDAP to their hexadecimal representation.
376
     *
377
     * @param mixed $value The value to escape.
378
     * @param null|string $ignore The characters to ignore.
379
     * @param null|int $flags The context for the escaped string. LDAP_ESCAPE_FILTER or LDAP_ESCAPE_DN.
380
     * @return string The escaped value.
381
     */
382
    public static function escapeValue($value, $ignore = null, $flags = null)
383
    {
384
        // If this is a hexadecimal escaped string, then do not escape it.
385
        $value = preg_match('/^(\\\[0-9a-fA-F]{2})+$/', (string) $value) ? $value : ldap_escape($value, $ignore, $flags);
386
387
        // Per RFC 4514, leading/trailing spaces should be encoded in DNs, as well as carriage returns.
388
        if ((int)$flags & LDAP_ESCAPE_DN) {
389 View Code Duplication
            if (!empty($value) && $value[0] === ' ') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
390
                $value = '\\20' . substr($value, 1);
391
            }
392 View Code Duplication
            if (!empty($value) && $value[strlen($value) - 1] === ' ') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
393
                $value = substr($value, 0, -1) . '\\20';
394
            }
395
            // Only carriage returns seem to be valid, not line feeds (per testing of AD anyway).
396
            $value = str_replace("\r", '\0d', $value);
397
        }
398
399
        return $value;
400
    }
401
402
    /**
403
     * Un-escapes a value from its hexadecimal form back to its string representation.
404
     *
405
     * @param string $value
406
     * @return string
407
     */
408 View Code Duplication
    public static function unescapeValue($value)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
409
    {
410
        $callback = function ($matches) {
411
            return chr(hexdec($matches[1]));
412
        };
413
414
        return preg_replace_callback('/\\\([0-9A-Fa-f]{2})/', $callback, $value);
415
    }
416
417
    /**
418
     * Converts a string distinguished name into its separate pieces.
419
     *
420
     * @param string $dn
421
     * @param int $withAttributes Set to 0 to get the attribute names along with the value.
422
     * @return array
423
     */
424
    public static function explodeDn($dn, $withAttributes = 1)
425
    {
426
        $pieces = ldap_explode_dn($dn, $withAttributes);
427
428
        if ($pieces === false || !isset($pieces['count']) || $pieces['count'] == 0) {
429
            throw new InvalidArgumentException(sprintf('Unable to parse DN "%s".', $dn));
430
        }
431
        for ($i = 0; $i < $pieces['count']; $i++) {
432
            $pieces[$i] = self::unescapeValue($pieces[$i]);
433
        }
434
        unset($pieces['count']);
435
436
        return $pieces;
437
    }
438
439
    /**
440
     * Given a DN as an array in ['cn=Name', 'ou=Employees', 'dc=example', 'dc=com'] form, return it as its string
441
     * representation that is safe to pass back to a query or to save back to LDAP for a DN.
442
     *
443
     * @param array $dn
444
     * @return string
445
     */
446
    public static function implodeDn(array $dn)
447
    {
448
        foreach ($dn as $index => $piece) {
449
            $values = explode('=', $piece, 2);
450
            if (count($values) === 1) {
451
                throw new InvalidArgumentException(sprintf('Unable to parse DN piece "%s".', $values[0]));
452
            }
453
            $dn[$index] = $values[0].'='.self::escapeValue($values[1], null, LDAP_ESCAPE_DN);
454
        }
455
456
        return implode(',', $dn);
457
    }
458
459
    /**
460
     * Encode a string for LDAP with a specific encoding type.
461
     *
462
     * @param string $value The value to encode.
463
     * @param string $toEncoding The encoding type to use (ie. UTF-8)
464
     * @return string The encoded value.
465
     */
466
    public static function encode($value, $toEncoding)
467
    {
468
        // If the encoding is already UTF-8, and that's what was requested, then just send the value back.
469
        if ($toEncoding == 'UTF-8' && preg_match('//u', $value)) {
470
            return $value;
471
        }
472
473
        if (function_exists('mb_detect_encoding')) {
474
            $value = iconv(mb_detect_encoding($value, mb_detect_order(), true), $toEncoding, $value);
475
        } else {
476
            // How else to better handle if they don't have mb_* ? The below is definitely not an optimal solution.
477
            $value = utf8_encode($value);
478
        }
479
480
        return $value;
481
    }
482
483
    /**
484
     * Given a string, try to determine if it is a valid distinguished name for a LDAP object. This is a somewhat
485
     * unsophisticated approach. A regex might be a better solution, but would probably be rather difficult to get
486
     * right.
487
     *
488
     * @param string $dn
489
     * @return bool
490
     */
491
    public static function isValidLdapObjectDn($dn)
492
    {
493
        return (($pieces = ldap_explode_dn($dn, 1)) && isset($pieces['count']) && $pieces['count'] > 2);
494
    }
495
496
    /**
497
     * Determine whether a value is a valid attribute name or OID. The name should meet the format described in RFC 2252.
498
     * However, the regex is fairly forgiving for each.
499
     *
500
     * @param string $value
501
     * @return bool
502
     */
503
    public static function isValidAttributeFormat($value)
504
    {
505
        return (preg_match(self::MATCH_DESCRIPTOR, $value) || preg_match(self::MATCH_OID, $value));
506
    }
507
508
    /**
509
     * Attempts to mask passwords in a LDAP batch array while keeping the rest intact.
510
     *
511
     * @param array $batch
512
     * @return array
513
     */
514
    public static function maskBatchArray(array $batch)
515
    {
516
        foreach ($batch as $i => $batchItem) {
517
            if (!isset($batchItem['attrib']) || !isset($batchItem['values'])) {
518
                continue;
519
            }
520
            if (!in_array(strtolower($batchItem['attrib']), self::MASK_ATTRIBUTES)) {
521
                continue;
522
            }
523
            $batch[$i]['values'] = [self::MASK];
524
        }
525
526
        return $batch;
527
    }
528
529
    /**
530
     * Attempts to mask password attribute values used in logging.
531
     *
532
     * @param array $attributes
533
     * @return array
534
     */
535
    public static function maskAttributeArray(array $attributes)
536
    {
537
        foreach ($attributes as $key => $value) {
538
            if (in_array(strtolower($key), self::MASK_ATTRIBUTES)) {
539
                $attributes[$key] = self::MASK;
540
            }
541
        }
542
543
        return $attributes;
544
    }
545
546
    /**
547
     * Get an array of all the LDAP servers for a domain by querying DNS.
548
     *
549
     * @param string $domain The domain name to query.
550
     * @return string[]
551
     */
552
    public static function getLdapServersForDomain($domain)
553
    {
554
        $hosts = (new Dns())->getRecord(self::SRV_PREFIX.$domain, DNS_SRV);
555
556
        return is_array($hosts) ? array_column($hosts, 'target') : [];
557
    }
558
559
    /**
560
     * Given a full escaped DN return the RDN in escaped form.
561
     *
562
     * @param string $dn
563
     * @return string
564
     */
565
    public static function getRdnFromDn($dn)
566
    {
567
        $rdn = self::explodeDn($dn, 0)[0];
568
        $rdn = explode('=', $rdn, 2);
569
570
        return $rdn[0].'='.self::escapeValue($rdn[1], null, LDAP_ESCAPE_DN);
571
    }
572
573
    /**
574
     * Given an attribute, split it between its alias and attribute. This will return an array where the first value
575
     * is the alias and the second is the attribute name. If there is no alias then the first value will be null.
576
     * 
577
     * ie. list($alias, $attribute) = LdapUtilities::getAliasAndAttribute($attribute);
578
     * 
579
     * @param string $attribute
580
     * @return array
581
     */
582
    public static function getAliasAndAttribute($attribute)
583
    {
584
        $alias = null;
585
586
        if (strpos($attribute, '.') !== false) {
587
            $pieces = explode('.', $attribute, 2);
588
            $alias = $pieces[0];
589
            $attribute = $pieces[1];
590
        }
591
        
592
        return [$alias, $attribute];
593
    }
594
595
    /**
596
     * Looks up an array value in a case-insensitive way and return it how it appears in the array.
597
     *
598
     * @param string $needle
599
     * @param array $haystack
600
     * @return string
601
     */
602
    public static function getValueCaseInsensitive($needle, array $haystack)
603
    {
604
        $lcNeedle = strtolower($needle);
605
        $lcKeys = array_change_key_case(array_flip($haystack));
606
        
607
        if (!isset($lcKeys[$lcNeedle])) {
608
            throw new InvalidArgumentException(sprintf('Value "%s" not found in array.', $needle));
609
        }
610
611
        return $haystack[$lcKeys[$lcNeedle]];
612
    }
613
}
614