1<?php 2/** 3 * This file is part of the FreeDSx LDAP package. 4 * 5 * (c) Chad Sikorra <Chad.Sikorra@gmail.com> 6 * 7 * For the full copyright and license information, please view the LICENSE 8 * file that was distributed with this source code. 9 */ 10 11namespace FreeDSx\Ldap; 12 13use FreeDSx\Ldap\Control\Control; 14use FreeDSx\Ldap\Control\ControlBag; 15use FreeDSx\Ldap\Control\Sorting\SortingControl; 16use FreeDSx\Ldap\Control\Sorting\SortKey; 17use FreeDSx\Ldap\Entry\Entries; 18use FreeDSx\Ldap\Entry\Entry; 19use FreeDSx\Ldap\Exception\BindException; 20use FreeDSx\Ldap\Exception\OperationException; 21use FreeDSx\Ldap\Operation\Request\ExtendedRequest; 22use FreeDSx\Ldap\Operation\Request\RequestInterface; 23use FreeDSx\Ldap\Operation\Request\SearchRequest; 24use FreeDSx\Ldap\Operation\ResultCode; 25use FreeDSx\Ldap\Protocol\ClientProtocolHandler; 26use FreeDSx\Ldap\Protocol\LdapMessageResponse; 27use FreeDSx\Ldap\Search\DirSync; 28use FreeDSx\Ldap\Search\Filter\FilterInterface; 29use FreeDSx\Ldap\Search\Paging; 30use FreeDSx\Ldap\Search\RangeRetrieval; 31use FreeDSx\Ldap\Search\Vlv; 32 33/** 34 * The LDAP client. 35 * 36 * @author Chad Sikorra <Chad.Sikorra@gmail.com> 37 */ 38class LdapClient 39{ 40 public const REFERRAL_IGNORE = 'ignore'; 41 42 public const REFERRAL_FOLLOW = 'follow'; 43 44 public const REFERRAL_THROW = 'throw'; 45 46 /** 47 * @var array 48 */ 49 protected $options = [ 50 'version' => 3, 51 'servers' => [], 52 'port' => 389, 53 'base_dn' => null, 54 'page_size' => 1000, 55 'use_ssl' => false, 56 'ssl_validate_cert' => true, 57 'ssl_allow_self_signed' => null, 58 'ssl_ca_cert' => null, 59 'ssl_peer_name' => null, 60 'timeout_connect' => 3, 61 'timeout_read' => 10, 62 'referral' => 'throw', 63 'referral_chaser' => null, 64 'referral_limit' => 10, 65 ]; 66 67 /** 68 * @var ClientProtocolHandler|null 69 */ 70 protected $handler; 71 72 /** 73 * @param array $options 74 */ 75 public function __construct(array $options = []) 76 { 77 $this->options = array_merge($this->options, $options); 78 } 79 80 /** 81 * A Simple Bind to LDAP with a username and password. 82 * 83 * @param string $username 84 * @param string $password 85 * @return LdapMessageResponse 86 * @throws BindException 87 * @throws OperationException 88 */ 89 public function bind(string $username, string $password): LdapMessageResponse 90 { 91 return $this->sendAndReceive(Operations::bind($username, $password)->setVersion($this->options['version'])); 92 } 93 94 /** 95 * A SASL Bind to LDAP with SASL options and an optional specific mechanism type. 96 * 97 * @param array $options The SASL options (ie. ['username' => '...', 'password' => '...']) 98 * @param string $mechanism A specific mechanism to use. If none is supplied, one will be selected. 99 * @return LdapMessageResponse 100 * @throws BindException 101 * @throws OperationException 102 */ 103 public function bindSasl(array $options = [], string $mechanism = ''): LdapMessageResponse 104 { 105 return $this->sendAndReceive(Operations::bindSasl($options, $mechanism)->setVersion($this->options['version'])); 106 } 107 108 /** 109 * Check whether or not an entry matches a certain attribute and value. 110 * 111 * @param string|\FreeDSx\Ldap\Entry\Dn $dn 112 * @param string $attributeName 113 * @param string $value 114 * @param Control ...$controls 115 * @return bool 116 * @throws OperationException 117 */ 118 public function compare($dn, string $attributeName, string $value, Control ...$controls): bool 119 { 120 /** @var \FreeDSx\Ldap\Operation\Response\CompareResponse $response */ 121 $response = $this->sendAndReceive(Operations::compare($dn, $attributeName, $value), ...$controls)->getResponse(); 122 123 return $response->getResultCode() === ResultCode::COMPARE_TRUE; 124 } 125 126 /** 127 * Create a new entry. 128 * 129 * @param Entry $entry 130 * @param Control ...$controls 131 * @return LdapMessageResponse 132 * @throws OperationException 133 */ 134 public function create(Entry $entry, Control ...$controls): LdapMessageResponse 135 { 136 $response = $this->sendAndReceive(Operations::add($entry), ...$controls); 137 $entry->changes()->reset(); 138 139 return $response; 140 } 141 142 /** 143 * Read an entry. 144 * 145 * @param string $entry 146 * @param string[] $attributes 147 * @param Control ...$controls 148 * @return Entry|null 149 * @throws Exception\OperationException 150 */ 151 public function read(string $entry = '', $attributes = [], Control ...$controls): ?Entry 152 { 153 try { 154 return $this->readOrFail($entry, $attributes, ...$controls); 155 } catch (Exception\OperationException $e) { 156 if ($e->getCode() === ResultCode::NO_SUCH_OBJECT) { 157 return null; 158 } 159 throw $e; 160 } 161 } 162 163 /** 164 * Read an entry from LDAP. If the entry is not found an OperationException is thrown. 165 * 166 * @param string $entry 167 * @param string[] $attributes 168 * @param Control ...$controls 169 * @return Entry 170 * @throws OperationException 171 */ 172 public function readOrFail(string $entry = '', $attributes = [], Control ...$controls): Entry 173 { 174 $entryObj = $this->search(Operations::read($entry, ...$attributes), ...$controls)->first(); 175 if ($entryObj === null) { 176 throw new OperationException(sprintf( 177 'The entry "%s" was not found.', 178 $entry 179 ), ResultCode::NO_SUCH_OBJECT); 180 } 181 182 return $entryObj; 183 } 184 185 /** 186 * Delete an entry. 187 * 188 * @param string $entry 189 * @param Control ...$controls 190 * @return LdapMessageResponse 191 * @throws OperationException 192 */ 193 public function delete(string $entry, Control ...$controls): LdapMessageResponse 194 { 195 return $this->sendAndReceive(Operations::delete($entry), ...$controls); 196 } 197 198 /** 199 * Update an existing entry. 200 * 201 * @param Entry $entry 202 * @param Control ...$controls 203 * @return LdapMessageResponse 204 * @throws OperationException 205 */ 206 public function update(Entry $entry, Control ...$controls): LdapMessageResponse 207 { 208 $response = $this->sendAndReceive(Operations::modify($entry->getDn(), ...$entry->changes()), ...$controls); 209 $entry->changes()->reset(); 210 211 return $response; 212 } 213 214 /** 215 * Move an entry to a new location. 216 * 217 * @param string|Entry $dn 218 * @param string|Entry $newParentDn 219 * @return LdapMessageResponse 220 * @throws OperationException 221 */ 222 public function move($dn, $newParentDn): LdapMessageResponse 223 { 224 return $this->sendAndReceive(Operations::move($dn, $newParentDn)); 225 } 226 227 /** 228 * Rename an entry (changing the RDN). 229 * 230 * @param string|Entry $dn 231 * @param string $newRdn 232 * @param bool $deleteOldRdn 233 * @return LdapMessageResponse 234 * @throws OperationException 235 */ 236 public function rename($dn, $newRdn, bool $deleteOldRdn = true): LdapMessageResponse 237 { 238 return $this->sendAndReceive(Operations::rename($dn, $newRdn, $deleteOldRdn)); 239 } 240 241 /** 242 * Send a search response and return the entries. 243 * 244 * @param SearchRequest $request 245 * @param Control ...$controls 246 * @return \FreeDSx\Ldap\Entry\Entries 247 * @throws OperationException 248 */ 249 public function search(SearchRequest $request, Control ...$controls): Entries 250 { 251 /** @var \FreeDSx\Ldap\Operation\Response\SearchResponse $response */ 252 $response = $this->sendAndReceive($request, ...$controls)->getResponse(); 253 254 return $response->getEntries(); 255 } 256 257 /** 258 * A helper for performing a paging based search. 259 * 260 * @param SearchRequest $search 261 * @param int $size 262 * @return Paging 263 */ 264 public function paging(SearchRequest $search, ?int $size = null): Paging 265 { 266 return new Paging($this, $search, $size ?? $this->options['page_size']); 267 } 268 269 /** 270 * A helper for performing a VLV (Virtual List View) based search. 271 * 272 * @param SearchRequest $search 273 * @param SortingControl|string|SortKey $sort 274 * @param int $afterCount 275 * @return Vlv 276 */ 277 public function vlv(SearchRequest $search, $sort, int $afterCount): Vlv 278 { 279 return new Vlv($this, $search, $sort, $afterCount); 280 } 281 282 /** 283 * A helper for performing a DirSync search operation against AD. 284 * 285 * @param string|null $rootNc 286 * @param FilterInterface|null $filter 287 * @param mixed ...$attributes 288 * @return DirSync 289 */ 290 public function dirSync(?string $rootNc = null, FilterInterface $filter = null, ...$attributes): DirSync 291 { 292 return new DirSync($this, $rootNc, $filter, ...$attributes); 293 } 294 295 /** 296 * Send a request operation to LDAP. This may return null if the request expects no response. 297 * 298 * @param RequestInterface $request 299 * @param Control ...$controls 300 * @return LdapMessageResponse|null 301 * @throws Exception\ConnectionException 302 * @throws Exception\UnsolicitedNotificationException 303 * @throws OperationException 304 */ 305 public function send(RequestInterface $request, Control ...$controls): ?LdapMessageResponse 306 { 307 return $this->handler()->send($request, ...$controls); 308 } 309 310 /** 311 * Send a request to LDAP that expects a response. If none is received an OperationException is thrown. 312 * 313 * @param RequestInterface $request 314 * @param Control ...$controls 315 * @return LdapMessageResponse 316 * @throws OperationException 317 */ 318 public function sendAndReceive(RequestInterface $request, Control ...$controls): LdapMessageResponse 319 { 320 $response = $this->send($request, ...$controls); 321 if ($response === null) { 322 throw new OperationException('Expected an LDAP message response, but none was received.'); 323 } 324 325 return $response; 326 } 327 328 /** 329 * Issue a startTLS to encrypt the LDAP connection. 330 * 331 * @return $this 332 * @throws OperationException 333 */ 334 public function startTls() 335 { 336 $this->send(Operations::extended(ExtendedRequest::OID_START_TLS)); 337 338 return $this; 339 } 340 341 /** 342 * Unbind and close the LDAP TCP connection. 343 * 344 * @return $this 345 * @throws OperationException 346 */ 347 public function unbind() 348 { 349 $this->send(Operations::unbind()); 350 351 return $this; 352 } 353 354 /** 355 * Perform a whoami request and get the returned value. 356 * 357 * @return string 358 * @throws OperationException 359 */ 360 public function whoami(): ?string 361 { 362 /** @var \FreeDSx\Ldap\Operation\Response\ExtendedResponse $response */ 363 $response = $this->sendAndReceive(Operations::whoami())->getResponse(); 364 365 return $response->getValue(); 366 } 367 368 /** 369 * Get a helper class for handling ranged attributes. 370 * 371 * @return RangeRetrieval 372 */ 373 public function range(): RangeRetrieval 374 { 375 return new RangeRetrieval($this); 376 } 377 378 /** 379 * Access to add/set/remove/reset the controls to be used for each request. If you want request specific controls in 380 * addition to these, then pass them as a parameter to the send() method. 381 * 382 * @return ControlBag 383 */ 384 public function controls(): ControlBag 385 { 386 return $this->handler()->controls(); 387 } 388 389 /** 390 * Get the options currently set. 391 * 392 * @return array 393 */ 394 public function getOptions(): array 395 { 396 return $this->options; 397 } 398 399 /** 400 * Merge a set of options. 401 * 402 * @param array $options 403 * @return $this 404 */ 405 public function setOptions(array $options) 406 { 407 $this->options = array_merge($this->options, $options); 408 409 return $this; 410 } 411 412 /** 413 * @param ClientProtocolHandler|null $handler 414 * @return $this 415 */ 416 public function setProtocolHandler(ClientProtocolHandler $handler = null) 417 { 418 $this->handler = $handler; 419 420 return $this; 421 } 422 423 /** 424 * A simple check to determine if this client has an established connection to a server. 425 * 426 * @return bool 427 */ 428 public function isConnected(): bool 429 { 430 return ($this->handler !== null && $this->handler->isConnected()); 431 } 432 433 /** 434 * Try to clean-up if needed. 435 */ 436 public function __destruct() 437 { 438 if ($this->handler !== null && $this->handler->isConnected()) { 439 $this->unbind(); 440 } 441 } 442 443 protected function handler(): ClientProtocolHandler 444 { 445 if ($this->handler === null) { 446 $this->handler = new Protocol\ClientProtocolHandler($this->options); 447 } 448 449 return $this->handler; 450 } 451} 452