520 lines
15 KiB
PHP
520 lines
15 KiB
PHP
<?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);
|
|
}
|
|
} |