xref: /plugin/pureldap/vendor/freedsx/ldap/src/FreeDSx/Ldap/LdapUrl.php (revision dad993c57a70866aa1db59c43f043769c2eb7ed0)
10b3fd2d3SAndreas Gohr<?php
2*dad993c5SAndreas Gohr
30b3fd2d3SAndreas Gohr/**
40b3fd2d3SAndreas Gohr * This file is part of the FreeDSx LDAP package.
50b3fd2d3SAndreas Gohr *
60b3fd2d3SAndreas Gohr * (c) Chad Sikorra <Chad.Sikorra@gmail.com>
70b3fd2d3SAndreas Gohr *
80b3fd2d3SAndreas Gohr * For the full copyright and license information, please view the LICENSE
90b3fd2d3SAndreas Gohr * file that was distributed with this source code.
100b3fd2d3SAndreas Gohr */
110b3fd2d3SAndreas Gohr
120b3fd2d3SAndreas Gohrnamespace FreeDSx\Ldap;
130b3fd2d3SAndreas Gohr
140b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Entry\Attribute;
150b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Entry\Dn;
160b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Exception\InvalidArgumentException;
170b3fd2d3SAndreas Gohruse FreeDSx\Ldap\Exception\UrlParseException;
18*dad993c5SAndreas Gohruse function array_map;
19*dad993c5SAndreas Gohruse function count;
20*dad993c5SAndreas Gohruse function end;
21*dad993c5SAndreas Gohruse function explode;
22*dad993c5SAndreas Gohruse function implode;
23*dad993c5SAndreas Gohruse function key;
24*dad993c5SAndreas Gohruse function ltrim;
25*dad993c5SAndreas Gohruse function parse_url;
26*dad993c5SAndreas Gohruse function preg_match;
27*dad993c5SAndreas Gohruse function reset;
28*dad993c5SAndreas Gohruse function strlen;
29*dad993c5SAndreas Gohruse function strpos;
30*dad993c5SAndreas Gohruse function strtolower;
310b3fd2d3SAndreas Gohr
320b3fd2d3SAndreas Gohr/**
330b3fd2d3SAndreas Gohr * Represents a LDAP URL. RFC 4516.
340b3fd2d3SAndreas Gohr *
350b3fd2d3SAndreas Gohr * @see https://tools.ietf.org/html/rfc4516
360b3fd2d3SAndreas Gohr * @author Chad Sikorra <Chad.Sikorra@gmail.com>
370b3fd2d3SAndreas Gohr */
380b3fd2d3SAndreas Gohrclass LdapUrl
390b3fd2d3SAndreas Gohr{
400b3fd2d3SAndreas Gohr    use LdapUrlTrait;
410b3fd2d3SAndreas Gohr
420b3fd2d3SAndreas Gohr    public const SCOPE_BASE = 'base';
430b3fd2d3SAndreas Gohr
440b3fd2d3SAndreas Gohr    public const SCOPE_ONE = 'one';
450b3fd2d3SAndreas Gohr
460b3fd2d3SAndreas Gohr    public const SCOPE_SUB = 'sub';
470b3fd2d3SAndreas Gohr
480b3fd2d3SAndreas Gohr    /**
490b3fd2d3SAndreas Gohr     * @var null|int
500b3fd2d3SAndreas Gohr     */
510b3fd2d3SAndreas Gohr    protected $port;
520b3fd2d3SAndreas Gohr
530b3fd2d3SAndreas Gohr    /**
540b3fd2d3SAndreas Gohr     * @var bool
550b3fd2d3SAndreas Gohr     */
560b3fd2d3SAndreas Gohr    protected $useSsl = false;
570b3fd2d3SAndreas Gohr
580b3fd2d3SAndreas Gohr    /**
590b3fd2d3SAndreas Gohr     * @var null|string
600b3fd2d3SAndreas Gohr     */
610b3fd2d3SAndreas Gohr    protected $host;
620b3fd2d3SAndreas Gohr
630b3fd2d3SAndreas Gohr    /**
640b3fd2d3SAndreas Gohr     * @var null|Dn
650b3fd2d3SAndreas Gohr     */
660b3fd2d3SAndreas Gohr    protected $dn;
670b3fd2d3SAndreas Gohr
680b3fd2d3SAndreas Gohr    /**
690b3fd2d3SAndreas Gohr     * @var null|string
700b3fd2d3SAndreas Gohr     */
710b3fd2d3SAndreas Gohr    protected $scope;
720b3fd2d3SAndreas Gohr
730b3fd2d3SAndreas Gohr    /**
740b3fd2d3SAndreas Gohr     * @var Attribute[]
750b3fd2d3SAndreas Gohr     */
760b3fd2d3SAndreas Gohr    protected $attributes = [];
770b3fd2d3SAndreas Gohr
780b3fd2d3SAndreas Gohr    /**
790b3fd2d3SAndreas Gohr     * @var null|string
800b3fd2d3SAndreas Gohr     */
810b3fd2d3SAndreas Gohr    protected $filter;
820b3fd2d3SAndreas Gohr
830b3fd2d3SAndreas Gohr    /**
840b3fd2d3SAndreas Gohr     * @var LdapUrlExtension[]
850b3fd2d3SAndreas Gohr     */
860b3fd2d3SAndreas Gohr    protected $extensions = [];
870b3fd2d3SAndreas Gohr
880b3fd2d3SAndreas Gohr    public function __construct(?string $host = null)
890b3fd2d3SAndreas Gohr    {
900b3fd2d3SAndreas Gohr        $this->host = $host;
910b3fd2d3SAndreas Gohr    }
920b3fd2d3SAndreas Gohr
930b3fd2d3SAndreas Gohr    /**
940b3fd2d3SAndreas Gohr     * @param null|string|Dn $dn
950b3fd2d3SAndreas Gohr     * @return $this
960b3fd2d3SAndreas Gohr     */
970b3fd2d3SAndreas Gohr    public function setDn($dn)
980b3fd2d3SAndreas Gohr    {
990b3fd2d3SAndreas Gohr        $this->dn = $dn === null ? $dn : new Dn($dn);
1000b3fd2d3SAndreas Gohr
1010b3fd2d3SAndreas Gohr        return $this;
1020b3fd2d3SAndreas Gohr    }
1030b3fd2d3SAndreas Gohr
1040b3fd2d3SAndreas Gohr    public function getDn(): ?Dn
1050b3fd2d3SAndreas Gohr    {
1060b3fd2d3SAndreas Gohr        return $this->dn;
1070b3fd2d3SAndreas Gohr    }
1080b3fd2d3SAndreas Gohr
1090b3fd2d3SAndreas Gohr    public function getHost(): ?string
1100b3fd2d3SAndreas Gohr    {
1110b3fd2d3SAndreas Gohr        return $this->host;
1120b3fd2d3SAndreas Gohr    }
1130b3fd2d3SAndreas Gohr
1140b3fd2d3SAndreas Gohr    /**
1150b3fd2d3SAndreas Gohr     * @return $this
1160b3fd2d3SAndreas Gohr     */
1170b3fd2d3SAndreas Gohr    public function setHost(?string $host)
1180b3fd2d3SAndreas Gohr    {
1190b3fd2d3SAndreas Gohr        $this->host = $host;
1200b3fd2d3SAndreas Gohr
1210b3fd2d3SAndreas Gohr        return $this;
1220b3fd2d3SAndreas Gohr    }
1230b3fd2d3SAndreas Gohr
1240b3fd2d3SAndreas Gohr    /**
1250b3fd2d3SAndreas Gohr     * @param int|null $port
1260b3fd2d3SAndreas Gohr     * @return $this
1270b3fd2d3SAndreas Gohr     */
1280b3fd2d3SAndreas Gohr    public function setPort(?int $port)
1290b3fd2d3SAndreas Gohr    {
1300b3fd2d3SAndreas Gohr        $this->port = $port;
1310b3fd2d3SAndreas Gohr
1320b3fd2d3SAndreas Gohr        return $this;
1330b3fd2d3SAndreas Gohr    }
1340b3fd2d3SAndreas Gohr
1350b3fd2d3SAndreas Gohr    /**
1360b3fd2d3SAndreas Gohr     * @return int|null
1370b3fd2d3SAndreas Gohr     */
1380b3fd2d3SAndreas Gohr    public function getPort(): ?int
1390b3fd2d3SAndreas Gohr    {
1400b3fd2d3SAndreas Gohr        return $this->port;
1410b3fd2d3SAndreas Gohr    }
1420b3fd2d3SAndreas Gohr
1430b3fd2d3SAndreas Gohr    /**
1440b3fd2d3SAndreas Gohr     * @return null|string
1450b3fd2d3SAndreas Gohr     */
1460b3fd2d3SAndreas Gohr    public function getScope(): ?string
1470b3fd2d3SAndreas Gohr    {
1480b3fd2d3SAndreas Gohr        return $this->scope;
1490b3fd2d3SAndreas Gohr    }
1500b3fd2d3SAndreas Gohr
1510b3fd2d3SAndreas Gohr    /**
1520b3fd2d3SAndreas Gohr     * @param null|string $scope
1530b3fd2d3SAndreas Gohr     * @return $this
154*dad993c5SAndreas Gohr     * @throws InvalidArgumentException
1550b3fd2d3SAndreas Gohr     */
1560b3fd2d3SAndreas Gohr    public function setScope(?string $scope)
1570b3fd2d3SAndreas Gohr    {
1580b3fd2d3SAndreas Gohr        $scope = $scope === null ? $scope : strtolower($scope);
1590b3fd2d3SAndreas Gohr        if ($scope !== null && !in_array($scope, [self::SCOPE_BASE, self::SCOPE_ONE, self::SCOPE_SUB], true)) {
1600b3fd2d3SAndreas Gohr            throw new InvalidArgumentException(sprintf(
1610b3fd2d3SAndreas Gohr                'The scope "%s" is not valid. It must be one of: %s, %s, %s',
1620b3fd2d3SAndreas Gohr                $scope,
1630b3fd2d3SAndreas Gohr                self::SCOPE_BASE,
1640b3fd2d3SAndreas Gohr                self::SCOPE_ONE,
1650b3fd2d3SAndreas Gohr                self::SCOPE_SUB
1660b3fd2d3SAndreas Gohr            ));
1670b3fd2d3SAndreas Gohr        }
1680b3fd2d3SAndreas Gohr        $this->scope = $scope;
1690b3fd2d3SAndreas Gohr
1700b3fd2d3SAndreas Gohr        return $this;
1710b3fd2d3SAndreas Gohr    }
1720b3fd2d3SAndreas Gohr
1730b3fd2d3SAndreas Gohr    /**
1740b3fd2d3SAndreas Gohr     * @return null|string
1750b3fd2d3SAndreas Gohr     */
1760b3fd2d3SAndreas Gohr    public function getFilter(): ?string
1770b3fd2d3SAndreas Gohr    {
1780b3fd2d3SAndreas Gohr        return $this->filter;
1790b3fd2d3SAndreas Gohr    }
1800b3fd2d3SAndreas Gohr
1810b3fd2d3SAndreas Gohr    /**
1820b3fd2d3SAndreas Gohr     * @param null|string $filter
1830b3fd2d3SAndreas Gohr     * @return $this
1840b3fd2d3SAndreas Gohr     */
1850b3fd2d3SAndreas Gohr    public function setFilter(?string $filter)
1860b3fd2d3SAndreas Gohr    {
1870b3fd2d3SAndreas Gohr        $this->filter = $filter;
1880b3fd2d3SAndreas Gohr
1890b3fd2d3SAndreas Gohr        return $this;
1900b3fd2d3SAndreas Gohr    }
1910b3fd2d3SAndreas Gohr
1920b3fd2d3SAndreas Gohr    /**
1930b3fd2d3SAndreas Gohr     * @return LdapUrlExtension[]
1940b3fd2d3SAndreas Gohr     */
1950b3fd2d3SAndreas Gohr    public function getExtensions(): array
1960b3fd2d3SAndreas Gohr    {
1970b3fd2d3SAndreas Gohr        return $this->extensions;
1980b3fd2d3SAndreas Gohr    }
1990b3fd2d3SAndreas Gohr
2000b3fd2d3SAndreas Gohr    /**
2010b3fd2d3SAndreas Gohr     * @param LdapUrlExtension ...$extensions
2020b3fd2d3SAndreas Gohr     * @return $this
2030b3fd2d3SAndreas Gohr     */
2040b3fd2d3SAndreas Gohr    public function setExtensions(LdapUrlExtension ...$extensions)
2050b3fd2d3SAndreas Gohr    {
2060b3fd2d3SAndreas Gohr        $this->extensions = $extensions;
2070b3fd2d3SAndreas Gohr
2080b3fd2d3SAndreas Gohr        return $this;
2090b3fd2d3SAndreas Gohr    }
2100b3fd2d3SAndreas Gohr
2110b3fd2d3SAndreas Gohr    /**
2120b3fd2d3SAndreas Gohr     * @return Attribute[]
2130b3fd2d3SAndreas Gohr     */
2140b3fd2d3SAndreas Gohr    public function getAttributes(): array
2150b3fd2d3SAndreas Gohr    {
2160b3fd2d3SAndreas Gohr        return $this->attributes;
2170b3fd2d3SAndreas Gohr    }
2180b3fd2d3SAndreas Gohr
2190b3fd2d3SAndreas Gohr    /**
220*dad993c5SAndreas Gohr     * @param string|Attribute ...$attributes
2210b3fd2d3SAndreas Gohr     * @return $this
2220b3fd2d3SAndreas Gohr     */
2230b3fd2d3SAndreas Gohr    public function setAttributes(...$attributes)
2240b3fd2d3SAndreas Gohr    {
2250b3fd2d3SAndreas Gohr        $attr = [];
2260b3fd2d3SAndreas Gohr        foreach ($attributes as $attribute) {
2270b3fd2d3SAndreas Gohr            $attr[] = $attribute instanceof Attribute ? $attribute : new Attribute($attribute);
2280b3fd2d3SAndreas Gohr        }
2290b3fd2d3SAndreas Gohr        $this->attributes = $attr;
2300b3fd2d3SAndreas Gohr
2310b3fd2d3SAndreas Gohr        return $this;
2320b3fd2d3SAndreas Gohr    }
2330b3fd2d3SAndreas Gohr
2340b3fd2d3SAndreas Gohr
2350b3fd2d3SAndreas Gohr    /**
2360b3fd2d3SAndreas Gohr     * @param bool $useSsl
237*dad993c5SAndreas Gohr     * @return static
2380b3fd2d3SAndreas Gohr     */
2390b3fd2d3SAndreas Gohr    public function setUseSsl(bool $useSsl)
2400b3fd2d3SAndreas Gohr    {
2410b3fd2d3SAndreas Gohr        $this->useSsl = $useSsl;
2420b3fd2d3SAndreas Gohr
2430b3fd2d3SAndreas Gohr        return $this;
2440b3fd2d3SAndreas Gohr    }
2450b3fd2d3SAndreas Gohr
2460b3fd2d3SAndreas Gohr    /**
2470b3fd2d3SAndreas Gohr     * @return bool
2480b3fd2d3SAndreas Gohr     */
2490b3fd2d3SAndreas Gohr    public function getUseSsl(): bool
2500b3fd2d3SAndreas Gohr    {
2510b3fd2d3SAndreas Gohr        return $this->useSsl;
2520b3fd2d3SAndreas Gohr    }
2530b3fd2d3SAndreas Gohr
2540b3fd2d3SAndreas Gohr    /**
2550b3fd2d3SAndreas Gohr     * Get the string representation of the URL.
2560b3fd2d3SAndreas Gohr     *
2570b3fd2d3SAndreas Gohr     * @return string
2580b3fd2d3SAndreas Gohr     */
2590b3fd2d3SAndreas Gohr    public function toString(): string
2600b3fd2d3SAndreas Gohr    {
2610b3fd2d3SAndreas Gohr        $url = ($this->useSsl ? 'ldaps' : 'ldap') . '://' . $this->host;
2620b3fd2d3SAndreas Gohr
2630b3fd2d3SAndreas Gohr        if ($this->host !== null && $this->port !== null) {
2640b3fd2d3SAndreas Gohr            $url .= ':' . $this->port;
2650b3fd2d3SAndreas Gohr        }
2660b3fd2d3SAndreas Gohr
2670b3fd2d3SAndreas Gohr        return $url . '/' . self::encode($this->dn) . $this->getQueryString();
2680b3fd2d3SAndreas Gohr    }
2690b3fd2d3SAndreas Gohr
2700b3fd2d3SAndreas Gohr    /**
2710b3fd2d3SAndreas Gohr     * @return string
2720b3fd2d3SAndreas Gohr     */
2730b3fd2d3SAndreas Gohr    public function __toString()
2740b3fd2d3SAndreas Gohr    {
2750b3fd2d3SAndreas Gohr        return $this->toString();
2760b3fd2d3SAndreas Gohr    }
2770b3fd2d3SAndreas Gohr
2780b3fd2d3SAndreas Gohr    /**
2790b3fd2d3SAndreas Gohr     * Given a string LDAP URL, get its object representation.
2800b3fd2d3SAndreas Gohr     *
2810b3fd2d3SAndreas Gohr     * @param string $ldapUrl
2820b3fd2d3SAndreas Gohr     * @return LdapUrl
2830b3fd2d3SAndreas Gohr     * @throws UrlParseException
284*dad993c5SAndreas Gohr     * @throws InvalidArgumentException
2850b3fd2d3SAndreas Gohr     */
2860b3fd2d3SAndreas Gohr    public static function parse(string $ldapUrl): LdapUrl
2870b3fd2d3SAndreas Gohr    {
2880b3fd2d3SAndreas Gohr        $pieces = self::explodeUrl($ldapUrl);
2890b3fd2d3SAndreas Gohr
2900b3fd2d3SAndreas Gohr        $url = new LdapUrl($pieces['host'] ?? null);
2910b3fd2d3SAndreas Gohr        $url->setUseSsl($pieces['scheme'] === 'ldaps');
2920b3fd2d3SAndreas Gohr        $url->setPort($pieces['port'] ?? null);
293*dad993c5SAndreas Gohr        $url->setDn((isset($pieces['path']) && $pieces['path'] !== '/') ? self::decode(ltrim($pieces['path'], '/')) : null);
2940b3fd2d3SAndreas Gohr
295*dad993c5SAndreas Gohr        $query = explode('?', $pieces['query'] ?? '');
296*dad993c5SAndreas Gohr        if (count($query) !== 0) {
297*dad993c5SAndreas Gohr            $url->setAttributes(...($query[0] === '' ? [] : explode(',', $query[0])));
2980b3fd2d3SAndreas Gohr            $url->setScope(isset($query[1]) && $query[1] !== '' ? $query[1] : null);
2990b3fd2d3SAndreas Gohr            $url->setFilter(isset($query[2]) && $query[2] !== '' ? self::decode($query[2]) : null);
3000b3fd2d3SAndreas Gohr
3010b3fd2d3SAndreas Gohr            $extensions = [];
3020b3fd2d3SAndreas Gohr            if (isset($query[3]) && $query[3] !== '') {
303*dad993c5SAndreas Gohr                $extensions = array_map(function ($ext) {
3040b3fd2d3SAndreas Gohr                    return LdapUrlExtension::parse($ext);
305*dad993c5SAndreas Gohr                }, explode(',', $query[3]));
3060b3fd2d3SAndreas Gohr            }
3070b3fd2d3SAndreas Gohr            $url->setExtensions(...$extensions);
3080b3fd2d3SAndreas Gohr        }
3090b3fd2d3SAndreas Gohr
3100b3fd2d3SAndreas Gohr        return $url;
3110b3fd2d3SAndreas Gohr    }
3120b3fd2d3SAndreas Gohr
3130b3fd2d3SAndreas Gohr    /**
3140b3fd2d3SAndreas Gohr     * @param string $url
3150b3fd2d3SAndreas Gohr     * @return array
3160b3fd2d3SAndreas Gohr     * @throws UrlParseException
3170b3fd2d3SAndreas Gohr     */
3180b3fd2d3SAndreas Gohr    protected static function explodeUrl(string $url): array
3190b3fd2d3SAndreas Gohr    {
320*dad993c5SAndreas Gohr        $pieces = parse_url($url);
3210b3fd2d3SAndreas Gohr
3220b3fd2d3SAndreas Gohr        if ($pieces === false || !isset($pieces['scheme'])) {
3230b3fd2d3SAndreas Gohr            # 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
3240b3fd2d3SAndreas Gohr            # for LDAP URLs. In the case of an empty host a client should determine what host to connect to.
325*dad993c5SAndreas Gohr            if (preg_match('/^(ldaps?)\:\/\/\/(.*)$/', $url, $matches) === 0) {
3260b3fd2d3SAndreas Gohr                throw new UrlParseException(sprintf('The LDAP URL is malformed: %s', $url));
3270b3fd2d3SAndreas Gohr            }
3280b3fd2d3SAndreas Gohr            $query = null;
3290b3fd2d3SAndreas Gohr            $path = null;
3300b3fd2d3SAndreas Gohr
3310b3fd2d3SAndreas Gohr            # Check for query parameters but no path...
332*dad993c5SAndreas Gohr            if (strlen($matches[2]) > 0 && $matches[2][0] === '?') {
3330b3fd2d3SAndreas Gohr                $query = substr($matches[2], 1);
3340b3fd2d3SAndreas Gohr            # Check if there are any query parameters and a possible path...
335*dad993c5SAndreas Gohr            } elseif (strpos($matches[2], '?') !== false) {
336*dad993c5SAndreas Gohr                $parts = explode('?', $matches[2], 2);
3370b3fd2d3SAndreas Gohr                $path = $parts[0];
3380b3fd2d3SAndreas Gohr                $query = isset($parts[1]) ? $parts[1] : null;
3390b3fd2d3SAndreas Gohr            # A path only...
3400b3fd2d3SAndreas Gohr            } else {
3410b3fd2d3SAndreas Gohr                $path = $matches[2];
3420b3fd2d3SAndreas Gohr            }
3430b3fd2d3SAndreas Gohr
3440b3fd2d3SAndreas Gohr            $pieces = [
3450b3fd2d3SAndreas Gohr                'scheme' => $matches[1],
3460b3fd2d3SAndreas Gohr                'path' => $path,
3470b3fd2d3SAndreas Gohr                'query' => $query,
3480b3fd2d3SAndreas Gohr            ];
3490b3fd2d3SAndreas Gohr        }
350*dad993c5SAndreas Gohr        $pieces['scheme'] = strtolower($pieces['scheme']);
3510b3fd2d3SAndreas Gohr
3520b3fd2d3SAndreas Gohr        if (!($pieces['scheme'] === 'ldap' || $pieces['scheme'] === 'ldaps')) {
3530b3fd2d3SAndreas Gohr            throw new UrlParseException(sprintf('The URL scheme "%s" is not valid. It must be "ldap" or "ldaps".', $pieces['scheme']));
3540b3fd2d3SAndreas Gohr        }
3550b3fd2d3SAndreas Gohr
3560b3fd2d3SAndreas Gohr        return $pieces;
3570b3fd2d3SAndreas Gohr    }
3580b3fd2d3SAndreas Gohr
3590b3fd2d3SAndreas Gohr    /**
3600b3fd2d3SAndreas Gohr     * Generate the query part of the URL string representation. Only generates the parts actually used.
3610b3fd2d3SAndreas Gohr     *
3620b3fd2d3SAndreas Gohr     * @return string
3630b3fd2d3SAndreas Gohr     */
3640b3fd2d3SAndreas Gohr    protected function getQueryString(): string
3650b3fd2d3SAndreas Gohr    {
3660b3fd2d3SAndreas Gohr        $query = [];
3670b3fd2d3SAndreas Gohr
368*dad993c5SAndreas Gohr        if (count($this->attributes) !== 0) {
369*dad993c5SAndreas Gohr            $query[0] = implode(',', array_map(function ($v) {
3700b3fd2d3SAndreas Gohr                /** @var $v Attribute */
3710b3fd2d3SAndreas Gohr                return self::encode($v->getDescription());
3720b3fd2d3SAndreas Gohr            }, $this->attributes));
3730b3fd2d3SAndreas Gohr        }
3740b3fd2d3SAndreas Gohr        if ($this->scope !== null) {
3750b3fd2d3SAndreas Gohr            $query[1] = self::encode($this->scope);
3760b3fd2d3SAndreas Gohr        }
3770b3fd2d3SAndreas Gohr        if ($this->filter !== null) {
3780b3fd2d3SAndreas Gohr            $query[2] = self::encode($this->filter);
3790b3fd2d3SAndreas Gohr        }
380*dad993c5SAndreas Gohr        if (count($this->extensions) !== 0) {
381*dad993c5SAndreas Gohr            $query[3] = implode(',', $this->extensions);
3820b3fd2d3SAndreas Gohr        }
3830b3fd2d3SAndreas Gohr
384*dad993c5SAndreas Gohr        if (count($query) === 0) {
3850b3fd2d3SAndreas Gohr            return '';
3860b3fd2d3SAndreas Gohr        }
3870b3fd2d3SAndreas Gohr
388*dad993c5SAndreas Gohr        end($query);
389*dad993c5SAndreas Gohr        $last = key($query);
390*dad993c5SAndreas Gohr        reset($query);
3910b3fd2d3SAndreas Gohr
3920b3fd2d3SAndreas Gohr        # This is so we stop at the last query part that was actually set, but also capture cases where the first and
3930b3fd2d3SAndreas Gohr        # third were set but not the second.
3940b3fd2d3SAndreas Gohr        $url = '';
3950b3fd2d3SAndreas Gohr        for ($i = 0; $i <= $last; $i++) {
3960b3fd2d3SAndreas Gohr            $url .= '?';
3970b3fd2d3SAndreas Gohr            if (isset($query[$i])) {
398*dad993c5SAndreas Gohr                /* @phpstan-ignore-next-line */
3990b3fd2d3SAndreas Gohr                $url .= $query[$i];
4000b3fd2d3SAndreas Gohr            }
4010b3fd2d3SAndreas Gohr        }
4020b3fd2d3SAndreas Gohr
4030b3fd2d3SAndreas Gohr        return $url;
4040b3fd2d3SAndreas Gohr    }
4050b3fd2d3SAndreas Gohr}
406