plugin updates

This commit is contained in:
Tony Volpe
2024-09-17 10:43:54 -04:00
parent 44b413346f
commit b7c8882c8c
1359 changed files with 58219 additions and 11364 deletions

View File

@@ -0,0 +1,282 @@
<?php
/* ============================================================================
* Copyright 2021 Zindex Software
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ============================================================================ */
namespace Opis\Uri;
use Opis\String\UnicodeString;
final class Punycode
{
private const BASE = 36;
private const TMIN = 1;
private const TMAX = 26;
private const SKEW = 38;
private const DAMP = 700;
private const INITIAL_BIAS = 72;
private const INITIAL_N = 0x80;
private const PREFIX = 'xn--';
private const PREFIX_LEN = 4;
private const DELIMITER = 0x2D;
private const MAX_INT = 0x7FFFFFFF;
private const NON_ASCII = '#[^\0-\x7E]#';
public static function encode(string $input): string
{
return implode('.', array_map([self::class, 'encodePart'], explode('.', $input)));
}
public static function decode(string $input): string
{
return implode('.', array_map([self::class, 'decodePart'], explode('.', $input)));
}
public static function normalize(string $input): string
{
return implode('.', array_map([self::class, 'normalizePart'], explode('.', $input)));
}
public static function encodePart(string $input): string
{
if (!preg_match(self::NON_ASCII, $input)) {
return $input;
}
$input = UnicodeString::getCodePointsFromString($input, UnicodeString::LOWER_CASE);
$input_len = count($input);
$output = array_filter($input, static function (int $code): bool {
return $code < 0x80;
});
if ($output) {
$output = array_values($output);
}
$delta = 0;
$n = self::INITIAL_N;
$bias = self::INITIAL_BIAS;
$handled = $basic_length = count($output);
if ($basic_length) {
$output[] = self::DELIMITER;
}
while ($handled < $input_len) {
$m = self::MAX_INT;
for ($i = 0; $i < $input_len; $i++) {
if ($input[$i] >= $n && $input[$i] < $m) {
$m = $input[$i];
}
}
if (($m - $n) > intdiv(self::MAX_INT - $delta, $handled + 1)) {
throw new PunycodeException("Punycode overflow");
}
$delta += ($m - $n) * ($handled + 1);
$n = $m;
for ($i = 0; $i < $input_len; $i++) {
if ($input[$i] < $n && (++$delta === 0)) {
throw new PunycodeException("Punycode overflow");
}
if ($input[$i] === $n) {
$q = $delta;
for ($k = self::BASE; ; $k += self::BASE) {
$t = self::threshold($k, $bias);
if ($q < $t) {
break;
}
$base_minus_t = self::BASE - $t;
$q -= $t;
$output[] = self::encodeDigit($t + ($q % $base_minus_t));
$q = intdiv($q, $base_minus_t);
}
$output[] = self::encodeDigit($q);
$bias = self::adapt($delta, $handled + 1, $handled === $basic_length);
$delta = 0;
$handled++;
}
}
$delta++; $n++;
}
return self::PREFIX . UnicodeString::getStringFromCodePoints($output);
}
public static function decodePart(string $input): string
{
if (stripos($input, self::PREFIX) !== 0) {
return $input;
}
$input = UnicodeString::getCodePointsFromString(substr($input, self::PREFIX_LEN), UnicodeString::LOWER_CASE);
$input_len = count($input);
$pos = array_keys($input, self::DELIMITER, true);
if ($pos) {
$pos = end($pos);
} else {
$pos = -1;
}
/** @var int $pos */
if ($pos === -1) {
$output = [];
$pos = $output_len = 0;
} else {
$output = array_slice($input, 0, ++$pos);
$output_len = $pos;
for ($i = 0; $i < $pos; $i++) {
if ($output[$i] >= 0x80) {
throw new PunycodeException("Non-basic code point is not allowed: {$output[$i]}");
}
}
}
$i = 0;
$n = self::INITIAL_N;
$bias = self::INITIAL_BIAS;
while ($pos < $input_len) {
$old_i = $i;
for ($w = 1, $k = self::BASE; ; $k += self::BASE) {
if ($pos >= $input_len) {
throw new PunycodeException("Punycode bad input");
}
$digit = self::decodeDigit($input[$pos++]);
if ($digit >= self::BASE || $digit > intdiv(self::MAX_INT - $i, $w)) {
throw new PunycodeException("Punycode overflow");
}
$i += $digit * $w;
$t = self::threshold($k, $bias);
if ($digit < $t) {
break;
}
$t = self::BASE - $t;
if ($w > intdiv(self::MAX_INT, $t)) {
throw new PunycodeException("Punycode overflow");
}
$w *= $t;
}
$output_len++;
if (intdiv($i, $output_len) > self::MAX_INT - $n) {
throw new PunycodeException("Punycode overflow");
}
$n += intdiv($i, $output_len);
$bias = self::adapt($i - $old_i, $output_len, $old_i === 0);
$i %= $output_len;
array_splice($output, $i, 0, $n);
$i++;
}
return UnicodeString::getStringFromCodePoints($output);
}
public static function normalizePart(string $input): string
{
$input = strtolower($input);
if (strpos($input, self::DELIMITER) === 0) {
self::decodePart($input); // just validate
return $input;
}
return self::encodePart($input);
}
private static function encodeDigit(int $digit): int
{
return $digit + 0x16 + ($digit < 0x1A ? 0x4B: 0x00);
}
private static function decodeDigit(int $code): int
{
if ($code < 0x3A) {
return $code - 0x16;
}
if ($code < 0x5B) {
return $code - 0x41;
}
if ($code < 0x7B) {
return $code - 0x61;
}
return self::BASE;
}
private static function threshold(int $k, int $bias): int
{
$d = $k - $bias;
if ($d <= self::TMIN) {
return self::TMIN;
}
if ($d >= self::TMAX) {
return self::TMAX;
}
return $d;
}
private static function adapt(int $delta, int $num_points, bool $first_time = false): int
{
$delta = intdiv($delta, $first_time ? self::DAMP : 2);
$delta += intdiv($delta, $num_points);
$k = 0;
$base_tmin_diff = self::BASE - self::TMIN;
$lim = $base_tmin_diff * self::TMAX / 2;
while ($delta > $lim) {
$delta = intdiv($delta, $base_tmin_diff);
$k += self::BASE;
}
$k += intdiv(($base_tmin_diff + 1) * $delta, $delta + self::SKEW);
return $k;
}
}

View File

@@ -0,0 +1,25 @@
<?php
/* ============================================================================
* Copyright 2021 Zindex Software
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ============================================================================ */
namespace Opis\Uri;
use RuntimeException;
class PunycodeException extends RuntimeException
{
}

View File

@@ -0,0 +1,965 @@
<?php
/* ============================================================================
* Copyright 2021 Zindex Software
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ============================================================================ */
namespace Opis\Uri;
use Opis\String\UnicodeString;
class Uri
{
protected const URI_REGEX = '`^(([^:/?#]+):)?(//([^/?#]*))?([^?#]*)(\?([^#]*))?(#(.*))?$`';
protected const SCHEME_REGEX = '`^[a-z][a-z0-9-+.]*$`i';
protected const USER_OR_PASS_REGEX = '`^(?:(?:%[a-f0-9]{2})+|[a-z0-9-._~!$&\'()*+,:;=]+)*$`i';
protected const USERINFO_REGEX = '`^(?<user>[^:]+)(?::(?<pass>.*))?$`';
protected const HOST_LABEL_REGEX = '`^(?:(?:%[a-f0-9]{2})+|[a-z0-9-]+)*$`i';
protected const AUTHORITY_REGEX = '`^(?:(?<userinfo>[^@]+)\@)?(?<host>(\[[a-f0-9:]+\]|[^:]+))(?::(?<port>\d+))?$`i';
protected const PATH_REGEX = '`^(?:(?:%[a-f0-9]{2})+|[a-z0-9-._~!$&\'()*+,;=:@/]+)*$`i';
protected const QUERY_OR_FRAGMENT_REGEX = '`^(?:(?:%[a-f0-9]{2})+|[a-z0-9-._~!$&\'"()\[\]*+,;=:@?/%]+)*$`i';
protected array $components;
protected ?string $str = null;
/**
* @param array $components An array of normalized components
*/
public function __construct(array $components)
{
$this->components = $components + [
'scheme' => null,
'user' => null,
'pass' => null,
'host' => null,
'port' => null,
'path' => null,
'query' => null,
'fragment' => null,
];
}
/**
* @return string|null
*/
public function scheme(): ?string
{
return $this->components['scheme'];
}
/**
* @return string|null
*/
public function user(): ?string
{
return $this->components['user'];
}
/**
* @return string|null
*/
public function pass(): ?string
{
return $this->components['pass'];
}
/**
* @return string|null
*/
public function userInfo(): ?string
{
if ($this->components['user'] === null) {
return null;
}
if ($this->components['pass'] === null) {
return $this->components['user'];
}
return $this->components['user'] . ':' . $this->components['pass'];
}
/**
* @return string|null
*/
public function host(): ?string
{
return $this->components['host'];
}
/**
* @return int|null
*/
public function port(): ?int
{
return $this->components['port'];
}
/**
* @return string|null
*/
public function authority(): ?string
{
if ($this->components['host'] === null) {
return null;
}
$authority = $this->userInfo();
if ($authority !== null) {
$authority .= '@';
}
$authority .= $this->components['host'];
if ($this->components['port'] !== null) {
$authority .= ':' . $this->components['port'];
}
return $authority;
}
/**
* @return string|null
*/
public function path(): ?string
{
return $this->components['path'];
}
/**
* @return string|null
*/
public function query(): ?string
{
return $this->components['query'];
}
/**
* @return string|null
*/
public function fragment(): ?string
{
return $this->components['fragment'];
}
/**
* @return array|null[]
*/
public function components(): array
{
return $this->components;
}
/**
* @return bool
*/
public function isAbsolute(): bool
{
return $this->components['scheme'] !== null;
}
/**
* Use this URI as base to resolve the reference
* @param static|string|array $ref
* @param bool $normalize
* @return $this|null
*/
public function resolveRef($ref, bool $normalize = false): ?self
{
$ref = self::resolveComponents($ref);
if ($ref === null) {
return $this;
}
return new static(self::mergeComponents($ref, $this->components, $normalize));
}
/**
* Resolve this URI reference using a base URI
* @param static|string|array $base
* @param bool $normalize
* @return static
*/
public function resolve($base, bool $normalize = false): self
{
if ($this->isAbsolute()) {
return $this;
}
$base = self::resolveComponents($base);
if ($base === null) {
return $this;
}
return new static(self::mergeComponents($this->components, $base, $normalize));
}
/**
* @return string
*/
public function __toString(): string
{
if ($this->str !== null) {
return $this->str;
}
$str = '';
if ($this->components['scheme'] !== null) {
$str .= $this->components['scheme'] . ':';
}
if ($this->components['host'] !== null) {
$str .= '//' . $this->authority();
}
$str .= $this->components['path'];
if ($this->components['query'] !== null) {
$str .= '?' . $this->components['query'];
}
if ($this->components['fragment'] !== null) {
$str .= '#' . $this->components['fragment'];
}
return $this->str = $str;
}
/**
* @param string $uri
* @param bool $normalize
* @return static|null
*/
public static function create(string $uri, bool $normalize = false): ?self
{
$comp = self::parseComponents($uri);
if (!$comp) {
return null;
}
if ($normalize) {
$comp = self::normalizeComponents($comp);
}
return new static($comp);
}
/**
* Checks if the scheme contains valid chars
* @param string $scheme
* @return bool
*/
public static function isValidScheme(string $scheme): bool
{
return (bool)preg_match(self::SCHEME_REGEX, $scheme);
}
/**
* Checks if user contains valid chars
* @param string $user
* @return bool
*/
public static function isValidUser(string $user): bool
{
return (bool)preg_match(self::USER_OR_PASS_REGEX, $user);
}
/**
* Checks if pass contains valid chars
* @param string $pass
* @return bool
*/
public static function isValidPass(string $pass): bool
{
return (bool)preg_match(self::USER_OR_PASS_REGEX, $pass);
}
/**
* @param string $userInfo
* @return bool
*/
public static function isValidUserInfo(string $userInfo): bool
{
/** @var array|string $userInfo */
if (!preg_match(self::USERINFO_REGEX, $userInfo, $userInfo)) {
return false;
}
if (!self::isValidUser($userInfo['user'])) {
return false;
}
if (isset($userInfo['pass'])) {
return self::isValidPass($userInfo['pass']);
}
return true;
}
/**
* Checks if host is valid
* @param string $host
* @return bool
*/
public static function isValidHost(string $host): bool
{
// min and max length
if ($host === '' || isset($host[253])) {
return false;
}
// check ipv6
if ($host[0] === '[') {
if ($host[-1] !== ']') {
return false;
}
return filter_var(
substr($host, 1, -1),
\FILTER_VALIDATE_IP,
\FILTER_FLAG_IPV6
) !== false;
}
// check ipv4
if (preg_match('`^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\$`', $host)) {
return \filter_var($host, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4) !== false;
}
foreach (explode('.', $host) as $host) {
// empty or too long label
if ($host === '' || isset($host[63])) {
return false;
}
if ($host[0] === '-' || $host[-1] === '-') {
return false;
}
if (!preg_match(self::HOST_LABEL_REGEX, $host)) {
return false;
}
}
return true;
}
/**
* Checks if the port is valid
* @param int $port
* @return bool
*/
public static function isValidPort(int $port): bool
{
return $port >= 0 && $port <= 65535;
}
/**
* Checks if authority contains valid chars
* @param string $authority
* @return bool
*/
public static function isValidAuthority(string $authority): bool
{
if ($authority === '') {
return true;
}
/** @var array|string $authority */
if (!preg_match(self::AUTHORITY_REGEX, $authority, $authority)) {
return false;
}
if (isset($authority['port']) && !self::isValidPort((int)$authority['port'])) {
return false;
}
if (isset($authority['userinfo']) && !self::isValidUserInfo($authority['userinfo'])) {
return false;
}
return self::isValidHost($authority['host']);
}
/**
* Checks if the path contains valid chars
* @param string $path
* @return bool
*/
public static function isValidPath(string $path): bool
{
return $path === '' || (bool)preg_match(self::PATH_REGEX, $path);
}
/**
* Checks if the query string contains valid chars
* @param string $query
* @return bool
*/
public static function isValidQuery(string $query): bool
{
return $query === '' || (bool)preg_match(self::QUERY_OR_FRAGMENT_REGEX, $query);
}
/**
* Checks if the fragment contains valid chars
* @param string $fragment
* @return bool
*/
public static function isValidFragment(string $fragment): bool
{
return $fragment === '' || (bool)preg_match(self::QUERY_OR_FRAGMENT_REGEX, $fragment);
}
/**
* @param string $uri
* @param bool $expand_authority
* @param bool $validate
* @return array|null
*/
public static function parseComponents(string $uri, bool $expand_authority = true, bool $validate = true): ?array
{
if (!preg_match(self::URI_REGEX, $uri, $uri)) {
return null;
}
$comp = [];
// scheme
if (isset($uri[2]) && $uri[2] !== '') {
if ($validate && !self::isValidScheme($uri[2])) {
return null;
}
$comp['scheme'] = $uri[2];
}
// authority
if (isset($uri[4]) && isset($uri[3][0])) {
if ($uri[4] === '') {
if ($expand_authority) {
$comp['host'] = '';
} else {
$comp['authority'] = '';
}
} elseif ($expand_authority) {
$au = self::parseAuthorityComponents($uri[4], $validate);
if ($au === null) {
return null;
}
$comp += $au;
unset($au);
} else {
if ($validate && !self::isValidAuthority($uri[4])) {
return null;
}
$comp['authority'] = $uri[4];
}
}
// path
if (isset($uri[5])) {
if ($validate && !self::isValidPath($uri[5])) {
return null;
}
$comp['path'] = $uri[5];
// not a relative uri, remove dot segments
if (isset($comp['scheme']) || isset($comp['authority']) || isset($comp['host'])) {
$comp['path'] = self::removeDotSegmentsFromPath($comp['path']);
}
}
// query
if (isset($uri[7]) && isset($uri[6][0])) {
if ($validate && !self::isValidQuery($uri[7])) {
return null;
}
$comp['query'] = $uri[7];
}
// fragment
if (isset($uri[9]) && isset($uri[8][0])) {
if ($validate && !self::isValidFragment($uri[9])) {
return null;
}
$comp['fragment'] = $uri[9];
}
return $comp;
}
/**
* @param self|string|array $uri
* @return array|null
*/
public static function resolveComponents($uri): ?array
{
if ($uri instanceof self) {
return $uri->components;
}
if (is_string($uri)) {
return self::parseComponents($uri);
}
if (is_array($uri)) {
if (isset($uri['host'])) {
unset($uri['authority']);
} elseif (isset($uri['authority'])) {
$au = self::parseAuthorityComponents($uri['authority']);
unset($uri['authority']);
if ($au !== null) {
unset($uri['user'], $uri['pass'], $uri['host'], $uri['port']);
$uri += $au;
}
}
return $uri;
}
return null;
}
/**
* @param string $authority
* @param bool $validate
* @return array|null
*/
public static function parseAuthorityComponents(string $authority, bool $validate = true): ?array
{
/** @var array|string $authority */
if (!preg_match(self::AUTHORITY_REGEX, $authority, $authority)) {
return null;
}
$comp = [];
// userinfo
if (isset($authority['userinfo']) && $authority['userinfo'] !== '') {
if (!preg_match(self::USERINFO_REGEX, $authority['userinfo'], $ui)) {
return null;
}
// user
if ($validate && !self::isValidUser($ui['user'])) {
return null;
}
$comp['user'] = $ui['user'];
// pass
if (isset($ui['pass']) && $ui['pass'] !== '') {
if ($validate && !self::isValidPass($ui['pass'])) {
return null;
}
$comp['pass'] = $ui['pass'];
}
unset($ui);
}
// host
if ($validate && !self::isValidHost($authority['host'])) {
return null;
}
$comp['host'] = $authority['host'];
// port
if (isset($authority['port'])) {
$authority['port'] = (int)$authority['port'];
if (!self::isValidPort($authority['port'])) {
return null;
}
$comp['port'] = $authority['port'];
}
return $comp;
}
/**
* @param array $ref
* @param array $base
* @param bool $normalize
* @return array
*/
public static function mergeComponents(array $ref, array $base, bool $normalize = false): array
{
if (isset($ref['scheme'])) {
$dest = $ref;
} else {
$dest = [];
$dest['scheme'] = $base['scheme'] ?? null;
if (isset($ref['authority']) || isset($ref['host'])) {
$dest += $ref;
} else {
if (isset($base['authority'])) {
$dest['authority'] = $base['authority'];
} else {
$dest['user'] = $base['user'] ?? null;
$dest['pass'] = $base['pass'] ?? null;
$dest['host'] = $base['host'] ?? null;
$dest['port'] = $base['port'] ?? null;
}
if (!isset($ref['path'])) {
$ref['path'] = '';
}
if (!isset($base['path'])) {
$base['path'] = '';
}
if ($ref['path'] === '') {
$dest['path'] = $base['path'];
$dest['query'] = $ref['query'] ?? $base['query'] ?? null;
} else {
if ($ref['path'][0] === '/') {
$dest['path'] = $ref['path'];
} else {
if ((isset($base['authority']) || isset($base['host'])) && $base['path'] === '') {
$dest['path'] = '/' . $ref['path'];
} else {
$dest['path'] = $base['path'];
if ($dest['path'] !== '') {
$pos = strrpos($dest['path'], '/');
if ($pos === false) {
$dest['path'] = '';
} else {
$dest['path'] = substr($dest['path'], 0, $pos);
}
unset($pos);
}
$dest['path'] .= '/' . $ref['path'];
}
}
$dest['query'] = $ref['query'] ?? null;
}
}
}
$dest['fragment'] = $ref['fragment'] ?? null;
if ($normalize) {
return self::normalizeComponents($dest);
}
if (isset($dest['path'])) {
$dest['path'] = self::removeDotSegmentsFromPath($dest['path']);
}
return $dest;
}
public static function normalizeComponents(array $components): array
{
if (isset($components['scheme'])) {
$components['scheme'] = strtolower($components['scheme']);
// Remove default port
if (isset($components['port']) && self::getSchemePort($components['scheme']) === $components['port']) {
$components['port'] = null;
}
}
if (isset($components['host'])) {
$components['host'] = strtolower($components['host']);
}
if (isset($components['path'])) {
$components['path'] = self::removeDotSegmentsFromPath($components['path']);
}
if (isset($components['query'])) {
$components['query'] = self::normalizeQueryString($components['query']);
}
return $components;
}
/**
* Removes dot segments from path
* @param string $path
* @return string
*/
public static function removeDotSegmentsFromPath(string $path): string
{
// Fast check common simple paths
if ($path === '' || $path === '/') {
return $path;
}
$output = '';
$last_slash = 0;
$len = strlen($path);
$i = 0;
while ($i < $len) {
if ($path[$i] === '.') {
$j = $i + 1;
// search for .
if ($j >= $len) {
break;
}
// search for ./
if ($path[$j] === '/') {
$i = $j + 1;
continue;
}
// search for ../
if ($path[$j] === '.') {
$k = $j + 1;
if ($k >= $len) {
break;
}
if ($path[$k] === '/') {
$i = $k + 1;
continue;
}
}
} elseif ($path[$i] === '/') {
$j = $i + 1;
if ($j >= $len) {
$output .= '/';
break;
}
// search for /.
if ($path[$j] === '.') {
$k = $j + 1;
if ($k >= $len) {
$output .= '/';
break;
}
// search for /./
if ($path[$k] === '/') {
$i = $k;
continue;
}
// search for /..
if ($path[$k] === '.') {
$n = $k + 1;
if ($n >= $len) {
// keep the slash
$output = substr($output, 0, $last_slash + 1);
break;
}
// search for /../
if ($path[$n] === '/') {
$output = substr($output, 0, $last_slash);
$last_slash = (int)strrpos($output, '/');
$i = $n;
continue;
}
}
}
}
$pos = strpos($path, '/', $i + 1);
if ($pos === false) {
$output .= substr($path, $i);
break;
}
$last_slash = strlen($output);
$output .= substr($path, $i, $pos - $i);
$i = $pos;
}
return $output;
}
/**
* @param string|null $query
* @return array
*/
public static function parseQueryString(?string $query): array
{
if ($query === null) {
return [];
}
$list = [];
foreach (explode('&', $query) as $name) {
$value = null;
if (($pos = strpos($name, '=')) !== false) {
$value = self::decodeComponent(substr($name, $pos + 1));
$name = self::decodeComponent(substr($name, 0, $pos));
} else {
$name = self::decodeComponent($name);
}
$list[$name] = $value;
}
return $list;
}
/**
* @param array $qs
* @param string|null $prefix
* @param string $separator
* @param bool $sort
* @return string
*/
public static function buildQueryString(array $qs, ?string $prefix = null,
string $separator = '&', bool $sort = false): string
{
$isIndexed = static function (array $array): bool {
for ($i = 0, $max = count($array); $i < $max; $i++) {
if (!array_key_exists($i, $array)) {
return false;
}
}
return true;
};
$f = static function (array $arr, ?string $prefix = null) use (&$f, &$isIndexed): iterable {
$indexed = $prefix !== null && $isIndexed($arr);
foreach ($arr as $key => $value) {
if ($prefix !== null) {
$key = $prefix . ($indexed ? "[]" : "[{$key}]");
}
if (is_array($value)) {
yield from $f($value, $key);
} else {
yield $key => $value;
}
}
};
$data = [];
foreach ($f($qs, $prefix) as $key => $value) {
$item = is_string($key) ? self::encodeComponent($key) : $key;
if ($value !== null) {
$item .= '=';
$item .= is_string($value) ? self::encodeComponent($value) : $value;
}
if ($item === '' || $item === '=') {
continue;
}
$data[] = $item;
}
if (!$data) {
return '';
}
if ($sort) {
sort($data);
}
return implode($separator, $data);
}
/**
* @param string $query
* @return string
*/
public static function normalizeQueryString(string $query): string
{
return static::buildQueryString(self::parseQueryString($query), null, '&', true);
}
public static function decodeComponent(string $component): string
{
return rawurldecode($component);
}
public static function encodeComponent(string $component, ?array $skip = null): string
{
if (!$skip) {
return rawurlencode($component);
}
$str = '';
foreach (UnicodeString::walkString($component) as [$cp, $chars]) {
if ($cp < 0x80) {
if ($cp === 0x2D || $cp === 0x2E ||
$cp === 0x5F || $cp === 0x7E ||
($cp >= 0x41 && $cp <= 0x5A) ||
($cp >= 0x61 && $cp <= 0x7A) ||
($cp >= 0x30 && $cp <= 0x39) ||
in_array($cp, $skip, true)
) {
$str .= chr($cp);
} else {
$str .= '%' . strtoupper(dechex($cp));
}
} else {
$i = 0;
while (isset($chars[$i])) {
$str .= '%' . strtoupper(dechex($chars[$i++]));
}
}
}
return $str;
}
public static function setSchemePort(string $scheme, ?int $port): void
{
$scheme = strtolower($scheme);
if ($port === null) {
unset(self::$KNOWN_PORTS[$scheme]);
} else {
self::$KNOWN_PORTS[$scheme] = $port;
}
}
public static function getSchemePort(string $scheme): ?int
{
return self::$KNOWN_PORTS[strtolower($scheme)] ?? null;
}
protected static array $KNOWN_PORTS = [
'ftp' => 21,
'ssh' => 22,
'telnet' => 23,
'smtp' => 25,
'tftp' => 69,
'http' => 80,
'pop' => 110,
'sftp' => 115,
'imap' => 143,
'irc' => 194,
'ldap' => 389,
'https' => 443,
'ldaps' => 636,
'telnets' => 992,
'imaps' => 993,
'ircs' => 994,
'pops' => 995,
];
}

View File

@@ -0,0 +1,520 @@
<?php
/* ============================================================================
* Copyright 2021 Zindex Software
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
* ============================================================================ */
namespace Opis\Uri;
use Opis\String\UnicodeString;
class UriTemplate
{
/** @var string */
protected const TEMPLATE_VARSPEC_REGEX = '~^(?<varname>[a-zA-Z0-9\_\%\.]+)(?:(?<explode>\*)?|\:(?<prefix>\d+))?$~';
/** @var string */
protected const TEMPLATE_REGEX = <<<'REGEX'
~\{
(?<operator>[+#./;&=,!@|\?])?
(?<varlist>
(?:(?P>varspec),)*
(?<varspec>(?:
[a-zA-Z0-9\_\%\.]+
(?:\*|\:\d+)?
))
)
\}~x
REGEX;
/** @var array */
protected const TEMPLATE_TABLE = [
'' => [
'first' => '',
'sep' => ',',
'named' => false,
'ifemp' => '',
'allow' => false,
],
'+' => [
'first' => '',
'sep' => ',',
'named' => false,
'ifemp' => '',
'allow' => true,
],
'.' => [
'first' => '.',
'sep' => '.',
'named' => false,
'ifemp' => '',
'allow' => false,
],
'/' => [
'first' => '/',
'sep' => '/',
'named' => false,
'ifemp' => '',
'allow' => false,
],
';' => [
'first' => ';',
'sep' => ';',
'named' => true,
'ifemp' => '',
'allow' => false,
],
'?' => [
'first' => '?',
'sep' => '&',
'named' => true,
'ifemp' => '=',
'allow' => false,
],
'&' => [
'first' => '&',
'sep' => '&',
'named' => true,
'ifemp' => '=',
'allow' => false,
],
'#' => [
'first' => '#',
'sep' => ',',
'named' => false,
'ifemp' => '',
'allow' => true,
],
];
protected string $uri;
/** @var bool|null|array */
protected $parsed = false;
/**
* UriTemplate constructor.
* @param string $uri_template
*/
public function __construct(string $uri_template)
{
$this->uri = $uri_template;
}
/**
* @param array $vars
* @return string
*/
public function resolve(array $vars): string
{
if ($this->parsed === false) {
$this->parsed = $this->parse($this->uri);
}
if ($this->parsed === null || !$vars) {
return $this->uri;
}
$data = '';
$vars = $this->prepareVars($vars);
foreach ($this->parsed as $item) {
if (!is_array($item)) {
$data .= $item;
continue;
}
$data .= $this->parseTemplateExpression(
self::TEMPLATE_TABLE[$item['operator']],
$this->resolveVars($item['vars'], $vars)
);
}
return $data;
}
/**
* @return bool
*/
public function hasPlaceholders(): bool
{
if ($this->parsed === false) {
$this->parse($this->uri);
}
return $this->parsed !== null;
}
/**
* @param string $uri
* @return array|null
*/
protected function parse(string $uri): ?array
{
$placeholders = null;
preg_match_all(self::TEMPLATE_REGEX, $uri, $placeholders, PREG_SET_ORDER | PREG_OFFSET_CAPTURE);
if (!$placeholders) {
return null;
}
$dataIndex = -1;
$data = [];
$hasVars = false;
$nextOffset = 0;
foreach ($placeholders as &$p) {
$offset = $p[0][1];
if ($nextOffset < $offset) {
$data[] = substr($uri, $nextOffset, $offset - $nextOffset);
$dataIndex++;
}
$matched = $p[0][0];
$nextOffset = $offset + strlen($matched);
$operator = $p['operator'][0] ?? null;
if ($operator === null || !isset(self::TEMPLATE_TABLE[$operator])) {
if ($dataIndex >= 0 && is_string($data[$dataIndex])) {
$data[$dataIndex] .= $matched;
} else {
$data[] = $matched;
$dataIndex++;
}
continue;
}
$varList = $p['varlist'][0] ?? '';
$varList = $varList === '' ? [] : explode(',', $varList);
$p = null;
$varData = [];
foreach ($varList as $var) {
if (!preg_match(self::TEMPLATE_VARSPEC_REGEX, $var, $spec)) {
continue;
}
$varData[] = [
'name' => $spec['varname'],
'explode' => isset($spec['explode']) && $spec['explode'] === '*',
'prefix' => isset($spec['prefix']) ? (int)$spec['prefix'] : 0,
];
unset($var, $spec);
}
if ($varData) {
$hasVars = true;
$data[] = [
'operator' => $operator,
'vars' => $varData,
];
$dataIndex++;
} else {
if ($dataIndex >= 0 && is_string($data[$dataIndex])) {
$data[$dataIndex] .= $matched;
} else {
$data[] = $matched;
$dataIndex++;
}
}
unset($varData, $varList, $operator);
}
if (!$hasVars) {
return null;
}
$matched = substr($uri, $nextOffset);
if ($matched !== false && $matched !== '') {
if ($dataIndex >= 0 && is_string($data[$dataIndex])) {
$data[$dataIndex] .= $matched;
} else {
$data[] = $matched;
}
}
return $data;
}
/**
* Convert assoc arrays to objects
* @param array $vars
* @return array
*/
protected function prepareVars(array $vars): array
{
foreach ($vars as &$value) {
if (is_scalar($value)) {
if (!is_string($value)) {
$value = (string)$value;
}
continue;
}
if (!is_array($value)) {
continue;
}
$len = count($value);
for ($i = 0; $i < $len; $i++) {
if (!array_key_exists($i, $value)) {
$value = (object)$value;
break;
}
}
}
return $vars;
}
/**
* @param array $vars
* @param array $data
* @return array
*/
protected function resolveVars(array $vars, array $data): array
{
$resolved = [];
foreach ($vars as $info) {
$name = $info['name'];
if (!isset($data[$name])) {
continue;
}
$resolved[] = $info + ['value' => &$data[$name]];
}
return $resolved;
}
/**
* @param array $table
* @param array $data
* @return string
*/
protected function parseTemplateExpression(array $table, array $data): string
{
$result = [];
foreach ($data as $var) {
$str = "";
if (is_string($var['value'])) {
if ($table['named']) {
$str .= $var['name'];
if ($var['value'] === '') {
$str .= $table['ifemp'];
} else {
$str .= '=';
}
}
if ($var['prefix']) {
$str .= $this->encodeTemplateString(self::prefix($var['value'], $var['prefix']), $table['allow']);
} else {
$str .= $this->encodeTemplateString($var['value'], $table['allow']);
}
} elseif ($var['explode']) {
$list = [];
if ($table['named']) {
if (is_array($var['value'])) {
foreach ($var['value'] as $v) {
if (is_null($v) || !is_scalar($v)) {
continue;
}
$v = $this->encodeTemplateString((string)$v, $table['allow']);
if ($v === '') {
$list[] = $var['name'] . $table['ifemp'];
} else {
$list[] = $var['name'] . '=' . $v;
}
}
} elseif (is_object($var['value'])) {
foreach ($var['value'] as $prop => $v) {
if (is_null($v) || !is_scalar($v)) {
continue;
}
$v = $this->encodeTemplateString((string)$v, $table['allow']);
$prop = $this->encodeTemplateString((string)$prop, $table['allow']);
if ($v === '') {
$list[] = $prop . $table['ifemp'];
} else {
$list[] = $prop . '=' . $v;
}
}
}
} else {
if (is_array($var['value'])) {
foreach ($var['value'] as $v) {
if (is_null($v) || !is_scalar($v)) {
continue;
}
$list[] = $this->encodeTemplateString($v, $table['allow']);
}
} elseif (is_object($var['value'])) {
foreach ($var['value'] as $prop => $v) {
if (is_null($v) || !is_scalar($v)) {
continue;
}
$v = $this->encodeTemplateString((string)$v, $table['allow']);
$prop = $this->encodeTemplateString((string)$prop, $table['allow']);
$list[] = $prop . '=' . $v;
}
}
}
if ($list) {
$str .= implode($table['sep'], $list);
}
unset($list);
} else {
if ($table['named']) {
$str .= $var['name'];
if ($var['value'] === '') {
$str .= $table['ifemp'];
} else {
$str .= '=';
}
}
$list = [];
if (is_array($var['value'])) {
foreach ($var['value'] as $v) {
$list[] = $this->encodeTemplateString($v, $table['allow']);
}
} elseif (is_object($var['value'])) {
foreach ($var['value'] as $prop => $v) {
$list[] = $this->encodeTemplateString((string)$prop, $table['allow']);
$list[] = $this->encodeTemplateString((string)$v, $table['allow']);
}
}
if ($list) {
$str .= implode(',', $list);
}
unset($list);
}
if ($str !== '') {
$result[] = $str;
}
}
if (!$result) {
return '';
}
$result = implode($table['sep'], $result);
if ($result !== '') {
$result = $table['first'] . $result;
}
return $result;
}
/**
* @param string $data
* @param bool $reserved
* @return string
*/
protected function encodeTemplateString(string $data, bool $reserved): string
{
$skip = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789-._~';
if ($reserved) {
$skip .= ':/?#[]@!$&\'()*+,;=';
}
$result = '';
$temp = '';
for ($i = 0, $len = strlen($data); $i < $len; $i++) {
if (strpos($skip, $data[$i]) !== false) {
if ($temp !== '') {
$result .= Uri::encodeComponent($temp);
$temp = '';
}
$result .= $data[$i];
continue;
}
if ($reserved && $data[$i] === '%') {
if (isset($data[$i + 1]) && isset($data[$i + 2])
&& strpos('ABCDEF0123456789', $data[$i + 1]) !== false
&& strpos('ABCDEF0123456789', $data[$i + 2]) !== false) {
if ($temp !== '') {
$result .= Uri::encodeComponent($temp);
}
$result .= '%' . $data[$i + 1] . $data[$i + 2];
$i += 3;
continue;
}
}
$temp .= $data[$i];
}
if ($temp !== '') {
$result .= Uri::encodeComponent($temp);
}
return $result;
}
/**
* @return string
*/
public function value(): string
{
return $this->uri;
}
public function __toString(): string
{
return $this->uri;
}
/**
* @param string $uri
* @return bool
*/
public static function isTemplate(string $uri): bool
{
$open = substr_count($uri, '{');
if ($open === 0) {
return false;
}
$close = substr_count($uri, '}');
if ($open !== $close) {
return false;
}
return (bool)preg_match(self::TEMPLATE_REGEX, $uri);
}
/**
* @param string $str
* @param int $len
* @return string
*/
protected static function prefix(string $str, int $len): string
{
if ($len === 0) {
return '';
}
if ($len >= strlen($str)) {
// Prefix is longer than string length
return $str;
}
return (string)UnicodeString::from($str)->substring(0, $len);
}
}