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\Entry\Attribute; 14use FreeDSx\Ldap\Entry\Dn; 15use FreeDSx\Ldap\Exception\InvalidArgumentException; 16use FreeDSx\Ldap\Exception\UrlParseException; 17 18/** 19 * Represents a LDAP URL. RFC 4516. 20 * 21 * @see https://tools.ietf.org/html/rfc4516 22 * @author Chad Sikorra <Chad.Sikorra@gmail.com> 23 */ 24class LdapUrl 25{ 26 use LdapUrlTrait; 27 28 public const SCOPE_BASE = 'base'; 29 30 public const SCOPE_ONE = 'one'; 31 32 public const SCOPE_SUB = 'sub'; 33 34 /** 35 * @var null|int 36 */ 37 protected $port; 38 39 /** 40 * @var bool 41 */ 42 protected $useSsl = false; 43 44 /** 45 * @var null|string 46 */ 47 protected $host; 48 49 /** 50 * @var null|Dn 51 */ 52 protected $dn; 53 54 /** 55 * @var null|string 56 */ 57 protected $scope; 58 59 /** 60 * @var Attribute[] 61 */ 62 protected $attributes = []; 63 64 /** 65 * @var null|string 66 */ 67 protected $filter; 68 69 /** 70 * @var LdapUrlExtension[] 71 */ 72 protected $extensions = []; 73 74 public function __construct(?string $host = null) 75 { 76 $this->host = $host; 77 } 78 79 /** 80 * @param null|string|Dn $dn 81 * @return $this 82 */ 83 public function setDn($dn) 84 { 85 $this->dn = $dn === null ? $dn : new Dn($dn); 86 87 return $this; 88 } 89 90 public function getDn(): ?Dn 91 { 92 return $this->dn; 93 } 94 95 public function getHost(): ?string 96 { 97 return $this->host; 98 } 99 100 /** 101 * @return $this 102 */ 103 public function setHost(?string $host) 104 { 105 $this->host = $host; 106 107 return $this; 108 } 109 110 /** 111 * @param int|null $port 112 * @return $this 113 */ 114 public function setPort(?int $port) 115 { 116 $this->port = $port; 117 118 return $this; 119 } 120 121 /** 122 * @return int|null 123 */ 124 public function getPort(): ?int 125 { 126 return $this->port; 127 } 128 129 /** 130 * @return null|string 131 */ 132 public function getScope(): ?string 133 { 134 return $this->scope; 135 } 136 137 /** 138 * @param null|string $scope 139 * @return $this 140 */ 141 public function setScope(?string $scope) 142 { 143 $scope = $scope === null ? $scope : strtolower($scope); 144 if ($scope !== null && !in_array($scope, [self::SCOPE_BASE, self::SCOPE_ONE, self::SCOPE_SUB], true)) { 145 throw new InvalidArgumentException(sprintf( 146 'The scope "%s" is not valid. It must be one of: %s, %s, %s', 147 $scope, 148 self::SCOPE_BASE, 149 self::SCOPE_ONE, 150 self::SCOPE_SUB 151 )); 152 } 153 $this->scope = $scope; 154 155 return $this; 156 } 157 158 /** 159 * @return null|string 160 */ 161 public function getFilter(): ?string 162 { 163 return $this->filter; 164 } 165 166 /** 167 * @param null|string $filter 168 * @return $this 169 */ 170 public function setFilter(?string $filter) 171 { 172 $this->filter = $filter; 173 174 return $this; 175 } 176 177 /** 178 * @return LdapUrlExtension[] 179 */ 180 public function getExtensions(): array 181 { 182 return $this->extensions; 183 } 184 185 /** 186 * @param LdapUrlExtension ...$extensions 187 * @return $this 188 */ 189 public function setExtensions(LdapUrlExtension ...$extensions) 190 { 191 $this->extensions = $extensions; 192 193 return $this; 194 } 195 196 /** 197 * @return Attribute[] 198 */ 199 public function getAttributes(): array 200 { 201 return $this->attributes; 202 } 203 204 /** 205 * @param string[]|Attribute[] ...$attributes 206 * @return $this 207 */ 208 public function setAttributes(...$attributes) 209 { 210 $attr = []; 211 foreach ($attributes as $attribute) { 212 $attr[] = $attribute instanceof Attribute ? $attribute : new Attribute($attribute); 213 } 214 $this->attributes = $attr; 215 216 return $this; 217 } 218 219 220 /** 221 * @param bool $useSsl 222 * @return LdapUrl 223 */ 224 public function setUseSsl(bool $useSsl) 225 { 226 $this->useSsl = $useSsl; 227 228 return $this; 229 } 230 231 /** 232 * @return bool 233 */ 234 public function getUseSsl(): bool 235 { 236 return $this->useSsl; 237 } 238 239 /** 240 * Get the string representation of the URL. 241 * 242 * @return string 243 */ 244 public function toString(): string 245 { 246 $url = ($this->useSsl ? 'ldaps' : 'ldap') . '://' . $this->host; 247 248 if ($this->host !== null && $this->port !== null) { 249 $url .= ':' . $this->port; 250 } 251 252 return $url . '/' . self::encode($this->dn) . $this->getQueryString(); 253 } 254 255 /** 256 * @return string 257 */ 258 public function __toString() 259 { 260 return $this->toString(); 261 } 262 263 /** 264 * Given a string LDAP URL, get its object representation. 265 * 266 * @param string $ldapUrl 267 * @return LdapUrl 268 * @throws UrlParseException 269 */ 270 public static function parse(string $ldapUrl): LdapUrl 271 { 272 $pieces = self::explodeUrl($ldapUrl); 273 274 $url = new LdapUrl($pieces['host'] ?? null); 275 $url->setUseSsl($pieces['scheme'] === 'ldaps'); 276 $url->setPort($pieces['port'] ?? null); 277 $url->setDn((isset($pieces['path']) && $pieces['path'] !== '/') ? self::decode(\ltrim($pieces['path'], '/')) : null); 278 279 $query = \explode('?', $pieces['query'] ?? ''); 280 if (\count($query) !== 0) { 281 $url->setAttributes(...($query[0] === '' ? [] : \explode(',', $query[0]))); 282 $url->setScope(isset($query[1]) && $query[1] !== '' ? $query[1] : null); 283 $url->setFilter(isset($query[2]) && $query[2] !== '' ? self::decode($query[2]) : null); 284 285 $extensions = []; 286 if (isset($query[3]) && $query[3] !== '') { 287 $extensions = \array_map(function ($ext) { 288 return LdapUrlExtension::parse($ext); 289 }, \explode(',', $query[3])); 290 } 291 $url->setExtensions(...$extensions); 292 } 293 294 return $url; 295 } 296 297 /** 298 * @param string $url 299 * @return array 300 * @throws UrlParseException 301 */ 302 protected static function explodeUrl(string $url): array 303 { 304 $pieces = \parse_url($url); 305 306 if ($pieces === false || !isset($pieces['scheme'])) { 307 # We are on our own here if it's an empty host, as parse_url will not treat it as valid, though it is valid 308 # for LDAP URLs. In the case of an empty host a client should determine what host to connect to. 309 if (\preg_match('/^(ldaps?)\:\/\/\/(.*)$/', $url, $matches) === 0) { 310 throw new UrlParseException(sprintf('The LDAP URL is malformed: %s', $url)); 311 } 312 $query = null; 313 $path = null; 314 315 # Check for query parameters but no path... 316 if (\strlen($matches[2]) > 0 && $matches[2][0] === '?') { 317 $query = substr($matches[2], 1); 318 # Check if there are any query parameters and a possible path... 319 } elseif (\strpos($matches[2], '?') !== false) { 320 $parts = \explode('?', $matches[2], 2); 321 $path = $parts[0]; 322 $query = isset($parts[1]) ? $parts[1] : null; 323 # A path only... 324 } else { 325 $path = $matches[2]; 326 } 327 328 $pieces = [ 329 'scheme' => $matches[1], 330 'path' => $path, 331 'query' => $query, 332 ]; 333 } 334 $pieces['scheme'] = \strtolower($pieces['scheme']); 335 336 if (!($pieces['scheme'] === 'ldap' || $pieces['scheme'] === 'ldaps')) { 337 throw new UrlParseException(sprintf('The URL scheme "%s" is not valid. It must be "ldap" or "ldaps".', $pieces['scheme'])); 338 } 339 340 return $pieces; 341 } 342 343 /** 344 * Generate the query part of the URL string representation. Only generates the parts actually used. 345 * 346 * @return string 347 */ 348 protected function getQueryString(): string 349 { 350 $query = []; 351 352 if (\count($this->attributes) !== 0) { 353 $query[0] = \implode(',', \array_map(function ($v) { 354 /** @var $v Attribute */ 355 return self::encode($v->getDescription()); 356 }, $this->attributes)); 357 } 358 if ($this->scope !== null) { 359 $query[1] = self::encode($this->scope); 360 } 361 if ($this->filter !== null) { 362 $query[2] = self::encode($this->filter); 363 } 364 if (\count($this->extensions) !== 0) { 365 $query[3] = \implode(',', $this->extensions); 366 } 367 368 if (\count($query) === 0) { 369 return ''; 370 } 371 372 \end($query); 373 $last = \key($query); 374 \reset($query); 375 376 # This is so we stop at the last query part that was actually set, but also capture cases where the first and 377 # third were set but not the second. 378 $url = ''; 379 for ($i = 0; $i <= $last; $i++) { 380 $url .= '?'; 381 if (isset($query[$i])) { 382 $url .= $query[$i]; 383 } 384 } 385 386 return $url; 387 } 388} 389