Passed
Pull Request — master (#28)
by Tim
02:49
created

Ldap::searchForMultiple()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 33
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
eloc 17
c 2
b 0
f 0
dl 0
loc 33
rs 9.3888
cc 5
nc 4
nop 5
1
<?php
2
3
declare(strict_types=1);
4
5
namespace SimpleSAML\Module\ldap\Utils;
6
7
use SimpleSAML\Assert\Assert;
8
use SimpleSAML\Error;
9
use SimpleSAML\Logger;
10
use SimpleSAML\Utils;
11
use Symfony\Component\Ldap\Entry;
12
use Symfony\Component\Ldap\Exception\ConnectionException;
13
use Symfony\Component\Ldap\Ldap as LdapObject;
14
15
use function array_pop;
16
use function count;
17
use function dechex;
18
use function is_array;
19
use function ord;
20
use function sprintf;
21
use function strlen;
22
use function strval;
23
use function substr;
24
use function str_replace;
25
26
/**
27
 * LDAP utilities
28
 *
29
 * @package simplesamlphp/simplesamlphp-module-ldap
30
 */
31
32
class Ldap
33
{
34
    /**
35
     * Create Ldap resource objects
36
     *
37
     * @param string $connection_strings
38
     * @param string $encryption
39
     * @param int $version
40
     * @param string $extension
41
     * @param bool $debug
42
     * @param array $options
43
     * @return \Symfony\Component\Ldap\Ldap
44
     */
45
    public function create(
46
        string $connection_strings,
47
        string $encryption = 'ssl',
48
        int $version = 3,
49
        string $extension = 'ext_ldap',
50
        bool $debug = false,
51
        array $options = ['referrals' => false, 'network_timeout' => 3]
52
    ): LdapObject {
53
        foreach (explode(' ', $connection_strings) as $connection_string) {
54
            Assert::regex($connection_string, '#^ldap[s]?:\/\/#');
55
        }
56
57
        return LdapObject::create(
58
            $extension,
59
            [
60
                'connection_string' => $connection_strings,
61
                'encryption' => $encryption,
62
                'version' => $version,
63
                'debug' => $debug,
64
                'options' => $options,
65
            ]
66
        );
67
    }
68
69
70
    /**
71
     * Bind to an LDAP-server
72
     *
73
     * @param \Symfony\Component\Ldap\Ldap $ldapObject
74
     * @param string $username
75
     * @param string|null $password  Null for passwordless logon
76
     * @throws \SimpleSAML\Error\Exception if none of the LDAP-servers could be contacted
77
     */
78
    public function bind(LdapObject $ldapObject, string $username, ?string $password): void
79
    {
80
        try {
81
            $ldapObject->bind($username, strval($password));
82
            return;
83
        } catch (ConnectionException $e) {
84
            Logger::error(sprintf("LDAP bind failed:  %s", $e->getMessage()));
85
        }
86
87
        throw new Error\Exception("Unable to bind to any of the configured LDAP servers.");
88
    }
89
90
91
    /**
92
     * Search the LDAP-directory for a specific object
93
     *
94
     * @param \Symfony\Component\Ldap\Ldap $ldap
95
     * @param array $searchBase
96
     * @param string $filter
97
     * @param array $options
98
     * @param boolean $allowMissing
99
     * @return \Symfony\Component\Ldap\Entry|null The result of the search or null if none found
100
     * @throws \SimpleSAML\Error\Exception if more than one entry was found
101
     * @throws \SimpleSAML\Error\Exception if the object cannot be found using the given search base and filter
102
     */
103
    public function search(
104
        LdapObject $ldap,
105
        array $searchBase,
106
        string $filter,
107
        array $options,
108
        bool $allowMissing
109
    ): ?Entry {
110
        $entry = null;
111
112
        foreach ($searchBase as $base) {
113
            $query = $ldap->query($base, $filter, $options);
114
            $result = $query->execute();
115
            $result = is_array($result) ? $result : $result->toArray();
116
117
            if (count($result) > 1) {
118
                throw new Error\Exception(
119
                    sprintf(
120
                        "Library - LDAP search(): Found %d entries searching base '%s' for '%s'",
121
                        count($result),
122
                        $base,
123
                        $filter,
124
                    )
125
                );
126
            } elseif (count($result) === 1) {
127
                $entry = array_pop($result);
0 ignored issues
show
Bug introduced by
It seems like $result can also be of type Symfony\Component\Ldap\Adapter\list; however, parameter $array of array_pop() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

127
                $entry = array_pop(/** @scrutinizer ignore-type */ $result);
Loading history...
128
                break;
129
            } else {
130
                Logger::debug(
131
                    sprintf(
132
                        "Library - LDAP search(): Found no entries searching base '%s' for '%s'",
133
                        $base,
134
                        $filter,
135
                    )
136
                );
137
            }
138
        }
139
140
        if ($entry === null && $allowMissing === false) {
141
            throw new Error\Exception(
142
                sprintf(
143
                    "Object not found using search base [%s] and filter '%s'",
144
                    implode(', ', $searchBase),
145
                    $filter
146
                )
147
            );
148
        }
149
150
        return $entry;
151
    }
152
153
154
    /**
155
     * Search the LDAP-directory for any object matching the search filter
156
     *
157
     * @param \Symfony\Component\Ldap\Ldap $ldap
158
     * @param array $searchBase
159
     * @param string $filter
160
     * @param array $options
161
     * @param boolean $allowMissing
162
     * @return \Symfony\Component\Ldap\Entry[] The result of the search
163
     * @throws \SimpleSAML\Error\Exception if more than one entry was found
164
     * @throws \SimpleSAML\Error\Exception if the object cannot be found using the given search base and filter
165
     */
166
    public function searchForMultiple(
167
        LdapObject $ldap,
168
        array $searchBase,
169
        string $filter,
170
        array $options,
171
        bool $allowMissing
172
    ): array {
173
        $results = [];
174
175
        foreach ($searchBase as $base) {
176
            $query = $ldap->query($base, $filter, $options);
177
            $result = $query->execute();
178
            $results = array_merge($results, is_array($result) ? $result : $result->toArray());
0 ignored issues
show
Bug introduced by
It seems like is_array($result) ? $result : $result->toArray() can also be of type Symfony\Component\Ldap\Adapter\list; however, parameter $arrays of array_merge() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

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

178
            $results = array_merge($results, /** @scrutinizer ignore-type */ is_array($result) ? $result : $result->toArray());
Loading history...
179
180
            sprintf(
181
                "Library - LDAP search(): Found %d entries searching base '%s' for '%s'",
182
                count($result),
183
                $base,
184
                $filter,
185
            );
186
        }
187
188
        if (empty($results) && ($allowMissing === false)) {
189
            throw new Error\Exception(
190
                sprintf(
191
                    "No Objects found using search base [%s] and filter '%s'",
192
                    implode(', ', $searchBase),
193
                    $filter
194
                )
195
            );
196
        }
197
198
        return $results;
199
    }
200
201
202
    /**
203
     * Escapes the given VALUES according to RFC 2254 so that they can be safely used in LDAP filters.
204
     *
205
     * Any control characters with an ASCII code < 32 as well as the characters with special meaning in
206
     * LDAP filters "*", "(", ")", and "\" (the backslash) are converted into the representation of a
207
     * backslash followed by two hex digits representing the hexadecimal value of the character.
208
     *
209
     * @param string|string[] $values Array of values to escape
210
     * @param bool $singleValue
211
     * @return string|string[] Array $values, but escaped
212
     */
213
    public function escapeFilterValue($values = [], bool $singleValue = true)
214
    {
215
        // Parameter validation
216
        $arrayUtils = new Utils\Arrays();
217
        $values = $arrayUtils->arrayize($values);
218
219
        foreach ($values as $key => $val) {
220
            if ($val === null) {
221
                $val = '\0'; // apply escaped "null" if string is empty
222
            } else {
223
                // Escaping of filter meta characters
224
                $val = str_replace('\\', '\5c', $val);
225
                $val = str_replace('*', '\2a', $val);
226
                $val = str_replace('(', '\28', $val);
227
                $val = str_replace(')', '\29', $val);
228
229
                // ASCII < 32 escaping
230
                $val = $this->asc2hex32($val);
231
            }
232
233
            $values[$key] = $val;
234
        }
235
236
        if ($singleValue) {
237
            return $values[0];
238
        }
239
240
        return $values;
241
    }
242
243
244
    /**
245
     * Converts all ASCII chars < 32 to "\HEX"
246
     *
247
     * @param string $string String to convert
248
     * @return string
249
     */
250
    public function asc2hex32(string $string): string
251
    {
252
        for ($i = 0; $i < strlen($string); $i++) {
253
            $char = substr($string, $i, 1);
254
255
            if (ord($char) < 32) {
256
                $hex = dechex(ord($char));
257
                if (strlen($hex) == 1) {
258
                    $hex = '0' . $hex;
259
                }
260
261
                $string = str_replace($char, '\\' . $hex, $string);
262
            }
263
        }
264
265
        return $string;
266
    }
267
}
268