Passed
Pull Request — master (#18)
by Tim
02:24
created

Ldap   F

Complexity

Total Complexity 113

Size/Duplication

Total Lines 878
Duplicated Lines 0 %

Importance

Changes 8
Bugs 1 Features 1
Metric Value
wmc 113
eloc 329
c 8
b 1
f 1
dl 0
loc 878
rs 2

15 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 107 24
C getAttributes() 0 75 13
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
A setAttributes() 0 10 2
A addAttributes() 0 10 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 bool $and If multiple filters defined, then either bind them with & or |
395
     * @param bool $escape Weather to escape the filter values or not
396
     * @param string $scope The scope of the search
397
     * @return array
398
     */
399
    public function searchformultiple(
400
        $bases,
401
        $filters,
402
        $attributes = [],
403
        bool $and = true,
404
        bool $escape = true,
405
        string $scope = 'subtree'
406
    ): array {
407
        // Escape the filter values, if requested
408
        if ($escape) {
409
            $filters = $this->escapeFilterValue($filters, false);
410
        }
411
412
        // Build search filter
413
        $filter = '';
414
        if (is_array($filters)) {
415
            foreach ($filters as $attribute => $value) {
416
                $filter .= "($attribute=$value)";
417
            }
418
            if (count($filters) > 1) {
419
                $filter = ($and ? '(&' : '(|') . $filter . ')';
420
            }
421
        } else {
422
            /** @psalm-suppress RedundantConditionGivenDocblockType */
423
            Assert::string($filters);
424
            $filter = $filters;
425
        }
426
427
        // Verify filter was created
428
        if ($filter == '' || $filter == '(=)') {
429
            throw $this->makeException('ldap:LdapConnection->search_manual : No search filters defined', ERR_INTERNAL);
430
        }
431
432
        // Verify at least one base was passed
433
        $bases = (array) $bases;
434
        if (empty($bases)) {
435
            throw $this->makeException('ldap:LdapConnection->search_manual : No base DNs were passed', ERR_INTERNAL);
436
        }
437
438
        $attributes = Utils\Arrays::arrayize($attributes);
439
440
        // Search each base until result is found
441
        $result = false;
442
        foreach ($bases as $base) {
443
            if ($scope === 'base') {
444
                $result = @ldap_read($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout);
445
            } elseif ($scope === 'onelevel') {
446
                $result = @ldap_list($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout);
447
            } else {
448
                $result = @ldap_search($this->ldap, $base, $filter, $attributes, 0, 0, $this->timeout);
449
            }
450
451
            if ($result !== false && @ldap_count_entries($this->ldap, $result) > 0) {
452
                break;
453
            }
454
        }
455
456
        // Verify that a result was found in one of the bases
457
        if ($result === false) {
458
            throw $this->makeException(
459
                'ldap:LdapConnection->search_manual : Failed to search LDAP using base(s) [' .
460
                implode('; ', $bases) . '] with filter [' . $filter . ']. LDAP error [' .
461
                ldap_error($this->ldap) . ']'
462
            );
463
        } elseif (@ldap_count_entries($this->ldap, $result) < 1) {
464
            throw $this->makeException(
465
                'ldap:LdapConnection->search_manual : No entries found in LDAP using base(s) [' .
466
                implode('; ', $bases) . '] with filter [' . $filter . ']',
467
                ERR_NO_USER
468
            );
469
        }
470
471
        // Get all results
472
        $results = ldap_get_entries($this->ldap, $result);
473
        if ($results === false) {
474
            throw $this->makeException(
475
                'ldap:LdapConnection->search_manual : Unable to retrieve entries from search results'
476
            );
477
        }
478
479
        // parse each entry and process its attributes
480
        for ($i = 0; $i < $results['count']; $i++) {
481
            $entry = $results[$i];
482
483
            // iterate over the attributes of the entry
484
            for ($j = 0; $j < $entry['count']; $j++) {
485
                $name = $entry[$j];
486
                $attribute = $entry[$name];
487
488
                // decide whether to base64 encode or not
489
                for ($k = 0; $k < $attribute['count']; $k++) {
490
                    // base64 encode binary attributes
491
                    if (
492
                        strtolower($name) === 'jpegphoto'
493
                        || strtolower($name) === 'objectguid'
494
                        || strtolower($name) === 'objectsid'
495
                        || strtolower($name) === 'ms-ds-consistencyguid'
496
                    ) {
497
                        $results[$i][$name][$k] = base64_encode($attribute[$k]);
498
                    }
499
                }
500
            }
501
        }
502
503
        // Remove the count and return
504
        unset($results['count']);
505
        return $results;
506
    }
507
508
509
    /**
510
     * Bind to LDAP with a specific DN and password. Simple wrapper around
511
     * ldap_bind() with some additional logging.
512
     *
513
     * @param string $dn
514
     * The DN used.
515
     * @param string $password
516
     * The password used.
517
     * @param array $sasl_args
518
     * Array of SASL options for SASL bind
519
     * @return bool
520
     * Returns TRUE if successful, FALSE if
521
     * LDAP_INVALID_CREDENTIALS, LDAP_X_PROXY_AUTHZ_FAILURE,
522
     * LDAP_INAPPROPRIATE_AUTH, LDAP_INSUFFICIENT_ACCESS
523
     * @throws Error\Exception on other errors
524
     */
525
    public function bind(string $dn, string $password, array $sasl_args = null): ?bool
526
    {
527
        if ($sasl_args != null) {
528
            if (!function_exists('ldap_sasl_bind')) {
529
                $ex_msg = 'Library - missing SASL support';
530
                throw $this->makeException($ex_msg);
531
            }
532
533
            // SASL Bind, with error handling
534
            $authz_id = $sasl_args['authz_id'];
535
            $error = @ldap_sasl_bind(
536
                $this->ldap,
537
                $dn,
538
                $password,
539
                $sasl_args['mech'],
540
                $sasl_args['realm'],
541
                $sasl_args['authc_id'],
542
                $sasl_args['authz_id'],
543
                $sasl_args['props']
544
            );
545
        } else {
546
            // Simple Bind, with error handling
547
            $authz_id = $dn;
548
            $error = @ldap_bind($this->ldap, $dn, $password);
549
        }
550
551
        if ($error === true) {
552
            // Good
553
            $this->authz_id = $authz_id;
554
            Logger::debug('Library - LDAP bind(): Bind successful with DN \'' . $dn . '\'');
555
            return true;
556
        }
557
558
        /* Handle errors
559
         * LDAP_INVALID_CREDENTIALS
560
         * LDAP_INSUFFICIENT_ACCESS */
561
        switch (ldap_errno($this->ldap)) {
562
            case 32: // LDAP_NO_SUCH_OBJECT
563
                // no break
564
            case 47: // LDAP_X_PROXY_AUTHZ_FAILURE
565
                // no break
566
            case 48: // LDAP_INAPPROPRIATE_AUTH
567
                // no break
568
            case 49: // LDAP_INVALID_CREDENTIALS
569
                // no break
570
            case 50: // LDAP_INSUFFICIENT_ACCESS
571
                return false;
572
            default:
573
                break;
574
        }
575
576
        // Bad
577
        throw $this->makeException('Library - LDAP bind(): Bind failed with DN \'' . $dn . '\'');
578
    }
579
580
581
    /**
582
     * Applies an LDAP option to the current connection.
583
     *
584
     * @throws \Exception
585
     * @param mixed $option
586
     * @param mixed $value
587
     * @return void
588
     */
589
    public function setOption($option, $value): void
590
    {
591
        // Attempt to set the LDAP option
592
        if (!@ldap_set_option($this->ldap, $option, $value)) {
593
            throw $this->makeException(
594
                'ldap:LdapConnection->setOption : Failed to set LDAP option [' .
595
                $option . '] with the value [' . $value . '] error: ' . ldap_error($this->ldap),
596
                ERR_INTERNAL
597
            );
598
        }
599
600
        // Log debug message
601
        Logger::debug(
602
            'ldap:LdapConnection->setOption : Set the LDAP option [' .
603
            $option . '] with the value [' . $value . ']'
604
        );
605
    }
606
607
608
    /**
609
     * Search a given DN for attributes, and return the resulting associative
610
     * array.
611
     *
612
     * @param string $dn
613
     * The DN of an element.
614
     * @param string|array $attributes
615
     * The names of the attribute(s) to retrieve. Defaults to NULL; that is,
616
     * all available attributes. Note that this is not very effective.
617
     * @param int $maxsize
618
     * The maximum size of any attribute's value(s). If exceeded, the attribute
619
     * will not be returned.
620
     * @return array
621
     * The array of attributes and their values.
622
     * @see http://no.php.net/manual/en/function.ldap-read.php
623
     */
624
    public function getAttributes(string $dn, $attributes = null, int $maxsize = null): array
625
    {
626
        // Preparations, including a pretty debug message...
627
        $description = 'all attributes';
628
        if (is_array($attributes)) {
629
            $description = '\'' . join(',', $attributes) . '\'';
630
        } else {
631
            // Get all attributes...
632
            // TODO: Verify that this originally was the intended behaviour. Could $attributes be a string?
633
            $attributes = [];
634
        }
635
        Logger::debug('Library - LDAP getAttributes(): Getting ' . $description . ' from DN \'' . $dn . '\'');
636
637
        // Attempt to get attributes
638
        // TODO: Should aliases be dereferenced?
639
        $result = @ldap_read($this->ldap, $dn, 'objectClass=*', $attributes, 0, 0, $this->timeout);
640
        if ($result === false) {
641
            throw $this->makeException(
642
                'Library - LDAP getAttributes(): Failed to get attributes from DN \'' . $dn . '\''
643
            );
644
        }
645
        $entry = @ldap_first_entry($this->ldap, $result);
646
        if ($entry === false) {
647
            throw $this->makeException(
648
                'Library - LDAP getAttributes(): Could not get first entry from DN \'' . $dn . '\''
649
            );
650
        }
651
        unset($attributes);
652
653
        $attributes = @ldap_get_attributes($this->ldap, $entry);
654
        if ($attributes === false) {
655
            throw $this->makeException(
656
                'Library - LDAP getAttributes(): Could not get attributes of first entry from DN \'' . $dn . '\''
657
            );
658
        }
659
660
        // Parsing each found attribute into our result set
661
        $result = []; // Recycling $result... Possibly bad practice.
662
        for ($i = 0; $i < $attributes['count']; $i++) {
663
            // Ignore attributes that exceed the maximum allowed size
664
            $name = $attributes[$i];
665
            $attribute = $attributes[$name];
666
667
            // Deciding whether to base64 encode
668
            $values = [];
669
            for ($j = 0; $j < $attribute['count']; $j++) {
670
                $value = $attribute[$j];
671
672
                if (!empty($maxsize) && strlen($value) > $maxsize) {
673
                    // Ignoring and warning
674
                    Logger::warning('Library - LDAP getAttributes(): Attribute \'' .
675
                        $name . '\' exceeded maximum allowed size by ' . (strlen($value) - $maxsize));
676
                    continue;
677
                }
678
679
                // Base64 encode binary attributes
680
                if (
681
                    strtolower($name) === 'jpegphoto'
682
                    || strtolower($name) === 'objectguid'
683
                    || strtolower($name) === 'objectsid'
684
                    || strtolower($name) === 'ms-ds-consistencyguid'
685
                ) {
686
                    $values[] = base64_encode($value);
687
                } else {
688
                    $values[] = $value;
689
                }
690
            }
691
692
            // Adding
693
            $result[$name] = $values;
694
        }
695
696
        // We're done
697
        Logger::debug('Library - LDAP getAttributes(): Found attributes \'(' . join(',', array_keys($result)) . ')\'');
698
        return $result;
699
    }
700
701
702
    /**
703
     * Enter description here...
704
     *
705
     * @param array $config
706
     * @param string $username
707
     * @param string|null $password
708
     * @return array|false
709
     */
710
    public function validate(array $config, string $username, string $password = null)
711
    {
712
        /**
713
         * Escape any characters with a special meaning in LDAP. The following
714
         * characters have a special meaning (according to RFC 2253):
715
         * ',', '+', '"', '\', '<', '>', ';', '*'
716
         * These characters are escaped by prefixing them with '\'.
717
         */
718
        $username = addcslashes($username, ',+"\\<>;*');
719
720
        if (isset($config['priv_user_dn'])) {
721
            $this->bind($config['priv_user_dn'], $config['priv_user_pw']);
722
        }
723
        if (isset($config['dnpattern'])) {
724
            $dn = str_replace('%username%', $username, $config['dnpattern']);
725
        } else {
726
            /** @var string $dn */
727
            $dn = $this->searchfordn($config['searchbase'], $config['searchattributes'], $username, false);
728
        }
729
730
        if ($password !== null) {
731
            // checking users credentials ... assuming below that she may read her own attributes ...
732
            // escape characters with a special meaning, also in the password
733
            $password = addcslashes($password, ',+"\\<>;*');
734
            if (!$this->bind($dn, $password)) {
735
                Logger::info(
736
                    'Library - LDAP validate(): Failed to authenticate \'' . $username . '\' using DN \'' . $dn . '\''
737
                );
738
                return false;
739
            }
740
        }
741
742
        /**
743
         * Retrieve attributes from LDAP
744
         */
745
        $attributes = $this->getAttributes($dn, $config['attributes']);
746
        return $attributes;
747
    }
748
749
750
    /**
751
     * Borrowed function from PEAR:LDAP.
752
     *
753
     * Escapes the given VALUES according to RFC 2254 so that they can be safely used in LDAP filters.
754
     *
755
     * Any control characters with an ACII code < 32 as well as the characters with special meaning in
756
     * LDAP filters "*", "(", ")", and "\" (the backslash) are converted into the representation of a
757
     * backslash followed by two hex digits representing the hexadecimal value of the character.
758
     *
759
     * @static
760
     * @param string|array $values Array of values to escape
761
     * @param bool $singleValue
762
     * @return string|array Array $values, but escaped
763
     */
764
    public static function escapeFilterValue($values = [], bool $singleValue = true)
765
    {
766
        // Parameter validation
767
        $values = Utils\Arrays::arrayize($values);
768
769
        foreach ($values as $key => $val) {
770
            if ($val === null) {
771
                $val = '\0'; // apply escaped "null" if string is empty
772
            } else {
773
                // Escaping of filter meta characters
774
                $val = str_replace('\\', '\5c', $val);
775
                $val = str_replace('*', '\2a', $val);
776
                $val = str_replace('(', '\28', $val);
777
                $val = str_replace(')', '\29', $val);
778
779
                // ASCII < 32 escaping
780
                $val = self::asc2hex32($val);
781
            }
782
783
            $values[$key] = $val;
784
        }
785
        if ($singleValue) {
786
            return $values[0];
787
        }
788
        return $values;
789
    }
790
791
792
    /**
793
     * Borrowed function from PEAR:LDAP.
794
     *
795
     * Converts all ASCII chars < 32 to "\HEX"
796
     *
797
     * @param string $string String to convert
798
     *
799
     * @static
800
     * @return string
801
     */
802
    public static function asc2hex32(string $string): string
803
    {
804
        for ($i = 0; $i < strlen($string); $i++) {
805
            $char = substr($string, $i, 1);
806
            if (ord($char) < 32) {
807
                $hex = dechex(ord($char));
808
                if (strlen($hex) == 1) {
809
                    $hex = '0' . $hex;
810
                }
811
                $string = str_replace($char, '\\' . $hex, $string);
812
            }
813
        }
814
        return $string;
815
    }
816
817
818
    /**
819
     * Convert SASL authz_id into a DN
820
     *
821
     * @param string $searchBase
822
     * @param array $searchAttributes
823
     * @param string $authz_id
824
     * @return string|null
825
     */
826
    private function authzidToDn(string $searchBase, array $searchAttributes, string $authz_id): ?string
827
    {
828
        if (preg_match("/^dn:/", $authz_id)) {
829
            return preg_replace("/^dn:/", "", $authz_id);
830
        }
831
832
        if (preg_match("/^u:/", $authz_id)) {
833
            return $this->searchfordn(
834
                $searchBase,
835
                $searchAttributes,
836
                preg_replace("/^u:/", "", $authz_id)
837
            );
838
        }
839
        return $authz_id;
840
    }
841
842
843
    /**
844
     * ldap_exop_whoami accessor, if available. Use requested authz_id
845
     * otherwise.
846
     *
847
     * @param string $searchBase
848
     * @param array $searchAttributes
849
     * @throws \Exception
850
     * @return string
851
     */
852
    public function whoami(string $searchBase, array $searchAttributes): string
853
    {
854
        $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

854
        $authz_id = /** @scrutinizer ignore-call */ ldap_exop_whoami($this->ldap);
Loading history...
855
        if ($authz_id === false) {
856
            throw $this->makeException('LDAP whoami exop failure');
857
        }
858
859
        $dn = $this->authzidToDn($searchBase, $searchAttributes, $authz_id);
860
        if (empty($dn)) {
861
            throw $this->makeException('Cannot figure userID');
862
        }
863
864
        return $dn;
865
    }
866
867
868
    /**
869
     * Set for a given DN attributes
870
     *
871
     * @param string $dn
872
     * The DN of an element.
873
     * @param array $attributes
874
     * The names and value of the attribute(s) to set using ldap_mod_replace structure;
875
     * @return bool
876
     * Result of operation
877
     */
878
    public function setAttributes($dn, array $attributes) {
879
      Logger::debug('Library - LDAP setAttributes(): Received arraydata:'.print_r($attributes, true));
880
881
      // Attempt to set attributes
882
      $result = @ldap_mod_replace($this->ldap, $dn, $attributes);
883
      if ($result === false) {
884
          throw $this->makeException('Library - LDAP setAttributes(): Failed to set attributes for DN \''.$dn.'\'. Bind necessary?');
885
      }
886
887
      return $result;
888
    }
889
890
891
    /**
892
      * Adds for a given DN attributes
893
      *
894
      * @param string $dn
895
      * The DN of an element.
896
      * @param array $attributes
897
      * The names and value of the attribute(s) to set using ldap_mod_add structure;
898
      * @return bool
899
      * Result of operation
900
      */
901
     public function addAttributes($dn, array $attributes) {
902
       Logger::debug('Library - LDAP addAttributes(): Received arraydata:'.print_r($attributes, true));
903
904
       // Attempt to add attributes
905
       $result = @ldap_mod_add($this->ldap, $dn, $attributes);
906
       if ($result === false) {
907
           throw $this->makeException('Library - LDAP addAttributes(): Failed to set attributes for DN \''.$dn.'\'. Bind necessary?');
908
       }
909
910
       return $result;
911
     }    
912
}
913