Passed
Pull Request — master (#18)
by Matt
02:33
created

LDAPIterator::getBaseDn()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 1
nc 1
nop 0
dl 0
loc 3
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace SilverStripe\LDAP\Iterators;
4
5
use \Iterator;
6
use Zend\Ldap\Ldap;
7
use Zend\Ldap\Exception;
8
use Zend\Ldap\ErrorHandler;
9
use Zend\Ldap\Exception\LdapException;
10
11
/**
12
 * Class LDAPIterator
13
 * @package SilverStripe\LDAP\Iterators
14
 *
15
 * Original implementation provided by ThaDafinser: https://gist.github.com/ThaDafinser/1d081bed8e5e6505e97bedf5863a187c
16
 * See also: https://github.com/zendframework/zend-ldap/issues/41
17
 *
18
 * This uses the ldap_control_paged_result() and ldap_control_paged_result_response() functions to request multiple
19
 * pages of data from LDAP as needed, to ensure that the end result is everything that matches the requested filter,
20
 * regardless of the maximum page size set by the LDAP server.
21
 *
22
 * Note: The page size attribute provided to this class must be less than the LDAP server's page size, or objects may
23
 * still be missing from results.
24
 */
25
final class LDAPIterator implements Iterator
26
{
27
    private $ldap;
28
    private $filter;
29
    private $baseDn;
30
    private $returnAttributes;
31
    private $pageSize;
32
    private $resolveRangedAttributes;
33
    private $entries;
34
    private $current;
35
    /**
36
     * Required for paging
37
     *
38
     * @var unknown
0 ignored issues
show
Bug introduced by
The type SilverStripe\LDAP\Iterators\unknown was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
39
     */
40
    private $currentResult;
0 ignored issues
show
introduced by
The private property $currentResult is not used, and could be removed.
Loading history...
41
    /**
42
     * Required for paging
43
     *
44
     * @var unknown
45
     */
46
    private $cookie = true;
47
48
    public function __construct(Ldap $ldap, string $filter, string $baseDn = null, array $returnAttributes = null, $pageSize = 250, bool $resolveRangedAttributes = false)
49
    {
50
        $this->ldap = $ldap;
51
        $this->filter = $filter;
52
        $this->baseDn = $baseDn;
53
        $this->returnAttributes = $returnAttributes;
54
        $this->pageSize = $pageSize;
55
        $this->resolveRangedAttributes = $resolveRangedAttributes;
56
    }
57
58
    private function getLdap()
59
    {
60
        return $this->ldap;
61
    }
62
63
    private function getFilter()
64
    {
65
        return $this->filter;
66
    }
67
68
    private function getBaseDn()
69
    {
70
        return $this->baseDn;
71
    }
72
73
    private function getReturnAttributes()
74
    {
75
        return $this->returnAttributes;
76
    }
77
78
    private function getPageSize()
79
    {
80
        return $this->pageSize;
81
    }
82
83
    /**
84
     * @return bool
85
     */
86
    private function getResolveRangedAttributes()
87
    {
88
        return $this->resolveRangedAttributes;
89
    }
90
91
    private function fetchPagedResult()
92
    {
93
        if ($this->cookie === null || $this->cookie === '') {
94
            return false;
95
        }
96
97
        if ($this->cookie === true) {
0 ignored issues
show
introduced by
The condition $this->cookie is always false. If $this->cookie can have other possible types, add them to src/Iterators/LDAPIterator.php:44
Loading history...
98
            // First fetch!
99
            $this->cookie = '';
100
        }
101
102
        $ldap = $this->getLdap();
103
        $resource = $ldap->getResource();
104
105
        $baseDn = $this->getBaseDn();
106
        if (!$baseDn) {
107
            $baseDn = $ldap->getBaseDn();
108
        }
109
110
        ldap_control_paged_result($resource, $this->getPageSize(), true, $this->cookie);
111
        if ($this->getReturnAttributes() !== null) {
112
            $resultResource = ldap_search($resource, $baseDn, $this->getFilter(), $this->getReturnAttributes());
113
        } else {
114
            $resultResource = ldap_search($resource, $baseDn, $this->getFilter());
115
        }
116
        if (! is_resource($resultResource)) {
117
            /*
118
             * @TODO better exception msg
119
             */
120
            throw new \Exception('ldap_search returned something wrong...' . ldap_error($resource));
121
        }
122
123
        $entries = ldap_get_entries($resource, $resultResource);
124
        if ($entries === false) {
125
            throw new LdapException($ldap, 'Entries could not get fetched');
126
        }
127
        $entries = $this->getConvertedEntries($entries);
128
129
        ErrorHandler::start();
130
        $response = ldap_control_paged_result_response($resource, $resultResource, $this->cookie);
131
        ErrorHandler::stop();
132
133
        if ($response !== true) {
134
            throw new LdapException($ldap, 'Paged result was empty');
135
        }
136
137
        if ($this->entries === null) {
138
            $this->entries = [];
139
        }
140
141
        $this->entries = array_merge($this->entries, $entries);
142
143
        return true;
144
    }
145
    private function getConvertedEntries(array $entries)
146
    {
147
        $result = [];
148
149
        foreach ($entries as $key => $entry) {
150
            if ($key === 'count') {
151
                continue;
152
            }
153
154
            $result[$key] = $this->getConvertedEntry($entry);
155
        }
156
157
        return $result;
158
    }
159
    private function getConvertedEntry(array $entry)
160
    {
161
        $result = [];
162
163
        foreach ($entry as $key => $value) {
164
            if (is_int($key)) {
165
                continue;
166
            }
167
            if ($key === 'count') {
168
                continue;
169
            }
170
171
            if (isset($value['count'])) {
172
                unset($value['count']);
173
            }
174
175
            $result[$key] = $value;
176
        }
177
178
        if ($this->getResolveRangedAttributes() === true) {
179
            $result = $this->resolveRangedAttributes($result);
180
        }
181
182
        return $result;
183
    }
184
    private function resolveRangedAttributes(array $row)
185
    {
186
        $result = [];
187
        foreach ($row as $key => $value) {
188
            $keyExploded = explode(';range=', $key);
189
190
            if (count($keyExploded) === 2) {
191
                $range = explode('-', $keyExploded[1]);
192
                $offsetAndLimit = (int) $range[1] + 1;
193
194
                $result[$keyExploded[0]] = array_merge($value, $this->getAttributeRecursive($row['dn'], $keyExploded[0], $offsetAndLimit, $offsetAndLimit));
195
            } else {
196
                $result[$key] = $value;
197
            }
198
        }
199
200
        return $result;
201
    }
202
    private function getAttributeRecursive(string $dn, string $attrName, int $offset, int $maxPerRequest)
203
    {
204
        $attributeValue = [];
205
206
        $limit = $offset + $maxPerRequest - 1;
207
        $searchedAttribute = $attrName . ';range=' . $offset . '-' . $limit;
208
209
        $ldap = $this->getLdap();
210
        $entry = $ldap->getEntry($dn, [
211
            $searchedAttribute
212
        ], true);
213
        foreach ($entry as $key => $value) {
214
            // skip DN and other fields (if returned)
215
            if (stripos($key, $attrName) === false) {
216
                continue;
217
            }
218
219
            $attributeValue = $value;
220
221
            // range result (pagination)
222
            $keyExploded = explode(';range=', $key);
223
224
            $range = explode('-', $keyExploded[1]);
225
            $rangeEnd = (int) $range[1];
226
227
            if ($range[0] == $offset && $range[1] == $limit) {
228
                // more pages, there are more pages to fetch
229
                $attributeValue = array_merge($attributeValue, $this->getAttributeRecursive($dn, $attrName, $rangeEnd + 1, $maxPerRequest));
230
            }
231
        }
232
233
        return $attributeValue;
234
    }
235
    public function current()
236
    {
237
        if (! is_array($this->current)) {
238
            $this->rewind();
239
        }
240
        if (! is_array($this->current)) {
241
            return;
242
        }
243
244
        return $this->current;
245
    }
246
    public function key()
247
    {
248
        if (! is_array($this->current)) {
249
            $this->rewind();
250
        }
251
        if (! is_array($this->current)) {
252
            return;
253
        }
254
255
        return $this->current['dn'];
256
    }
257
    public function next()
258
    {
259
        // initial
260
        if ($this->entries === null) {
261
            $this->fetchPagedResult();
262
        }
263
264
        next($this->entries);
265
266
        $this->current = current($this->entries);
267
    }
268
    public function rewind()
269
    {
270
        // initial
271
        if ($this->entries === null) {
272
            $this->fetchPagedResult();
273
        }
274
275
        reset($this->entries);
276
        $this->current = current($this->entries);
277
    }
278
    public function valid()
279
    {
280
        if (is_array($this->current)) {
281
            return true;
282
        }
283
284
        return $this->fetchPagedResult();
285
    }
286
}
287