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
247 lines
9.3 KiB
PHP
247 lines
9.3 KiB
PHP
<?php
|
|
|
|
namespace DeliciousBrains\WP_Offload_Media\Aws3\Aws\Api;
|
|
|
|
use DeliciousBrains\WP_Offload_Media\Aws3\Aws;
|
|
/**
|
|
* Validates a schema against a hash of input.
|
|
*/
|
|
class Validator
|
|
{
|
|
private $path = [];
|
|
private $errors = [];
|
|
private $constraints = [];
|
|
private static $defaultConstraints = ['required' => \true, 'min' => \true, 'max' => \false, 'pattern' => \false];
|
|
/**
|
|
* @param array $constraints Associative array of constraints to enforce.
|
|
* Accepts the following keys: "required", "min",
|
|
* "max", and "pattern". If a key is not
|
|
* provided, the constraint will assume false.
|
|
*/
|
|
public function __construct(array $constraints = null)
|
|
{
|
|
static $assumedFalseValues = ['required' => \false, 'min' => \false, 'max' => \false, 'pattern' => \false];
|
|
$this->constraints = empty($constraints) ? self::$defaultConstraints : $constraints + $assumedFalseValues;
|
|
}
|
|
/**
|
|
* Validates the given input against the schema.
|
|
*
|
|
* @param string $name Operation name
|
|
* @param Shape $shape Shape to validate
|
|
* @param array $input Input to validate
|
|
*
|
|
* @throws \InvalidArgumentException if the input is invalid.
|
|
*/
|
|
public function validate($name, Shape $shape, array $input)
|
|
{
|
|
$this->dispatch($shape, $input);
|
|
if ($this->errors) {
|
|
$message = \sprintf("Found %d error%s while validating the input provided for the " . "%s operation:\n%s", \count($this->errors), \count($this->errors) > 1 ? 's' : '', $name, \implode("\n", $this->errors));
|
|
$this->errors = [];
|
|
throw new \InvalidArgumentException($message);
|
|
}
|
|
}
|
|
private function dispatch(Shape $shape, $value)
|
|
{
|
|
static $methods = ['structure' => 'check_structure', 'list' => 'check_list', 'map' => 'check_map', 'blob' => 'check_blob', 'boolean' => 'check_boolean', 'integer' => 'check_numeric', 'float' => 'check_numeric', 'long' => 'check_numeric', 'string' => 'check_string', 'byte' => 'check_string', 'char' => 'check_string'];
|
|
$type = $shape->getType();
|
|
if (isset($methods[$type])) {
|
|
$this->{$methods[$type]}($shape, $value);
|
|
}
|
|
}
|
|
private function check_structure(StructureShape $shape, $value)
|
|
{
|
|
$isDocument = isset($shape['document']) && $shape['document'];
|
|
$isUnion = isset($shape['union']) && $shape['union'];
|
|
if ($isDocument) {
|
|
if (!$this->checkDocumentType($value)) {
|
|
$this->addError("is not a valid document type");
|
|
return;
|
|
}
|
|
} elseif ($isUnion) {
|
|
if (!$this->checkUnion($value)) {
|
|
$this->addError("is a union type and must have exactly one non null value");
|
|
return;
|
|
}
|
|
} elseif (!$this->checkAssociativeArray($value)) {
|
|
return;
|
|
}
|
|
if ($this->constraints['required'] && $shape['required']) {
|
|
foreach ($shape['required'] as $req) {
|
|
if (!isset($value[$req])) {
|
|
$this->path[] = $req;
|
|
$this->addError('is missing and is a required parameter');
|
|
\array_pop($this->path);
|
|
}
|
|
}
|
|
}
|
|
if (!$isDocument) {
|
|
foreach ($value as $name => $v) {
|
|
if ($shape->hasMember($name)) {
|
|
$this->path[] = $name;
|
|
$this->dispatch($shape->getMember($name), isset($value[$name]) ? $value[$name] : null);
|
|
\array_pop($this->path);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
private function check_list(ListShape $shape, $value)
|
|
{
|
|
if (!\is_array($value)) {
|
|
$this->addError('must be an array. Found ' . Aws\describe_type($value));
|
|
return;
|
|
}
|
|
$this->validateRange($shape, \count($value), "list element count");
|
|
$items = $shape->getMember();
|
|
foreach ($value as $index => $v) {
|
|
$this->path[] = $index;
|
|
$this->dispatch($items, $v);
|
|
\array_pop($this->path);
|
|
}
|
|
}
|
|
private function check_map(MapShape $shape, $value)
|
|
{
|
|
if (!$this->checkAssociativeArray($value)) {
|
|
return;
|
|
}
|
|
$values = $shape->getValue();
|
|
foreach ($value as $key => $v) {
|
|
$this->path[] = $key;
|
|
$this->dispatch($values, $v);
|
|
\array_pop($this->path);
|
|
}
|
|
}
|
|
private function check_blob(Shape $shape, $value)
|
|
{
|
|
static $valid = ['string' => \true, 'integer' => \true, 'double' => \true, 'resource' => \true];
|
|
$type = \gettype($value);
|
|
if (!isset($valid[$type])) {
|
|
if ($type != 'object' || !\method_exists($value, '__toString')) {
|
|
$this->addError('must be an fopen resource, a ' . 'DeliciousBrains\\WP_Offload_Media\\Aws3\\GuzzleHttp\\Stream\\StreamInterface object, or something ' . 'that can be cast to a string. Found ' . Aws\describe_type($value));
|
|
}
|
|
}
|
|
}
|
|
private function check_numeric(Shape $shape, $value)
|
|
{
|
|
if (!\is_numeric($value)) {
|
|
$this->addError('must be numeric. Found ' . Aws\describe_type($value));
|
|
return;
|
|
}
|
|
$this->validateRange($shape, $value, "numeric value");
|
|
}
|
|
private function check_boolean(Shape $shape, $value)
|
|
{
|
|
if (!\is_bool($value)) {
|
|
$this->addError('must be a boolean. Found ' . Aws\describe_type($value));
|
|
}
|
|
}
|
|
private function check_string(Shape $shape, $value)
|
|
{
|
|
if ($shape['jsonvalue']) {
|
|
if (!self::canJsonEncode($value)) {
|
|
$this->addError('must be a value encodable with \'json_encode\'.' . ' Found ' . Aws\describe_type($value));
|
|
}
|
|
return;
|
|
}
|
|
if (!$this->checkCanString($value)) {
|
|
$this->addError('must be a string or an object that implements ' . '__toString(). Found ' . Aws\describe_type($value));
|
|
return;
|
|
}
|
|
$value = isset($value) ? $value : '';
|
|
$this->validateRange($shape, \strlen($value), "string length");
|
|
if ($this->constraints['pattern']) {
|
|
$pattern = $shape['pattern'];
|
|
if ($pattern && !\preg_match("/{$pattern}/", $value)) {
|
|
$this->addError("Pattern /{$pattern}/ failed to match '{$value}'");
|
|
}
|
|
}
|
|
}
|
|
private function validateRange(Shape $shape, $length, $descriptor)
|
|
{
|
|
if ($this->constraints['min']) {
|
|
$min = $shape['min'];
|
|
if ($min && $length < $min) {
|
|
$this->addError("expected {$descriptor} to be >= {$min}, but " . "found {$descriptor} of {$length}");
|
|
}
|
|
}
|
|
if ($this->constraints['max']) {
|
|
$max = $shape['max'];
|
|
if ($max && $length > $max) {
|
|
$this->addError("expected {$descriptor} to be <= {$max}, but " . "found {$descriptor} of {$length}");
|
|
}
|
|
}
|
|
}
|
|
private function checkArray($arr)
|
|
{
|
|
return $this->isIndexed($arr) || $this->isAssociative($arr);
|
|
}
|
|
private function isAssociative($arr)
|
|
{
|
|
return \count(\array_filter(\array_keys($arr), "is_string")) == \count($arr);
|
|
}
|
|
private function isIndexed(array $arr)
|
|
{
|
|
return $arr == \array_values($arr);
|
|
}
|
|
private function checkCanString($value)
|
|
{
|
|
static $valid = ['string' => \true, 'integer' => \true, 'double' => \true, 'NULL' => \true];
|
|
$type = \gettype($value);
|
|
return isset($valid[$type]) || $type == 'object' && \method_exists($value, '__toString');
|
|
}
|
|
private function checkAssociativeArray($value)
|
|
{
|
|
$isAssociative = \false;
|
|
if (\is_array($value)) {
|
|
$expectedIndex = 0;
|
|
$key = \key($value);
|
|
do {
|
|
$isAssociative = $key !== $expectedIndex++;
|
|
\next($value);
|
|
$key = \key($value);
|
|
} while (!$isAssociative && null !== $key);
|
|
}
|
|
if (!$isAssociative) {
|
|
$this->addError('must be an associative array. Found ' . Aws\describe_type($value));
|
|
return \false;
|
|
}
|
|
return \true;
|
|
}
|
|
private function checkDocumentType($value)
|
|
{
|
|
if (\is_array($value)) {
|
|
$typeOfFirstKey = \gettype(\key($value));
|
|
foreach ($value as $key => $val) {
|
|
if (!$this->checkDocumentType($val) || \gettype($key) != $typeOfFirstKey) {
|
|
return \false;
|
|
}
|
|
}
|
|
return $this->checkArray($value);
|
|
}
|
|
return \is_null($value) || \is_numeric($value) || \is_string($value) || \is_bool($value);
|
|
}
|
|
private function checkUnion($value)
|
|
{
|
|
if (\is_array($value)) {
|
|
$nonNullCount = 0;
|
|
foreach ($value as $key => $val) {
|
|
if (!\is_null($val) && !(\strpos($key, "@") === 0)) {
|
|
$nonNullCount++;
|
|
}
|
|
}
|
|
return $nonNullCount == 1;
|
|
}
|
|
return !\is_null($value);
|
|
}
|
|
private function addError($message)
|
|
{
|
|
$this->errors[] = \implode('', \array_map(function ($s) {
|
|
return "[{$s}]";
|
|
}, $this->path)) . ' ' . $message;
|
|
}
|
|
private function canJsonEncode($data)
|
|
{
|
|
return !\is_resource($data);
|
|
}
|
|
}
|