Passed
Push — master ( f35c18...d8ff86 )
by Tim
02:30
created

Ldap   F

Complexity

Total Complexity 103

Size/Duplication

Total Lines 826
Duplicated Lines 0 %

Importance

Changes 7
Bugs 1 Features 0
Metric Value
eloc 311
c 7
b 1
f 0
dl 0
loc 826
rs 2
wmc 103

13 Methods

Rating   Name   Duplication   Size   Complexity  
B search() 0 80 11
A searchfordn() 0 34 5
C makeException() 0 51 14
C __construct() 0 75 12
A whoami() 0 13 3
F searchformultiple() 0 103 21
B getAttributes() 0 70 10
B bind() 0 53 9
A authzidToDn() 0 14 3
A asc2hex32() 0 13 4
A validate() 0 37 5
A escapeFilterValue() 0 25 4
A setOption() 0 15 2

How to fix   Complexity   

Complex Class

Complex classes like Ldap often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use Ldap, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/**
4
 * The LDAP class holds helper functions to access an LDAP database.
5
 *
6
 * @author Andreas Aakre Solberg, UNINETT AS. <[email protected]>
7
 * @author Anders Lund, UNINETT AS. <[email protected]>
8
 * @package SimpleSAMLphp
9
 */
10
11
namespace SimpleSAML\Module\ldap\Auth;
12
13
use SimpleSAML\Error;
14
use SimpleSAML\Logger;
15
use SimpleSAML\Utils;
16
use Webmozart\Assert\Assert;
17
18
/**
19
 * Constants defining possible errors
20
 */
21
define('ERR_INTERNAL', 1);
22
define('ERR_NO_USER', 2);
23
define('ERR_WRONG_PW', 3);
24
define('ERR_AS_DATA_INCONSIST', 4);
25
define('ERR_AS_INTERNAL', 5);
26
define('ERR_AS_ATTRIBUTE', 6);
27
28
// not defined in earlier PHP versions
29
if (!defined('LDAP_OPT_DIAGNOSTIC_MESSAGE')) {
30
    define('LDAP_OPT_DIAGNOSTIC_MESSAGE', 0x0032);
31
}
32
33
class Ldap
34
{
35
    /**
36
     * LDAP link identifier.
37
     *
38
     * @var resource
39
     */
40
    protected $ldap;
41
42
    /**
43
     * LDAP user: authz_id if SASL is in use, binding dn otherwise
44
     *
45
     * @var string|null
46
     */
47
    protected $authz_id = null;
48
49
    /**
50
     * Timeout value, in seconds.
51
     *
52
     * @var int
53
     */
54
    protected $timeout = 0;
55
56
    /**
57
     * Private constructor restricts instantiation to getInstance().
58
     *
59
     * @param string $hostname
60
     * @param bool $enable_tls
61
     * @param bool $debug
62
     * @param int $timeout
63
     * @param int $port
64
     * @param bool $referrals
65
     * @psalm-suppress NullArgument
66
     */
67
    public function __construct(
68
        string $hostname,
69
        bool $enable_tls = true,
70
        bool $debug = false,
71
        int $timeout = 0,
72
        int $port = 389,
73
        bool $referrals = true
74
    ) {
75
        // Debug
76
        Logger::debug('Library - LDAP __construct(): Setup LDAP with ' .
77
            'host=\'' . $hostname .
78
            '\', tls=' . var_export($enable_tls, true) .
79
            ', debug=' . var_export($debug, true) .
80
            ', timeout=' . var_export($timeout, true) .
81
            ', referrals=' . var_export($referrals, true));
82
83
        /*
84
         * Set debug level before calling connect. Note that this passes
85
         * NULL to ldap_set_option, which is an undocumented feature.
86
         *
87
         * OpenLDAP 2.x.x or Netscape Directory SDK x.x needed for this option.
88
         */
89
        if ($debug && !ldap_set_option(null, LDAP_OPT_DEBUG_LEVEL, 7)) {
90
            Logger::warning('Library - LDAP __construct(): Unable to set debug level (LDAP_OPT_DEBUG_LEVEL) to 7');
91
        }
92
93
        /*
94
         * Prepare a connection for to this LDAP server. Note that this function
95
         * doesn't actually connect to the server.
96
         */
97
        $resource = @ldap_connect($hostname, $port);
98
        if ($resource === false) {
99
            throw $this->makeException(
100
                'Library - LDAP __construct(): Unable to connect to \'' . $hostname . '\'',
101
                ERR_INTERNAL
102
            );
103
        }
104
        $this->ldap = $resource;
105
106
        // Enable LDAP protocol version 3
107
        if (!@ldap_set_option($this->ldap, LDAP_OPT_PROTOCOL_VERSION, 3)) {
108
            throw $this->makeException(
109
                'Library - LDAP __construct(): Failed to set LDAP Protocol version (LDAP_OPT_PROTOCOL_VERSION) to 3',
110
                ERR_INTERNAL
111
            );
112
        }
113
114
        // Set referral option
115
        if (!@ldap_set_option($this->ldap, LDAP_OPT_REFERRALS, $referrals)) {
116
            throw $this->makeException(
117
                'Library - LDAP __construct(): Failed to set LDAP Referrals (LDAP_OPT_REFERRALS) to ' . $referrals,
118
                ERR_INTERNAL
119
            );
120
        }
121
122
        // Set timeouts, if supported
123
        // (OpenLDAP 2.x.x or Netscape Directory SDK x.x needed)
124
        $this->timeout = $timeout;
125
        if ($timeout > 0) {
126
            if (!@ldap_set_option($this->ldap, LDAP_OPT_NETWORK_TIMEOUT, $timeout)) {
127
                Logger::warning(
128
                    'Library - LDAP __construct(): Unable to set timeouts (LDAP_OPT_NETWORK_TIMEOUT) to ' . $timeout
129
                );
130
            }
131
            if (!@ldap_set_option($this->ldap, LDAP_OPT_TIMELIMIT, $timeout)) {
132
                Logger::warning(
133
                    'Library - LDAP __construct(): Unable to set timeouts (LDAP_OPT_TIMELIMIT) to ' . $timeout
134
                );
135
            }
136
        }
137
138
        // Enable TLS, if needed
139
        if (stripos($hostname, "ldaps:") === false && $enable_tls) {
140
            if (!@ldap_start_tls($this->ldap)) {
141
                throw $this->makeException('Library - LDAP __construct():' . ' Unable to force TLS', ERR_INTERNAL);
142
            }
143
        }
144
    }
145
146
147
    /**
148
     * Convenience method to create an LDAPException as well as log the
149
     * description.
150
     *
151
     * @param string $description The exception's description
152
     * @param int|null $type The exception's type
153
     * @return \Exception
154
     */
155
    private function makeException(string $description, int $type = null): \Exception
156
    {
157
        $errNo = @ldap_errno($this->ldap);
158
159
        // Decide exception type and return
160
        if ($type !== null) {
161
            if ($errNo !== 0) {
162
                // Only log real LDAP errors; not success
163
                Logger::error($description . '; cause: \'' . ldap_error($this->ldap) . '\' (0x' . dechex($errNo) . ')');
164
            } else {
165
                Logger::error($description);
166
            }
167
168
            switch ($type) {
169
                case ERR_INTERNAL:// 1 - ExInternal
170
                    return new Error\Exception($description, $errNo);
171
                case ERR_NO_USER:// 2 - ExUserNotFound
172
                    return new Error\UserNotFound($description, $errNo);
173
                case ERR_WRONG_PW:// 3 - ExInvalidCredential
174
                    return new Error\InvalidCredential($description, $errNo);
175
                case ERR_AS_DATA_INCONSIST:// 4 - ExAsDataInconsist
176
                    return new Error\AuthSource('ldap', $description);
177
                case ERR_AS_INTERNAL:// 5 - ExAsInternal
178
                    return new Error\AuthSource('ldap', $description);
179
            }
180
        } else {
181
            if ($errNo !== 0) {
182
                $description .= '; cause: \'' . ldap_error($this->ldap) . '\' (0x' . dechex($errNo) . ')';
183
                if (
184
                    @ldap_get_option($this->ldap, LDAP_OPT_DIAGNOSTIC_MESSAGE, $extendedError)
185
                    && !empty($extendedError)
186
                ) {
187
                    $description .= '; additional: \'' . $extendedError . '\'';
188
                }
189
            }
190
            switch ($errNo) {
191
                case 0x20://LDAP_NO_SUCH_OBJECT
192
                    Logger::warning($description);
193
                    return new Error\UserNotFound($description, $errNo);
194
                case 0x31://LDAP_INVALID_CREDENTIALS
195
                    Logger::info($description);
196
                    return new Error\InvalidCredential($description, $errNo);
197
                case -1://NO_SERVER_CONNECTION
198
                    Logger::error($description);
199
                    return new Error\AuthSource('ldap', $description);
200
                default:
201
                    Logger::error($description);
202
                    return new Error\AuthSource('ldap', $description);
203
            }
204
        }
205
        return new \Exception('Unknown LDAP error.');
206
    }
207
208
209
    /**
210
     * Search for DN from a single base.
211
     *
212
     * @param string $base
213
     * Indication of root of subtree to search
214
     * @param string|array $attribute
215
     * The attribute name(s) to search for.
216
     * @param string $value
217
     * The attribute value to search for.
218
     * Additional search filter
219
     * @param string|null $searchFilter
220
     * The scope of the search
221
     * @param string $scope
222
     * @return string
223
     * The DN of the resulting found element.
224
     * @throws Error\Exception if:
225
     * - Attribute parameter is wrong type
226
     * @throws Error\AuthSource if:
227
     * - Not able to connect to LDAP server
228
     * - False search result
229
     * - Count return false
230
     * - Searche found more than one result
231
     * - Failed to get first entry from result
232
     * - Failed to get DN for entry
233
     * @throws Error\UserNotFound if:
234
     * - Zero entries were found
235
     * @psalm-suppress TypeDoesNotContainType
236
     */
237
    private function search(
238
        string $base,
239
        $attribute,
240
        string $value,
241
        ?string $searchFilter = null,
242
        string $scope = "subtree"
243
    ): string {
244
        // Create the search filter
245
        /** @var array $attribute */
246
        $attribute = self::escapeFilterValue($attribute, false);
247
248
        /** @var string $value */
249
        $value = self::escapeFilterValue($value, true);
250
251
        $filter = '';
252
        foreach ($attribute as $attr) {
253
            $filter .= '(' . $attr . '=' . $value . ')';
254
        }
255
        $filter = '(|' . $filter . ')';
256
257
        // Append LDAP filters if defined
258
        if ($searchFilter !== null) {
259
            $filter = "(&" . $filter . "" . $searchFilter . ")";
260
        }
261
262
        // Search using generated filter
263
        Logger::debug(
264
            'Library - LDAP search(): Searching base (' . $scope . ') \'' . $base . '\' for \'' . $filter . '\''
265
        );
266
        if ($scope === 'base') {
267
            $result = @ldap_read($this->ldap, $base, $filter, [], 0, 0, $this->timeout, LDAP_DEREF_NEVER);
268
        } elseif ($scope === 'onelevel') {
269
            $result = @ldap_list($this->ldap, $base, $filter, [], 0, 0, $this->timeout, LDAP_DEREF_NEVER);
270
        } else {
271
            $result = @ldap_search($this->ldap, $base, $filter, [], 0, 0, $this->timeout, LDAP_DEREF_NEVER);
272
        }
273
274
        if ($result === false) {
275
            throw $this->makeException(
276
                'Library - LDAP search(): Failed search on base \'' . $base . '\' for \'' . $filter . '\''
277
            );
278
        }
279
280
        // Sanity checks on search results
281
        $count = @ldap_count_entries($this->ldap, $result);
282
        if ($count === false) {
283
            throw $this->makeException('Library - LDAP search(): Failed to get number of entries returned');
284
        } elseif ($count > 1) {
285
            // More than one entry is found. External error
286
            throw $this->makeException(
287
                'Library - LDAP search(): Found ' . $count . ' entries searching base \'' . $base .
288
                    '\' for \'' . $filter . '\'',
289
                ERR_AS_DATA_INCONSIST
290
            );
291
        } elseif ($count === 0) {
292
            // No entry is fond => wrong username is given (or not registered in the catalogue). User error
293
            throw $this->makeException(
294
                'Library - LDAP search(): Found no entries searching base \'' . $base .
295
                    '\' for \'' . $filter . '\'',
296
                ERR_NO_USER
297
            );
298
        }
299
300
301
        // Resolve the DN from the search result
302
        $entry = @ldap_first_entry($this->ldap, $result);
303
        if ($entry === false) {
304
            throw $this->makeException(
305
                'Library - LDAP search(): Unable to retrieve result after searching base \'' .
306
                    $base . '\' for \'' . $filter . '\''
307
            );
308
        }
309
        $dn = @ldap_get_dn($this->ldap, $entry);
310
        if ($dn === false) {
311
            throw $this->makeException(
312
                'Library - LDAP search(): Unable to get DN after searching base \'' . $base .
313
                    '\' for \'' . $filter . '\''
314
            );
315
        }
316
        return $dn;
317
    }
318
319
320
    /**
321
     * Search for a DN.
322
     *
323
     * @param string|array $base
324
     * The base, or bases, which to search from.
325
     * @param string|array $attribute
326
     * The attribute name(s) searched for.
327
     * @param string $value
328
     * The attribute value searched for.
329
     * @param bool $allowZeroHits
330
     * Determines if the method will throw an exception if no hits are found.
331
     * Defaults to FALSE.
332
     * @param string|null $searchFilter
333
     * Additional searchFilter to be added to the (attribute=value) filter
334
     * @param string $scope
335
     * The scope of the search
336
     * @return string|null
337
     * The DN of the matching element, if found. If no element was found and
338
     * $allowZeroHits is set to FALSE, an exception will be thrown; otherwise
339
     * NULL will be returned.
340
     * @throws Error\AuthSource if:
341
     * - LDAP search encounter some problems when searching cataloge
342
     * - Not able to connect to LDAP server
343
     * @throws Error\UserNotFound if:
344
     * - $allowZeroHits is FALSE and no result is found
345
     *
346
     */
347
    public function searchfordn(
348
        $base,
349
        $attribute,
350
        string $value,
351
        bool $allowZeroHits = false,
352
        ?string $searchFilter = null,
353
        string $scope = 'subtree'
354
    ): ?string {
355
        // Traverse all search bases, returning DN if found
356
        $bases = Utils\Arrays::arrayize($base);
357
        foreach ($bases as $current) {
358
            try {
359
                // Single base search
360
                $result = $this->search($current, $attribute, $value, $searchFilter, $scope);
361
362
                // We don't hawe to look any futher if user is found
363
                if (!empty($result)) {
364
                    return $result;
365
                }
366
                // If search failed, attempt the other base DNs
367
            } catch (Error\UserNotFound $e) {
368
                // Just continue searching
369
            }
370
        }
371
        // Decide what to do for zero entries
372
        Logger::debug('Library - LDAP searchfordn(): No entries found');
373
        if ($allowZeroHits) {
374
            // Zero hits allowed
375
            return null;
376
        } else {
377
            // Zero hits not allowed
378
            throw $this->makeException('Library - LDAP searchfordn(): LDAP search returned zero entries for' .
379
                ' filter \'(' . join(' | ', Utils\Arrays::arrayize($attribute)) .
380
                ' = ' . $value . ')\' on base(s) \'(' . join(' & ', $bases) . ')\'', 2);
381
        }
382
    }
383
384
385
    /**
386
     * This method was created specifically for the ldap:AttributeAddUsersGroups->searchActiveDirectory()
387
     * method, but could be used for other LDAP search needs. It will search LDAP and return all the entries.
388
     *
389
     * @throws \Exception
390
     * @param string|array $bases
391
     * @param string|array $filters Array of 'attribute' => 'values' to be combined into the filter,
392
     *     or a raw filter string
393
     * @param string|array $attributes Array of attributes requested from LDAP
394
     * @param array $binaryAttributes Array of attributes that need to be base64 encoded
395
     * @param bool $and If multiple filters defined, then either bind them with & or |
396
     * @param bool $escape Weather to escape the filter values or not
397
     * @param string $scope The scope of the search
398
     * @return array
399
     */
400
    public function searchformultiple(
401
        $bases,
402
        $filters,
403
        $attributes = [],
404
        $binaryAttributes = [],
405
        bool $and = true,
406
        bool $escape = true,
407
        string $scope = 'subtree'
408
    ): array {
409
        // Escape the filter values, if requested
410
        if ($escape) {
411
            $filters = $this->escapeFilterValue($filters, false);
412
        }
413
414
        // Build search filter
415
        $filter = '';
416
        if (is_array($filters)) {
417
            foreach ($filters as $attribute => $value) {
418
                $filter .= "($attribute=$value)";
419
            }
420
            if (count($filters) > 1) {
421
                $filter = ($and ? '(&' : '(|') . $filter . ')';
422
            }
423
        } else {
424
            /** @psalm-suppress RedundantConditionGivenDocblockType */
425
            Assert::string($filters);
426
            $filter = $filters;
427
        }
428
429
        // Verify filter was created
430
        if ($filter == '' || $filter == '(=)') {
431
            throw $this->makeException('ldap:LdapConnection->search_manual : No search filters defined', ERR_INTERNAL);
432
        }
433
434
        // Verify at least one base was passed
435
        $bases = (array) $bases;
436
        if (empty($bases)) {
437
            throw $this->makeException('ldap:LdapConnection->search_manual : No base DNs were passed', ERR_INTERNAL);
438
        }
439
440
        $attributes = Utils\Arrays::arrayize($attributes);
441
442
        // Search each base until result is found
443
        $result = false;
444
        foreach ($bases as $base) {
445
            if ($scope === 'base') {
446
                $result = @ldap_read($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout);
447
            } elseif ($scope === 'onelevel') {
448
                $result = @ldap_list($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout);
449
            } else {
450
                $result = @ldap_search($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout);
451
            }
452
453
            if ($result !== false && @ldap_count_entries($this->ldap, $result) > 0) {
454
                break;
455
            }
456
        }
457
458
        // Verify that a result was found in one of the bases
459
        if ($result === false) {
460
            throw $this->makeException(
461
                'ldap:LdapConnection->search_manual : Failed to search LDAP using base(s) [' .
462
                implode('; ', $bases) . '] with filter [' . $filter . ']. LDAP error [' .
463
                ldap_error($this->ldap) . ']'
464
            );
465
        } elseif (@ldap_count_entries($this->ldap, $result) < 1) {
466
            throw $this->makeException(
467
                'ldap:LdapConnection->search_manual : No entries found in LDAP using base(s) [' .
468
                implode('; ', $bases) . '] with filter [' . $filter . ']',
469
                ERR_NO_USER
470
            );
471
        }
472
473
        // Get all results
474
        $results = ldap_get_entries($this->ldap, $result);
475
        if ($results === false) {
476
            throw $this->makeException(
477
                'ldap:LdapConnection->search_manual : Unable to retrieve entries from search results'
478
            );
479
        }
480
481
        // parse each entry and process its attributes
482
        for ($i = 0; $i < $results['count']; $i++) {
483
            $entry = $results[$i];
484
485
            // iterate over the attributes of the entry
486
            for ($j = 0; $j < $entry['count']; $j++) {
487
                $name = $entry[$j];
488
                $attribute = $entry[$name];
489
490
                // decide whether to base64 encode or not
491
                for ($k = 0; $k < $attribute['count']; $k++) {
492
                    // base64 encode binary attributes
493
                    if (in_array($name, $binaryAttributes, true)) {
494
                        $results[$i][$name][$k] = base64_encode($attribute[$k]);
495
                    }
496
                }
497
            }
498
        }
499
500
        // Remove the count and return
501
        unset($results['count']);
502
        return $results;
503
    }
504
505
506
    /**
507
     * Bind to LDAP with a specific DN and password. Simple wrapper around
508
     * ldap_bind() with some additional logging.
509
     *
510
     * @param string $dn
511
     * The DN used.
512
     * @param string $password
513
     * The password used.
514
     * @param array $sasl_args
515
     * Array of SASL options for SASL bind
516
     * @return bool
517
     * Returns TRUE if successful, FALSE if
518
     * LDAP_INVALID_CREDENTIALS, LDAP_X_PROXY_AUTHZ_FAILURE,
519
     * LDAP_INAPPROPRIATE_AUTH, LDAP_INSUFFICIENT_ACCESS
520
     * @throws Error\Exception on other errors
521
     */
522
    public function bind(string $dn, string $password, array $sasl_args = null): ?bool
523
    {
524
        if ($sasl_args != null) {
525
            if (!function_exists('ldap_sasl_bind')) {
526
                $ex_msg = 'Library - missing SASL support';
527
                throw $this->makeException($ex_msg);
528
            }
529
530
            // SASL Bind, with error handling
531
            $authz_id = $sasl_args['authz_id'];
532
            $error = @ldap_sasl_bind(
533
                $this->ldap,
534
                $dn,
535
                $password,
536
                $sasl_args['mech'],
537
                $sasl_args['realm'],
538
                $sasl_args['authc_id'],
539
                $sasl_args['authz_id'],
540
                $sasl_args['props']
541
            );
542
        } else {
543
            // Simple Bind, with error handling
544
            $authz_id = $dn;
545
            $error = @ldap_bind($this->ldap, $dn, $password);
546
        }
547
548
        if ($error === true) {
549
            // Good
550
            $this->authz_id = $authz_id;
551
            Logger::debug('Library - LDAP bind(): Bind successful with DN \'' . $dn . '\'');
552
            return true;
553
        }
554
555
        /* Handle errors
556
         * LDAP_INVALID_CREDENTIALS
557
         * LDAP_INSUFFICIENT_ACCESS */
558
        switch (ldap_errno($this->ldap)) {
559
            case 32: // LDAP_NO_SUCH_OBJECT
560
                // no break
561
            case 47: // LDAP_X_PROXY_AUTHZ_FAILURE
562
                // no break
563
            case 48: // LDAP_INAPPROPRIATE_AUTH
564
                // no break
565
            case 49: // LDAP_INVALID_CREDENTIALS
566
                // no break
567
            case 50: // LDAP_INSUFFICIENT_ACCESS
568
                return false;
569
            default:
570
                break;
571
        }
572
573
        // Bad
574
        throw $this->makeException('Library - LDAP bind(): Bind failed with DN \'' . $dn . '\'');
575
    }
576
577
578
    /**
579
     * Applies an LDAP option to the current connection.
580
     *
581
     * @throws \Exception
582
     * @param mixed $option
583
     * @param mixed $value
584
     * @return void
585
     */
586
    public function setOption($option, $value): void
587
    {
588
        // Attempt to set the LDAP option
589
        if (!@ldap_set_option($this->ldap, $option, $value)) {
590
            throw $this->makeException(
591
                'ldap:LdapConnection->setOption : Failed to set LDAP option [' .
592
                $option . '] with the value [' . $value . '] error: ' . ldap_error($this->ldap),
593
                ERR_INTERNAL
594
            );
595
        }
596
597
        // Log debug message
598
        Logger::debug(
599
            'ldap:LdapConnection->setOption : Set the LDAP option [' .
600
            $option . '] with the value [' . $value . ']'
601
        );
602
    }
603
604
605
    /**
606
     * Search a given DN for attributes, and return the resulting associative
607
     * array.
608
     *
609
     * @param string $dn
610
     * The DN of an element.
611
     * @param string|array $attributes
612
     * The names of the attribute(s) to retrieve. Defaults to NULL; that is,
613
     * all available attributes. Note that this is not very effective.
614
     * @param array $binaryAttributes
615
     * The names of the attribute(s) to base64 encode
616
     * @param int $maxsize
617
     * The maximum size of any attribute's value(s). If exceeded, the attribute
618
     * will not be returned.
619
     * @return array
620
     * The array of attributes and their values.
621
     * @see http://no.php.net/manual/en/function.ldap-read.php
622
     */
623
    public function getAttributes(string $dn, $attributes = null, array $binaryAttributes = [], int $maxsize = null): array
624
    {
625
        // Preparations, including a pretty debug message...
626
        $description = 'all attributes';
627
        if (is_array($attributes)) {
628
            $description = '\'' . join(',', $attributes) . '\'';
629
        } else {
630
            // Get all attributes...
631
            // TODO: Verify that this originally was the intended behaviour. Could $attributes be a string?
632
            $attributes = [];
633
        }
634
        Logger::debug('Library - LDAP getAttributes(): Getting ' . $description . ' from DN \'' . $dn . '\'');
635
636
        // Attempt to get attributes
637
        // TODO: Should aliases be dereferenced?
638
        $result = @ldap_read($this->ldap, $dn, 'objectClass=*', $attributes, 0, 0, $this->timeout);
639
        if ($result === false) {
640
            throw $this->makeException(
641
                'Library - LDAP getAttributes(): Failed to get attributes from DN \'' . $dn . '\''
642
            );
643
        }
644
        $entry = @ldap_first_entry($this->ldap, $result);
645
        if ($entry === false) {
646
            throw $this->makeException(
647
                'Library - LDAP getAttributes(): Could not get first entry from DN \'' . $dn . '\''
648
            );
649
        }
650
        unset($attributes);
651
652
        $attributes = @ldap_get_attributes($this->ldap, $entry);
653
        if ($attributes === false) {
654
            throw $this->makeException(
655
                'Library - LDAP getAttributes(): Could not get attributes of first entry from DN \'' . $dn . '\''
656
            );
657
        }
658
659
        // Parsing each found attribute into our result set
660
        $result = []; // Recycling $result... Possibly bad practice.
661
        for ($i = 0; $i < $attributes['count']; $i++) {
662
            // Ignore attributes that exceed the maximum allowed size
663
            $name = $attributes[$i];
664
            $attribute = $attributes[$name];
665
666
            // Deciding whether to base64 encode
667
            $values = [];
668
            for ($j = 0; $j < $attribute['count']; $j++) {
669
                $value = $attribute[$j];
670
671
                if (!empty($maxsize) && strlen($value) > $maxsize) {
672
                    // Ignoring and warning
673
                    Logger::warning('Library - LDAP getAttributes(): Attribute \'' .
674
                        $name . '\' exceeded maximum allowed size by ' . (strlen($value) - $maxsize));
675
                    continue;
676
                }
677
678
                // Base64 encode binary attributes
679
                if (in_array($name, $binaryAttributes)) {
680
                    $values[] = base64_encode($value);
681
                } else {
682
                    $values[] = $value;
683
                }
684
            }
685
686
            // Adding
687
            $result[$name] = $values;
688
        }
689
690
        // We're done
691
        Logger::debug('Library - LDAP getAttributes(): Found attributes \'(' . join(',', array_keys($result)) . ')\'');
692
        return $result;
693
    }
694
695
696
    /**
697
     * Enter description here...
698
     *
699
     * @param array $config
700
     * @param string $username
701
     * @param string|null $password
702
     * @return array|false
703
     */
704
    public function validate(array $config, string $username, string $password = null)
705
    {
706
        /**
707
         * Escape any characters with a special meaning in LDAP. The following
708
         * characters have a special meaning (according to RFC 2253):
709
         * ',', '+', '"', '\', '<', '>', ';', '*'
710
         * These characters are escaped by prefixing them with '\'.
711
         */
712
        $username = addcslashes($username, ',+"\\<>;*');
713
714
        if (isset($config['priv_user_dn'])) {
715
            $this->bind($config['priv_user_dn'], $config['priv_user_pw']);
716
        }
717
        if (isset($config['dnpattern'])) {
718
            $dn = str_replace('%username%', $username, $config['dnpattern']);
719
        } else {
720
            /** @var string $dn */
721
            $dn = $this->searchfordn($config['searchbase'], $config['searchattributes'], $username, false);
722
        }
723
724
        if ($password !== null) {
725
            // checking users credentials ... assuming below that she may read her own attributes ...
726
            // escape characters with a special meaning, also in the password
727
            $password = addcslashes($password, ',+"\\<>;*');
728
            if (!$this->bind($dn, $password)) {
729
                Logger::info(
730
                    'Library - LDAP validate(): Failed to authenticate \'' . $username . '\' using DN \'' . $dn . '\''
731
                );
732
                return false;
733
            }
734
        }
735
736
        /**
737
         * Retrieve attributes from LDAP
738
         */
739
        $attributes = $this->getAttributes($dn, $config['attributes'], $config['attributes.binary']);
740
        return $attributes;
741
    }
742
743
744
    /**
745
     * Borrowed function from PEAR:LDAP.
746
     *
747
     * Escapes the given VALUES according to RFC 2254 so that they can be safely used in LDAP filters.
748
     *
749
     * Any control characters with an ACII code < 32 as well as the characters with special meaning in
750
     * LDAP filters "*", "(", ")", and "\" (the backslash) are converted into the representation of a
751
     * backslash followed by two hex digits representing the hexadecimal value of the character.
752
     *
753
     * @static
754
     * @param string|array $values Array of values to escape
755
     * @param bool $singleValue
756
     * @return string|array Array $values, but escaped
757
     */
758
    public static function escapeFilterValue($values = [], bool $singleValue = true)
759
    {
760
        // Parameter validation
761
        $values = Utils\Arrays::arrayize($values);
762
763
        foreach ($values as $key => $val) {
764
            if ($val === null) {
765
                $val = '\0'; // apply escaped "null" if string is empty
766
            } else {
767
                // Escaping of filter meta characters
768
                $val = str_replace('\\', '\5c', $val);
769
                $val = str_replace('*', '\2a', $val);
770
                $val = str_replace('(', '\28', $val);
771
                $val = str_replace(')', '\29', $val);
772
773
                // ASCII < 32 escaping
774
                $val = self::asc2hex32($val);
775
            }
776
777
            $values[$key] = $val;
778
        }
779
        if ($singleValue) {
780
            return $values[0];
781
        }
782
        return $values;
783
    }
784
785
786
    /**
787
     * Borrowed function from PEAR:LDAP.
788
     *
789
     * Converts all ASCII chars < 32 to "\HEX"
790
     *
791
     * @param string $string String to convert
792
     *
793
     * @static
794
     * @return string
795
     */
796
    public static function asc2hex32(string $string): string
797
    {
798
        for ($i = 0; $i < strlen($string); $i++) {
799
            $char = substr($string, $i, 1);
800
            if (ord($char) < 32) {
801
                $hex = dechex(ord($char));
802
                if (strlen($hex) == 1) {
803
                    $hex = '0' . $hex;
804
                }
805
                $string = str_replace($char, '\\' . $hex, $string);
806
            }
807
        }
808
        return $string;
809
    }
810
811
812
    /**
813
     * Convert SASL authz_id into a DN
814
     *
815
     * @param string $searchBase
816
     * @param array $searchAttributes
817
     * @param string $authz_id
818
     * @return string|null
819
     */
820
    private function authzidToDn(string $searchBase, array $searchAttributes, string $authz_id): ?string
821
    {
822
        if (preg_match("/^dn:/", $authz_id)) {
823
            return preg_replace("/^dn:/", "", $authz_id);
824
        }
825
826
        if (preg_match("/^u:/", $authz_id)) {
827
            return $this->searchfordn(
828
                $searchBase,
829
                $searchAttributes,
830
                preg_replace("/^u:/", "", $authz_id)
831
            );
832
        }
833
        return $authz_id;
834
    }
835
836
837
    /**
838
     * ldap_exop_whoami accessor, if available. Use requested authz_id
839
     * otherwise.
840
     *
841
     * @param string $searchBase
842
     * @param array $searchAttributes
843
     * @throws \Exception
844
     * @return string
845
     */
846
    public function whoami(string $searchBase, array $searchAttributes): string
847
    {
848
        $authz_id = ldap_exop_whoami($this->ldap);
1 ignored issue
show
Bug introduced by
The function ldap_exop_whoami was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

848
        $authz_id = /** @scrutinizer ignore-call */ ldap_exop_whoami($this->ldap);
Loading history...
849
        if ($authz_id === false) {
850
            throw $this->makeException('LDAP whoami exop failure');
851
        }
852
853
        $dn = $this->authzidToDn($searchBase, $searchAttributes, $authz_id);
854
        if (empty($dn)) {
855
            throw $this->makeException('Cannot figure userID');
856
        }
857
858
        return $dn;
859
    }
860
}
861