blob: 2ce83fd2448a1ea135a9fbf9d3fe66d0587ca181 [file] [log] [blame]
Matthias Andreas Benkard7b2a3a12021-08-16 10:57:25 +02001<?php
2
3namespace Adldap\Connections;
4
5/**
6 * Class Ldap.
7 *
8 * A class that abstracts PHP's LDAP functions and stores the bound connection.
9 */
10class Ldap implements ConnectionInterface
11{
12 /**
13 * The connection name.
14 *
15 * @var string|null
16 */
17 protected $name;
18
19 /**
20 * The LDAP host that is currently connected.
21 *
22 * @var string|null
23 */
24 protected $host;
25
26 /**
27 * The active LDAP connection.
28 *
29 * @var resource
30 */
31 protected $connection;
32
33 /**
34 * The bound status of the connection.
35 *
36 * @var bool
37 */
38 protected $bound = false;
39
40 /**
41 * Whether the connection must be bound over SSL.
42 *
43 * @var bool
44 */
45 protected $useSSL = false;
46
47 /**
48 * Whether the connection must be bound over TLS.
49 *
50 * @var bool
51 */
52 protected $useTLS = false;
53
54 /**
55 * {@inheritdoc}
56 */
57 public function __construct($name = null)
58 {
59 $this->name = $name;
60 }
61
62 /**
63 * {@inheritdoc}
64 */
65 public function isUsingSSL()
66 {
67 return $this->useSSL;
68 }
69
70 /**
71 * {@inheritdoc}
72 */
73 public function isUsingTLS()
74 {
75 return $this->useTLS;
76 }
77
78 /**
79 * {@inheritdoc}
80 */
81 public function isBound()
82 {
83 return $this->bound;
84 }
85
86 /**
87 * {@inheritdoc}
88 */
89 public function canChangePasswords()
90 {
91 return $this->isUsingSSL() || $this->isUsingTLS();
92 }
93
94 /**
95 * {@inheritdoc}
96 */
97 public function ssl($enabled = true)
98 {
99 $this->useSSL = $enabled;
100
101 return $this;
102 }
103
104 /**
105 * {@inheritdoc}
106 */
107 public function tls($enabled = true)
108 {
109 $this->useTLS = $enabled;
110
111 return $this;
112 }
113
114 /**
115 * {@inheritdoc}
116 */
117 public function getHost()
118 {
119 return $this->host;
120 }
121
122 /**
123 * {@inheritdoc}
124 */
125 public function getName()
126 {
127 return $this->name;
128 }
129
130 /**
131 * {@inheritdoc}
132 */
133 public function getConnection()
134 {
135 return $this->connection;
136 }
137
138 /**
139 * {@inheritdoc}
140 */
141 public function getEntries($searchResults)
142 {
143 return ldap_get_entries($this->connection, $searchResults);
144 }
145
146 /**
147 * {@inheritdoc}
148 */
149 public function getFirstEntry($searchResults)
150 {
151 return ldap_first_entry($this->connection, $searchResults);
152 }
153
154 /**
155 * {@inheritdoc}
156 */
157 public function getNextEntry($entry)
158 {
159 return ldap_next_entry($this->connection, $entry);
160 }
161
162 /**
163 * {@inheritdoc}
164 */
165 public function getAttributes($entry)
166 {
167 return ldap_get_attributes($this->connection, $entry);
168 }
169
170 /**
171 * {@inheritdoc}
172 */
173 public function countEntries($searchResults)
174 {
175 return ldap_count_entries($this->connection, $searchResults);
176 }
177
178 /**
179 * {@inheritdoc}
180 */
181 public function compare($dn, $attribute, $value)
182 {
183 return ldap_compare($this->connection, $dn, $attribute, $value);
184 }
185
186 /**
187 * {@inheritdoc}
188 */
189 public function getLastError()
190 {
191 return ldap_error($this->connection);
192 }
193
194 /**
195 * {@inheritdoc}
196 */
197 public function getDetailedError()
198 {
199 // If the returned error number is zero, the last LDAP operation
200 // succeeded. We won't return a detailed error.
201 if ($number = $this->errNo()) {
202 ldap_get_option($this->connection, LDAP_OPT_DIAGNOSTIC_MESSAGE, $message);
203
204 return new DetailedError($number, $this->err2Str($number), $message);
205 }
206 }
207
208 /**
209 * {@inheritdoc}
210 */
211 public function getValuesLen($entry, $attribute)
212 {
213 return ldap_get_values_len($this->connection, $entry, $attribute);
214 }
215
216 /**
217 * {@inheritdoc}
218 */
219 public function setOption($option, $value)
220 {
221 return ldap_set_option($this->connection, $option, $value);
222 }
223
224 /**
225 * {@inheritdoc}
226 */
227 public function setOptions(array $options = [])
228 {
229 foreach ($options as $option => $value) {
230 $this->setOption($option, $value);
231 }
232 }
233
234 /**
235 * {@inheritdoc}
236 */
237 public function setRebindCallback(callable $callback)
238 {
239 return ldap_set_rebind_proc($this->connection, $callback);
240 }
241
242 /**
243 * {@inheritdoc}
244 */
245 public function startTLS()
246 {
247 try {
248 return ldap_start_tls($this->connection);
249 } catch (\ErrorException $e) {
250 throw new ConnectionException($e->getMessage(), $e->getCode(), $e);
251 }
252 }
253
254 /**
255 * {@inheritdoc}
256 */
257 public function connect($hosts = [], $port = 389)
258 {
259 $this->host = $this->getConnectionString($hosts, $this->getProtocol(), $port);
260
261 // Reset the bound status if reinitializing the connection.
262 $this->bound = false;
263
264 return $this->connection = ldap_connect($this->host);
265 }
266
267 /**
268 * {@inheritdoc}
269 */
270 public function close()
271 {
272 $connection = $this->connection;
273
274 $result = is_resource($connection) ? ldap_close($connection) : false;
275
276 $this->bound = false;
277
278 return $result;
279 }
280
281 /**
282 * {@inheritdoc}
283 */
284 public function search($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0)
285 {
286 return ldap_search($this->connection, $dn, $filter, $fields, $onlyAttributes, $size, $time);
287 }
288
289 /**
290 * {@inheritdoc}
291 */
292 public function listing($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0)
293 {
294 return ldap_list($this->connection, $dn, $filter, $fields, $onlyAttributes, $size, $time);
295 }
296
297 /**
298 * {@inheritdoc}
299 */
300 public function read($dn, $filter, array $fields, $onlyAttributes = false, $size = 0, $time = 0)
301 {
302 return ldap_read($this->connection, $dn, $filter, $fields, $onlyAttributes, $size, $time);
303 }
304
305 /**
306 * Extract information from an LDAP result.
307 *
308 * @link https://www.php.net/manual/en/function.ldap-parse-result.php
309 *
310 * @param resource $result
311 * @param int $errorCode
312 * @param string $dn
313 * @param string $errorMessage
314 * @param array $referrals
315 * @param array $serverControls
316 *
317 * @return bool
318 */
319 public function parseResult($result, &$errorCode, &$dn, &$errorMessage, &$referrals, &$serverControls = [])
320 {
321 return $this->supportsServerControlsInMethods() && !empty($serverControls) ?
322 ldap_parse_result($this->connection, $result, $errorCode, $dn, $errorMessage, $referrals, $serverControls) :
323 ldap_parse_result($this->connection, $result, $errorCode, $dn, $errorMessage, $referrals);
324 }
325
326 /**
327 * {@inheritdoc}
328 */
329 public function bind($username, $password, $sasl = false)
330 {
331 // Prior to binding, we will upgrade our connectivity to TLS on our current
332 // connection and ensure we are not already bound before upgrading.
333 // This is to prevent subsequent upgrading on several binds.
334 if ($this->isUsingTLS() && !$this->isBound()) {
335 $this->startTLS();
336 }
337
338 if ($sasl) {
339 return $this->bound = ldap_sasl_bind($this->connection, null, null, 'GSSAPI');
340 }
341
342 return $this->bound = ldap_bind(
343 $this->connection,
344 $username,
345 html_entity_decode($password)
346 );
347 }
348
349 /**
350 * {@inheritdoc}
351 */
352 public function add($dn, array $entry)
353 {
354 return ldap_add($this->connection, $dn, $entry);
355 }
356
357 /**
358 * {@inheritdoc}
359 */
360 public function delete($dn)
361 {
362 return ldap_delete($this->connection, $dn);
363 }
364
365 /**
366 * {@inheritdoc}
367 */
368 public function rename($dn, $newRdn, $newParent, $deleteOldRdn = false)
369 {
370 return ldap_rename($this->connection, $dn, $newRdn, $newParent, $deleteOldRdn);
371 }
372
373 /**
374 * {@inheritdoc}
375 */
376 public function modify($dn, array $entry)
377 {
378 return ldap_modify($this->connection, $dn, $entry);
379 }
380
381 /**
382 * {@inheritdoc}
383 */
384 public function modifyBatch($dn, array $values)
385 {
386 return ldap_modify_batch($this->connection, $dn, $values);
387 }
388
389 /**
390 * {@inheritdoc}
391 */
392 public function modAdd($dn, array $entry)
393 {
394 return ldap_mod_add($this->connection, $dn, $entry);
395 }
396
397 /**
398 * {@inheritdoc}
399 */
400 public function modReplace($dn, array $entry)
401 {
402 return ldap_mod_replace($this->connection, $dn, $entry);
403 }
404
405 /**
406 * {@inheritdoc}
407 */
408 public function modDelete($dn, array $entry)
409 {
410 return ldap_mod_del($this->connection, $dn, $entry);
411 }
412
413 /**
414 * {@inheritdoc}
415 */
416 public function controlPagedResult($pageSize = 1000, $isCritical = false, $cookie = '')
417 {
418 return ldap_control_paged_result($this->connection, $pageSize, $isCritical, $cookie);
419 }
420
421 /**
422 * {@inheritdoc}
423 */
424 public function controlPagedResultResponse($result, &$cookie)
425 {
426 return ldap_control_paged_result_response($this->connection, $result, $cookie);
427 }
428
429 /**
430 * {@inheritdoc}
431 */
432 public function freeResult($result)
433 {
434 return ldap_free_result($result);
435 }
436
437 /**
438 * {@inheritdoc}
439 */
440 public function errNo()
441 {
442 return ldap_errno($this->connection);
443 }
444
445 /**
446 * {@inheritdoc}
447 */
448 public function getExtendedError()
449 {
450 return $this->getDiagnosticMessage();
451 }
452
453 /**
454 * {@inheritdoc}
455 */
456 public function getExtendedErrorHex()
457 {
458 if (preg_match("/(?<=data\s).*?(?=\,)/", $this->getExtendedError(), $code)) {
459 return $code[0];
460 }
461 }
462
463 /**
464 * {@inheritdoc}
465 */
466 public function getExtendedErrorCode()
467 {
468 return $this->extractDiagnosticCode($this->getExtendedError());
469 }
470
471 /**
472 * {@inheritdoc}
473 */
474 public function err2Str($number)
475 {
476 return ldap_err2str($number);
477 }
478
479 /**
480 * {@inheritdoc}
481 */
482 public function getDiagnosticMessage()
483 {
484 ldap_get_option($this->connection, LDAP_OPT_ERROR_STRING, $message);
485
486 return $message;
487 }
488
489 /**
490 * {@inheritdoc}
491 */
492 public function extractDiagnosticCode($message)
493 {
494 preg_match('/^([\da-fA-F]+):/', $message, $matches);
495
496 return isset($matches[1]) ? $matches[1] : false;
497 }
498
499 /**
500 * Returns the LDAP protocol to utilize for the current connection.
501 *
502 * @return string
503 */
504 public function getProtocol()
505 {
506 return $this->isUsingSSL() ? $this::PROTOCOL_SSL : $this::PROTOCOL;
507 }
508
509 /**
510 * Determine if the current PHP version supports server controls.
511 *
512 * @return bool
513 */
514 public function supportsServerControlsInMethods()
515 {
516 return version_compare(PHP_VERSION, '7.3.0') >= 0;
517 }
518
519 /**
520 * Generates an LDAP connection string for each host given.
521 *
522 * @param string|array $hosts
523 * @param string $protocol
524 * @param string $port
525 *
526 * @return string
527 */
528 protected function getConnectionString($hosts, $protocol, $port)
529 {
530 // If we are using SSL and using the default port, we
531 // will override it to use the default SSL port.
532 if ($this->isUsingSSL() && $port == 389) {
533 $port = self::PORT_SSL;
534 }
535
536 // Normalize hosts into an array.
537 $hosts = is_array($hosts) ? $hosts : [$hosts];
538
539 $hosts = array_map(function ($host) use ($protocol, $port) {
540 return "{$protocol}{$host}:{$port}";
541 }, $hosts);
542
543 return implode(' ', $hosts);
544 }
545}