feat: add S3-compatible storage provider (MinIO, Ceph, R2, etc.)

Adds a new 'S3-Compatible Storage' provider that works with any
S3-API-compatible object storage service, including MinIO, Ceph,
Cloudflare R2, Backblaze B2, and others.

Changes:
- New provider class: classes/providers/storage/s3-compatible-provider.php
  - Provider key: s3compatible
  - Reads user-configured endpoint URL from settings
  - Uses path-style URL access (required by most S3-compatible services)
  - Supports credentials via AS3CF_S3COMPAT_ACCESS_KEY_ID /
    AS3CF_S3COMPAT_SECRET_ACCESS_KEY wp-config.php constants
  - Disables AWS-specific features (Block Public Access, Object Ownership)
- New provider SVG icons (s3compatible.svg, -link.svg, -round.svg)
- Registered provider in main plugin class with endpoint setting support
- Updated StorageProviderSubPage to show endpoint URL input for S3-compatible
- Built pro settings bundle with rollup (Svelte 4.2.19)
- Added package.json and updated rollup.config.mjs for pro-only builds
This commit is contained in:
2026-03-03 12:30:18 +01:00
commit 3248cbb029
2086 changed files with 359427 additions and 0 deletions

21
vendor/Gcp/rize/uri-template/LICENSE vendored Normal file
View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) [2014] [Marut Khumtong]
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,85 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Gcp\Rize;
use DeliciousBrains\WP_Offload_Media\Gcp\Rize\UriTemplate\Parser;
/**
* URI Template
*/
class UriTemplate
{
/**
* @var Parser
*/
protected $parser, $parsed = array(), $base_uri, $params = array();
public function __construct($base_uri = '', $params = array(), Parser $parser = null)
{
$this->base_uri = $base_uri;
$this->params = $params;
$this->parser = $parser ?: $this->createNodeParser();
}
/**
* Expands URI Template
*
* @param string $uri URI Template
* @param array $params URI Template's parameters
* @return string
*/
public function expand($uri, $params = array())
{
$params += $this->params;
$uri = $this->base_uri . $uri;
$result = array();
// quick check
if (($start = \strpos($uri, '{')) === \false) {
return $uri;
}
$parser = $this->parser;
$nodes = $parser->parse($uri);
foreach ($nodes as $node) {
$result[] = $node->expand($parser, $params);
}
return \implode('', $result);
}
/**
* Extracts variables from URI
*
* @param string $template
* @param string $uri
* @param bool $strict This will perform a full match
* @return null|array params or null if not match and $strict is true
*/
public function extract($template, $uri, $strict = \false)
{
$params = array();
$nodes = $this->parser->parse($template);
# PHP 8.1.0RC4-dev still throws deprecation warning for `strlen`.
# $uri = (string) $uri;
foreach ($nodes as $node) {
// if strict is given, and there's no remaining uri just return null
if ($strict && !\strlen((string) $uri)) {
return null;
}
// uri'll be truncated from the start when a match is found
$match = $node->match($this->parser, $uri, $params, $strict);
list($uri, $params) = $match;
}
// if there's remaining $uri, matching is failed
if ($strict && \strlen((string) $uri)) {
return null;
}
return $params;
}
public function getParser()
{
return $this->parser;
}
protected function createNodeParser()
{
static $parser;
if ($parser) {
return $parser;
}
return $parser = new Parser();
}
}

View File

@@ -0,0 +1,59 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Gcp\Rize\UriTemplate\Node;
use DeliciousBrains\WP_Offload_Media\Gcp\Rize\UriTemplate\Parser;
/**
* Base class for all Nodes
*/
abstract class Abstraction
{
/**
* @var string
*/
private $token;
public function __construct($token)
{
$this->token = $token;
}
/**
* Expands URI template
*
* @param Parser $parser
* @param array $params
* @return null|string
*/
public function expand(Parser $parser, array $params = array())
{
return $this->token;
}
/**
* Matches given URI against current node
*
* @param Parser $parser
* @param string $uri
* @param array $params
* @param bool $strict
* @return null|array `uri and params` or `null` if not match and $strict is true
*/
public function match(Parser $parser, $uri, $params = array(), $strict = \false)
{
// match literal string from start to end
$length = \strlen($this->token);
if (\substr($uri, 0, $length) === $this->token) {
$uri = \substr($uri, $length);
} else {
if ($strict) {
return null;
}
}
return array($uri, $params);
}
/**
* @return string
*/
public function getToken()
{
return $this->token;
}
}

View File

@@ -0,0 +1,139 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Gcp\Rize\UriTemplate\Node;
use DeliciousBrains\WP_Offload_Media\Gcp\Rize\UriTemplate\Parser;
use DeliciousBrains\WP_Offload_Media\Gcp\Rize\UriTemplate\Operator;
/**
* Description
*/
class Expression extends Abstraction
{
/**
* @var Operator\Abstraction
*/
private $operator;
/**
* @var array
*/
private $variables = array();
/**
* Whether to do a forward lookup for a given separator
* @var string
*/
private $forwardLookupSeparator;
public function __construct($token, Operator\Abstraction $operator, array $variables = null, $forwardLookupSeparator = null)
{
parent::__construct($token);
$this->operator = $operator;
$this->variables = $variables;
$this->forwardLookupSeparator = $forwardLookupSeparator;
}
/**
* @return Operator\Abstraction
*/
public function getOperator()
{
return $this->operator;
}
/**
* @return array
*/
public function getVariables()
{
return $this->variables;
}
/**
* @return string
*/
public function getForwardLookupSeparator()
{
return $this->forwardLookupSeparator;
}
/**
* @param string $forwardLookupSeparator
*/
public function setForwardLookupSeparator($forwardLookupSeparator)
{
$this->forwardLookupSeparator = $forwardLookupSeparator;
}
/**
* @param Parser $parser
* @param array $params
* @return null|string
*/
public function expand(Parser $parser, array $params = array())
{
$data = array();
$op = $this->operator;
// check for variable modifiers
foreach ($this->variables as $var) {
$val = $op->expand($parser, $var, $params);
// skip null value
if (!\is_null($val)) {
$data[] = $val;
}
}
return $data ? $op->first . \implode($op->sep, $data) : null;
}
/**
* Matches given URI against current node
*
* @param Parser $parser
* @param string $uri
* @param array $params
* @param bool $strict
* @return null|array `uri and params` or `null` if not match and $strict is true
*/
public function match(Parser $parser, $uri, $params = array(), $strict = \false)
{
$op = $this->operator;
// check expression operator first
if ($op->id && isset($uri[0]) && $uri[0] !== $op->id) {
return array($uri, $params);
}
// remove operator from input
if ($op->id) {
$uri = \substr($uri, 1);
}
foreach ($this->sortVariables($this->variables) as $var) {
/** @var \Rize\UriTemplate\Node\Variable $regex */
$regex = '#' . $op->toRegex($parser, $var) . '#';
$val = null;
// do a forward lookup and get just the relevant part
$remainingUri = '';
$preparedUri = $uri;
if ($this->forwardLookupSeparator) {
$lastOccurrenceOfSeparator = \stripos($uri, $this->forwardLookupSeparator);
$preparedUri = \substr($uri, 0, $lastOccurrenceOfSeparator);
$remainingUri = \substr($uri, $lastOccurrenceOfSeparator);
}
if (\preg_match($regex, $preparedUri, $match)) {
// remove matched part from input
$preparedUri = \preg_replace($regex, '', $preparedUri, $limit = 1);
$val = $op->extract($parser, $var, $match[0]);
} else {
if ($strict) {
return null;
}
}
$uri = $preparedUri . $remainingUri;
$params[$var->getToken()] = $val;
}
return array($uri, $params);
}
/**
* Sort variables before extracting data from uri.
* We have to sort vars by non-explode to explode.
*
* @param array $vars
* @return array
*/
protected function sortVariables(array $vars)
{
\usort($vars, function ($a, $b) {
return $a->options['modifier'] >= $b->options['modifier'] ? 1 : -1;
});
return $vars;
}
}

View File

@@ -0,0 +1,10 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Gcp\Rize\UriTemplate\Node;
/**
* Description
*/
class Literal extends Abstraction
{
}

View File

@@ -0,0 +1,26 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Gcp\Rize\UriTemplate\Node;
/**
* Description
*/
class Variable extends Abstraction
{
/**
* Variable name without modifier
* e.g. 'term:1' becomes 'term'
*/
public $name, $options = array('modifier' => null, 'value' => null);
public function __construct($token, array $options = array())
{
parent::__construct($token);
$this->options = $options + $this->options;
// normalize var name e.g. from 'term:1' becomes 'term'
$name = $token;
if ($options['modifier'] === ':') {
$name = \substr($name, 0, \strpos($name, $options['modifier']));
}
$this->name = $name;
}
}

View File

@@ -0,0 +1,262 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Gcp\Rize\UriTemplate\Operator;
use DeliciousBrains\WP_Offload_Media\Gcp\Rize\UriTemplate\Node;
use DeliciousBrains\WP_Offload_Media\Gcp\Rize\UriTemplate\Parser;
/**
* .------------------------------------------------------------------.
* | NUL + . / ; ? & # |
* |------------------------------------------------------------------|
* | first | "" "" "." "/" ";" "?" "&" "#" |
* | sep | "," "," "." "/" ";" "&" "&" "," |
* | named | false false false false true true true false |
* | ifemp | "" "" "" "" "" "=" "=" "" |
* | allow | U U+R U U U U U U+R |
* `------------------------------------------------------------------'
*
* named = false
* | 1 | {/list} /red,green,blue | {$value}*(?:,{$value}+)*
* | 2 | {/list*} /red/green/blue | {$value}+(?:{$sep}{$value}+)*
* | 3 | {/keys} /semi,%3B,dot,.,comma,%2C | /(\w+,?)+
* | 4 | {/keys*} /semi=%3B/dot=./comma=%2C | /(?:\w+=\w+/?)*
* named = true
* | 1 | {?list} ?list=red,green,blue | {name}=(?:\w+(?:,\w+?)*)*
* | 2 | {?list*} ?list=red&list=green&list=blue | {name}+=(?:{$value}+(?:{sep}{name}+={$value}*))*
* | 3 | {?keys} ?keys=semi,%3B,dot,.,comma,%2C | (same as 1)
* | 4 | {?keys*} ?semi=%3B&dot=.&comma=%2C | (same as 2)
*
* UNRESERVED
* ----------
* RFC 1738 ALPHA | DIGIT | "-" | "." | "_" | | "$" | "+" | "!" | "*" | "'" | "(" | ")" | ","
* RFC 3986 ALPHA | DIGIT | "-" | "." | "_" | "~"
* RFC 6570 ALPHA | DIGIT | "-" | "." | "_" | "~"
*
* RESERVED
* --------
* RFC 1738 ":" | "/" | "?" | | "@" | "!" | "$" | "&" | "'" | "(" | ")" | "*" | "+" | "," | ";" | "=" | "-" | "_" | "." |
* RFC 3986 ":" | "/" | "?" | "#" | "[" | "]" | "@" | "!" | "$" | "&" | "'" | "(" | ")" | "*" | "+" | "," | ";" | "="
* RFC 6570 ":" | "/" | "?" | "#" | "[" | "]" | "@" | "!" | "$" | "&" | "'" | "(" | ")" | "*" | "+" | "," | ";" | "="
*
* PHP_QUERY_RFC3986 was added in PHP 5.4.0
*/
abstract class Abstraction
{
/**
* start - Variable offset position, level-2 operators start at 1
* (exclude operator itself, e.g. {?query})
* first - If variables found, prepend this value to it
* named - Whether or not the expansion includes the variable or key name
* reserved - union of (unreserved / reserved / pct-encoded)
*/
public $id, $named, $sep, $empty, $reserved, $start, $first;
protected static $types = array('' => array('sep' => ',', 'named' => \false, 'empty' => '', 'reserved' => \false, 'start' => 0, 'first' => null), '+' => array('sep' => ',', 'named' => \false, 'empty' => '', 'reserved' => \true, 'start' => 1, 'first' => null), '.' => array('sep' => '.', 'named' => \false, 'empty' => '', 'reserved' => \false, 'start' => 1, 'first' => '.'), '/' => array('sep' => '/', 'named' => \false, 'empty' => '', 'reserved' => \false, 'start' => 1, 'first' => '/'), ';' => array('sep' => ';', 'named' => \true, 'empty' => '', 'reserved' => \false, 'start' => 1, 'first' => ';'), '?' => array('sep' => '&', 'named' => \true, 'empty' => '=', 'reserved' => \false, 'start' => 1, 'first' => '?'), '&' => array('sep' => '&', 'named' => \true, 'empty' => '=', 'reserved' => \false, 'start' => 1, 'first' => '&'), '#' => array('sep' => ',', 'named' => \false, 'empty' => '', 'reserved' => \true, 'start' => 1, 'first' => '#')), $loaded = array();
/**
* gen-delims | sub-delims
*/
public static $reserved_chars = array('%3A' => ':', '%2F' => '/', '%3F' => '?', '%23' => '#', '%5B' => '[', '%5D' => ']', '%40' => '@', '%21' => '!', '%24' => '$', '%26' => '&', '%27' => "'", '%28' => '(', '%29' => ')', '%2A' => '*', '%2B' => '+', '%2C' => ',', '%3B' => ';', '%3D' => '=');
/**
* RFC 3986 Allowed path characters regex except the path delimiter '/'.
*
* @var string
*/
protected static $pathRegex = '(?:[a-zA-Z0-9\\-\\._~!\\$&\'\\(\\)\\*\\+,;=%:@]+|%(?![A-Fa-f0-9]{2}))';
/**
* RFC 3986 Allowed query characters regex except the query parameter delimiter '&'.
*
* @var string
*/
protected static $queryRegex = '(?:[a-zA-Z0-9\\-\\._~!\\$\'\\(\\)\\*\\+,;=%:@\\/\\?]+|%(?![A-Fa-f0-9]{2}))';
public function __construct($id, $named, $sep, $empty, $reserved, $start, $first)
{
$this->id = $id;
$this->named = $named;
$this->sep = $sep;
$this->empty = $empty;
$this->start = $start;
$this->first = $first;
$this->reserved = $reserved;
}
public abstract function toRegex(Parser $parser, Node\Variable $var);
public function expand(Parser $parser, Node\Variable $var, array $params = array())
{
$options = $var->options;
$name = $var->name;
$is_explode = \in_array($options['modifier'], array('*', '%'));
// skip null
if (!isset($params[$name])) {
return null;
}
$val = $params[$name];
// This algorithm is based on RFC6570 http://tools.ietf.org/html/rfc6570
// non-array, e.g. string
if (!\is_array($val)) {
return $this->expandString($parser, $var, $val);
} else {
if (!$is_explode) {
return $this->expandNonExplode($parser, $var, $val);
} else {
return $this->expandExplode($parser, $var, $val);
}
}
}
public function expandString(Parser $parser, Node\Variable $var, $val)
{
$val = (string) $val;
$options = $var->options;
$result = null;
if ($options['modifier'] === ':') {
$val = \substr($val, 0, (int) $options['value']);
}
return $result . $this->encode($parser, $var, $val);
}
/**
* Non explode modifier ':'
*
* @param Parser $parser
* @param Node\Variable $var
* @param array $val
* @return null|string
*/
public function expandNonExplode(Parser $parser, Node\Variable $var, array $val)
{
if (empty($val)) {
return null;
}
return $this->encode($parser, $var, $val);
}
/**
* Explode modifier '*', '%'
*
* @param Parser $parser
* @param Node\Variable $var
* @param array $val
* @return null|string
*/
public function expandExplode(Parser $parser, Node\Variable $var, array $val)
{
if (empty($val)) {
return null;
}
return $this->encode($parser, $var, $val);
}
/**
* Encodes variable according to spec (reserved or unreserved)
*
* @param Parser $parser
* @param Node\Variable $var
* @param mixed $values
*
* @return string encoded string
*/
public function encode(Parser $parser, Node\Variable $var, $values)
{
$values = (array) $values;
$list = isset($values[0]);
$reserved = $this->reserved;
$maps = static::$reserved_chars;
$sep = $this->sep;
$assoc_sep = '=';
// non-explode modifier always use ',' as a separator
if ($var->options['modifier'] !== '*') {
$assoc_sep = $sep = ',';
}
\array_walk($values, function (&$v, $k) use($assoc_sep, $reserved, $list, $maps) {
$encoded = \rawurlencode($v);
// assoc? encode key too
if (!$list) {
$encoded = \rawurlencode($k) . $assoc_sep . $encoded;
}
// rawurlencode is compliant with 'unreserved' set
if (!$reserved) {
$v = $encoded;
} else {
$v = \str_replace(\array_keys($maps), $maps, $encoded);
}
});
return \implode($sep, $values);
}
/**
* Decodes variable
*
* @param Parser $parser
* @param Node\Variable $var
* @param mixed $values
*
* @return string decoded string
*/
public function decode(Parser $parser, Node\Variable $var, $values)
{
$single = !\is_array($values);
$values = (array) $values;
\array_walk($values, function (&$v, $k) {
$v = \rawurldecode($v);
});
return $single ? \reset($values) : $values;
}
/**
* Extracts value from variable
*
* @param Parser $parser
* @param Node\Variable $var
* @param string $data
* @return string
*/
public function extract(Parser $parser, Node\Variable $var, $data)
{
$value = $data;
$vals = \array_filter(\explode($this->sep, $data));
$options = $var->options;
switch ($options['modifier']) {
case '*':
$data = array();
foreach ($vals as $val) {
if (\strpos($val, '=') !== \false) {
list($k, $v) = \explode('=', $val);
$data[$k] = $v;
} else {
$data[] = $val;
}
}
break;
case ':':
break;
default:
$data = \strpos($data, $this->sep) !== \false ? $vals : $value;
}
return $this->decode($parser, $var, $data);
}
public static function createById($id)
{
if (!isset(static::$types[$id])) {
throw new \Exception("Invalid operator [{$id}]");
}
if (isset(static::$loaded[$id])) {
return static::$loaded[$id];
}
$op = static::$types[$id];
$class = __NAMESPACE__ . '\\' . ($op['named'] ? 'Named' : 'UnNamed');
return static::$loaded[$id] = new $class($id, $op['named'], $op['sep'], $op['empty'], $op['reserved'], $op['start'], $op['first']);
}
public static function isValid($id)
{
return isset(static::$types[$id]);
}
/**
* Returns the correct regex given the variable location in the URI
*
* @return string
*/
protected function getRegex()
{
switch ($this->id) {
case '?':
case '&':
case '#':
return self::$queryRegex;
case ';':
default:
return self::$pathRegex;
}
}
}

View File

@@ -0,0 +1,164 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Gcp\Rize\UriTemplate\Operator;
use DeliciousBrains\WP_Offload_Media\Gcp\Rize\UriTemplate\Node;
use DeliciousBrains\WP_Offload_Media\Gcp\Rize\UriTemplate\Parser;
/**
* | 1 | {?list} ?list=red,green,blue | {name}=(?:\w+(?:,\w+?)*)*
* | 2 | {?list*} ?list=red&list=green&list=blue | {name}+=(?:{$value}+(?:{sep}{name}+={$value}*))*
* | 3 | {?keys} ?keys=semi,%3B,dot,.,comma,%2C | (same as 1)
* | 4 | {?keys*} ?semi=%3B&dot=.&comma=%2C | (same as 2)
* | 5 | {?list*} ?list[]=red&list[]=green&list[]=blue | {name[]}+=(?:{$value}+(?:{sep}{name[]}+={$value}*))*
*/
class Named extends Abstraction
{
public function toRegex(Parser $parser, Node\Variable $var)
{
$regex = null;
$name = $var->name;
$value = $this->getRegex();
$options = $var->options;
if ($options['modifier']) {
switch ($options['modifier']) {
case '*':
// 2 | 4
$regex = "{$name}+=(?:{$value}+(?:{$this->sep}{$name}+={$value}*)*)" . "|{$value}+=(?:{$value}+(?:{$this->sep}{$value}+={$value}*)*)";
break;
case ':':
$regex = "{$value}\\{0,{$options['value']}\\}";
break;
case '%':
// 5
$name = $name . '+(?:%5B|\\[)[^=]*=';
$regex = "{$name}(?:{$value}+(?:{$this->sep}{$name}{$value}*)*)";
break;
default:
throw new \Exception("Unknown modifier `{$options['modifier']}`");
}
} else {
// 1, 3
$regex = "{$name}=(?:{$value}+(?:,{$value}+)*)*";
}
return '(?:&)?' . $regex;
}
public function expandString(Parser $parser, Node\Variable $var, $val)
{
$val = (string) $val;
$options = $var->options;
$result = $this->encode($parser, $var, $var->name);
// handle empty value
if ($val === '') {
return $result . $this->empty;
} else {
$result .= '=';
}
if ($options['modifier'] === ':') {
$val = \mb_substr($val, 0, (int) $options['value']);
}
return $result . $this->encode($parser, $var, $val);
}
public function expandNonExplode(Parser $parser, Node\Variable $var, array $val)
{
if (empty($val)) {
return null;
}
$result = $this->encode($parser, $var, $var->name);
if (empty($val)) {
return $result . $this->empty;
} else {
$result .= '=';
}
return $result . $this->encode($parser, $var, $val);
}
public function expandExplode(Parser $parser, Node\Variable $var, array $val)
{
if (empty($val)) {
return null;
}
$result = $this->encode($parser, $var, $var->name);
// RFC6570 doesn't specify how to handle empty list/assoc array
// for explode modifier
if (empty($val)) {
return $result . $this->empty;
}
$list = isset($val[0]);
$data = array();
foreach ($val as $k => $v) {
// if value is a list, use `varname` as keyname, otherwise use `key` name
$key = $list ? $var->name : $k;
if ($list) {
$data[$key][] = $v;
} else {
$data[$key] = $v;
}
}
// if it's array modifier, we have to use variable name as index
// e.g. if variable name is 'query' and value is ['limit' => 1]
// then we convert it to ['query' => ['limit' => 1]]
if (!$list and $var->options['modifier'] === '%') {
$data = array($var->name => $data);
}
return $this->encodeExplodeVars($parser, $var, $data);
}
public function extract(Parser $parser, Node\Variable $var, $data)
{
// get rid of optional `&` at the beginning
if ($data[0] === '&') {
$data = \substr($data, 1);
}
$value = $data;
$vals = \explode($this->sep, $data);
$options = $var->options;
switch ($options['modifier']) {
case '%':
\parse_str($data, $query);
return $query[$var->name];
case '*':
$data = array();
foreach ($vals as $val) {
list($k, $v) = \explode('=', $val);
// 2
if ($k === $var->getToken()) {
$data[] = $v;
} else {
$data[$k] = $v;
}
}
break;
case ':':
break;
default:
// 1, 3
// remove key from value e.g. 'lang=en,th' becomes 'en,th'
$value = \str_replace($var->getToken() . '=', '', $value);
$data = \explode(',', $value);
if (\sizeof($data) === 1) {
$data = \current($data);
}
}
return $this->decode($parser, $var, $data);
}
public function encodeExplodeVars(Parser $parser, Node\Variable $var, $data)
{
// http_build_query uses PHP_QUERY_RFC1738 encoding by default
// i.e. spaces are encoded as '+' (plus signs) we need to convert
// it to %20 RFC3986
$query = \http_build_query($data, '', $this->sep);
$query = \str_replace('+', '%20', $query);
// `%` array modifier
if ($var->options['modifier'] === '%') {
// it also uses numeric based-index by default e.g. list[] becomes list[0]
$query = \preg_replace('#%5B\\d+%5D#', '%5B%5D', $query);
} else {
// by default, http_build_query will convert array values to `a[]=1&a[]=2`
// which is different from the spec. It should be `a=1&a=2`
$query = \preg_replace('#%5B\\d+%5D#', '', $query);
}
// handle reserved charset
if ($this->reserved) {
$query = \str_replace(\array_keys(static::$reserved_chars), static::$reserved_chars, $query);
}
return $query;
}
}

View File

@@ -0,0 +1,40 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Gcp\Rize\UriTemplate\Operator;
use DeliciousBrains\WP_Offload_Media\Gcp\Rize\UriTemplate\Node;
use DeliciousBrains\WP_Offload_Media\Gcp\Rize\UriTemplate\Parser;
/**
* | 1 | {/list} /red,green,blue | {$value}*(?:,{$value}+)*
* | 2 | {/list*} /red/green/blue | {$value}+(?:{$sep}{$value}+)*
* | 3 | {/keys} /semi,%3B,dot,.,comma,%2C | /(\w+,?)+
* | 4 | {/keys*} /semi=%3B/dot=./comma=%2C | /(?:\w+=\w+/?)*
*/
class UnNamed extends Abstraction
{
public function toRegex(Parser $parser, Node\Variable $var)
{
$regex = null;
$value = $this->getRegex();
$options = $var->options;
if ($options['modifier']) {
switch ($options['modifier']) {
case '*':
// 2 | 4
$regex = "{$value}+(?:{$this->sep}{$value}+)*";
break;
case ':':
$regex = $value . '{0,' . $options['value'] . '}';
break;
case '%':
throw new \Exception("% (array) modifier only works with Named type operators e.g. ;,?,&");
default:
throw new \Exception("Unknown modifier `{$options['modifier']}`");
}
} else {
// 1, 3
$regex = "{$value}*(?:,{$value}+)*";
}
return $regex;
}
}

View File

@@ -0,0 +1,120 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Gcp\Rize\UriTemplate;
use DeliciousBrains\WP_Offload_Media\Gcp\Rize\UriTemplate\Node;
use DeliciousBrains\WP_Offload_Media\Gcp\Rize\UriTemplate\Node\Expression;
use DeliciousBrains\WP_Offload_Media\Gcp\Rize\UriTemplate\Operator;
use DeliciousBrains\WP_Offload_Media\Gcp\Rize\UriTemplate\Operator\UnNamed;
class Parser
{
const REGEX_VARNAME = '(?:[A-z0-9_\\.]|%[0-9a-fA-F]{2})';
/**
* Parses URI Template and returns nodes
*
* @param string $template
* @return Node\Abstraction[]
*/
public function parse($template)
{
$parts = \preg_split('#(\\{[^\\}]+\\})#', $template, -1, \PREG_SPLIT_DELIM_CAPTURE | \PREG_SPLIT_NO_EMPTY);
$nodes = array();
foreach ($parts as $part) {
$node = $this->createNode($part);
// if current node has dot separator that requires a forward lookup
// for the previous node iff previous node's operator is UnNamed
if ($node instanceof Expression && $node->getOperator()->id === '.') {
if (\sizeof($nodes) > 0) {
$previousNode = $nodes[\sizeof($nodes) - 1];
if ($previousNode instanceof Expression && $previousNode->getOperator() instanceof UnNamed) {
$previousNode->setForwardLookupSeparator($node->getOperator()->id);
}
}
}
$nodes[] = $node;
}
return $nodes;
}
/**
* @param string $token
* @return Node\Abstraction
*/
protected function createNode($token)
{
// literal string
if ($token[0] !== '{') {
$node = $this->createLiteralNode($token);
} else {
// remove `{}` from expression and parse it
$node = $this->parseExpression(\substr($token, 1, -1));
}
return $node;
}
protected function parseExpression($expression)
{
$token = $expression;
$prefix = $token[0];
// not a valid operator?
if (!Operator\Abstraction::isValid($prefix)) {
// not valid chars?
if (!\preg_match('#' . self::REGEX_VARNAME . '#', $token)) {
throw new \Exception("Invalid operator [{$prefix}] found at {$token}");
}
// default operator
$prefix = null;
}
// remove operator prefix if exists e.g. '?'
if ($prefix) {
$token = \substr($token, 1);
}
// parse variables
$vars = array();
foreach (\explode(',', $token) as $var) {
$vars[] = $this->parseVariable($var);
}
return $this->createExpressionNode($token, $this->createOperatorNode($prefix), $vars);
}
protected function parseVariable($var)
{
$var = \trim($var);
$val = null;
$modifier = null;
// check for prefix (:) / explode (*) / array (%) modifier
if (\strpos($var, ':') !== \false) {
$modifier = ':';
list($varname, $val) = \explode(':', $var);
// error checking
if (!\is_numeric($val)) {
throw new \Exception("Value for `:` modifier must be numeric value [{$varname}:{$val}]");
}
}
switch ($last = \substr($var, -1)) {
case '*':
case '%':
// there can be only 1 modifier per var
if ($modifier) {
throw new \Exception("Multiple modifiers per variable are not allowed [{$var}]");
}
$modifier = $last;
$var = \substr($var, 0, -1);
break;
}
return $this->createVariableNode($var, array('modifier' => $modifier, 'value' => $val));
}
protected function createVariableNode($token, $options = array())
{
return new Node\Variable($token, $options);
}
protected function createExpressionNode($token, Operator\Abstraction $operator = null, array $vars = array())
{
return new Node\Expression($token, $operator, $vars);
}
protected function createLiteralNode($token)
{
return new Node\Literal($token);
}
protected function createOperatorNode($token)
{
return Operator\Abstraction::createById($token);
}
}

View File

@@ -0,0 +1,11 @@
<?php
namespace DeliciousBrains\WP_Offload_Media\Gcp\Rize\UriTemplate;
use DeliciousBrains\WP_Offload_Media\Gcp\Rize\UriTemplate as Template;
/**
* Future compatibility
*/
class UriTemplate extends Template
{
}