From 1de0622fc889cca4651feba42fde70b08a0a909f Mon Sep 17 00:00:00 2001 From: Cees-Jan Kiewiet Date: Sat, 12 Jun 2021 15:00:57 +0200 Subject: [PATCH] House webhook related files --- composer.json | 25 +- composer.lock | 63 +-- external_files/cebe/OpenApi.php | 84 +++ external_files/cebe/Schema.php | 157 ++++++ external_files/cebe/SpecBaseObject.php | 515 ++++++++++++++++++ external_files/cebe/Type.php | 45 ++ external_files/cebe/WebHooks.php | 302 ++++++++++ .../thephpleague/SchemaValidator.php | 176 ++++++ external_files/thephpleague/Type.php | 111 ++++ 9 files changed, 1425 insertions(+), 53 deletions(-) create mode 100644 external_files/cebe/OpenApi.php create mode 100644 external_files/cebe/Schema.php create mode 100644 external_files/cebe/SpecBaseObject.php create mode 100644 external_files/cebe/Type.php create mode 100644 external_files/cebe/WebHooks.php create mode 100644 external_files/thephpleague/SchemaValidator.php create mode 100644 external_files/thephpleague/Type.php diff --git a/composer.json b/composer.json index d02abfb..a21ccef 100644 --- a/composer.json +++ b/composer.json @@ -20,7 +20,7 @@ }, "require": { "php": "^7.4", - "cebe/php-openapi": "dev-webhooks as 1.6.0", + "cebe/php-openapi": "^1.5", "jawira/case-converter": "^3.4", "twig/twig": "^3.0", "nikic/php-parser": "^4.8", @@ -29,23 +29,22 @@ "symfony/yaml": "^5.2", "wyrihaximus/composer-update-bin-autoload-path": "^1 || ^1.0.1", "wyrihaximus/hydrator": "dev-master", - "league/openapi-psr7-validator": "dev-webhooks as 0.16.99" + "league/openapi-psr7-validator": "^0.16" }, "autoload": { "psr-4": { "ApiClients\\Tools\\OpenApiClientGenerator\\": "src/" - } - }, - "repositories": [ - { - "type": "vcs", - "url": "git@github.com:WyriHaximus-labs/php-openapi.git" }, - { - "type": "vcs", - "url": "git@github.com:WyriHaximus-labs/openapi-psr7-validator.git" - } - ], + "files": [ + "external_files/cebe/SpecBaseObject.php", + "external_files/cebe/OpenApi.php", + "external_files/cebe/Schema.php", + "external_files/cebe/Type.php", + "external_files/cebe/WebHooks.php", + "external_files/thephpleague/SchemaValidator.php", + "external_files/thephpleague/Type.php" + ] + }, "config": { "platform": { "php": "7.4.7" diff --git a/composer.lock b/composer.lock index ce0ec1e..168a6ba 100644 --- a/composer.lock +++ b/composer.lock @@ -4,20 +4,20 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "502f644283921ac6b347f7f0b73a6235", + "content-hash": "1efcea84e72f9ae2be20504a7da849f6", "packages": [ { "name": "cebe/php-openapi", - "version": "dev-webhooks", + "version": "1.5.2", "source": { "type": "git", - "url": "https://github.com/WyriHaximus-labs/php-openapi.git", - "reference": "a0ed996272ae7a199df96a5af204c7e01680aeec" + "url": "https://github.com/cebe/php-openapi.git", + "reference": "8f1f70688fd4bea04410718616450a38b7b8c40b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WyriHaximus-labs/php-openapi/zipball/a0ed996272ae7a199df96a5af204c7e01680aeec", - "reference": "a0ed996272ae7a199df96a5af204c7e01680aeec", + "url": "https://api.github.com/repos/cebe/php-openapi/zipball/8f1f70688fd4bea04410718616450a38b7b8c40b", + "reference": "8f1f70688fd4bea04410718616450a38b7b8c40b", "shasum": "" }, "require": { @@ -49,6 +49,7 @@ "cebe\\openapi\\": "src/" } }, + "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], @@ -69,7 +70,7 @@ "issues": "https://github.com/cebe/php-openapi/issues", "source": "https://github.com/cebe/php-openapi" }, - "time": "2021-06-07T18:14:16+00:00" + "time": "2021-05-24T11:32:07+00:00" }, { "name": "doctrine/annotations", @@ -551,20 +552,20 @@ }, { "name": "league/openapi-psr7-validator", - "version": "dev-webhooks", + "version": "0.16.1", "source": { "type": "git", - "url": "https://github.com/WyriHaximus-labs/openapi-psr7-validator.git", - "reference": "739d63ecfbbfe965151102fd27063c09b78cf384" + "url": "https://github.com/thephpleague/openapi-psr7-validator.git", + "reference": "580ebf26336240417f253f77441aa12730b60735" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WyriHaximus-labs/openapi-psr7-validator/zipball/739d63ecfbbfe965151102fd27063c09b78cf384", - "reference": "739d63ecfbbfe965151102fd27063c09b78cf384", + "url": "https://api.github.com/repos/thephpleague/openapi-psr7-validator/zipball/580ebf26336240417f253f77441aa12730b60735", + "reference": "580ebf26336240417f253f77441aa12730b60735", "shasum": "" }, "require": { - "cebe/php-openapi": "dev-webhooks as 1.4.0", + "cebe/php-openapi": "^1.3", "ext-json": "*", "league/uri": "^6.3", "php": ">=7.2", @@ -592,26 +593,23 @@ "League\\OpenAPIValidation\\": "src/" } }, - "autoload-dev": { - "psr-4": { - "League\\OpenAPIValidation\\Tests\\": "tests/" - } - }, + "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "Validate PSR-7 messages against OpenAPI (3.0.2) specifications \nexpressed in YAML or JSON", + "description": "Validate PSR-7 messages against OpenAPI (3.0.2) specifications expressed in YAML or JSON", "homepage": "https://github.com/thephpleague/openapi-psr7-validator", "keywords": [ - "OpenAPI", "http", + "openapi", "psr7", "validation" ], "support": { - "source": "https://github.com/WyriHaximus-labs/openapi-psr7-validator/tree/webhooks" + "issues": "https://github.com/thephpleague/openapi-psr7-validator/issues", + "source": "https://github.com/thephpleague/openapi-psr7-validator/tree/0.16.1" }, - "time": "2021-06-07T18:15:16+00:00" + "time": "2021-05-12T16:52:09+00:00" }, { "name": "league/uri", @@ -1972,25 +1970,10 @@ } ], "packages-dev": [], - "aliases": [ - { - "package": "cebe/php-openapi", - "version": "dev-webhooks", - "alias": "1.6.0", - "alias_normalized": "1.6.0.0" - }, - { - "package": "league/openapi-psr7-validator", - "version": "dev-webhooks", - "alias": "0.16.99", - "alias_normalized": "0.16.99.0" - } - ], + "aliases": [], "minimum-stability": "stable", "stability-flags": { - "cebe/php-openapi": 20, - "wyrihaximus/hydrator": 20, - "league/openapi-psr7-validator": 20 + "wyrihaximus/hydrator": 20 }, "prefer-stable": false, "prefer-lowest": false, @@ -2001,5 +1984,5 @@ "platform-overrides": { "php": "7.4.7" }, - "plugin-api-version": "2.0.0" + "plugin-api-version": "2.1.0" } diff --git a/external_files/cebe/OpenApi.php b/external_files/cebe/OpenApi.php new file mode 100644 index 0000000..fccdae7 --- /dev/null +++ b/external_files/cebe/OpenApi.php @@ -0,0 +1,84 @@ + and contributors + * @license https://github.com/cebe/php-openapi/blob/master/LICENSE + */ + +namespace cebe\openapi\spec; + +use cebe\openapi\exceptions\TypeErrorException; +use cebe\openapi\SpecBaseObject; + +/** + * This is the root document object of the OpenAPI document. + * + * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#openapi-object + * + * @property string $openapi + * @property Info $info + * @property Server[] $servers + * @property Paths|PathItem[] $paths + * @property Components|null $components + * @property WebHooks|null $webhooks + * @property SecurityRequirement[] $security + * @property Tag[] $tags + * @property ExternalDocumentation|null $externalDocs + * + */ +class OpenApi extends SpecBaseObject +{ + /** + * @return array array of attributes available in this object. + */ + protected function attributes(): array + { + return [ + 'openapi' => Type::STRING, + 'info' => Info::class, + 'servers' => [Server::class], + 'paths' => Paths::class, + 'webhooks' => WebHooks::class, + 'components' => Components::class, + 'security' => [SecurityRequirement::class], + 'tags' => [Tag::class], + 'externalDocs' => ExternalDocumentation::class, + ]; + } + + /** + * @return array array of attributes default values. + */ + protected function attributeDefaults(): array + { + return [ + // Spec: If the servers property is not provided, or is an empty array, + // the default value would be a Server Object with a url value of /. + 'servers' => [ + new Server(['url' => '/']) + ], + ]; + } + + public function __get($name) + { + $ret = parent::__get($name); + // Spec: If the servers property is not provided, or is an empty array, + // the default value would be a Server Object with a url value of /. + if ($name === 'servers' && $ret === []) { + return $this->attributeDefaults()['servers']; + } + return $ret; + } + + /** + * Perform validation on this object, check data against OpenAPI Specification rules. + */ + public function performValidation() + { + $this->requireProperties(['openapi', 'info'], ['paths', 'webhooks']); + if (!empty($this->openapi) && !preg_match('/^3\.(0|1)\.\d+(-rc\d)?$/i', $this->openapi)) { + $this->addError('Unsupported openapi version: ' . $this->openapi); + } + } +} diff --git a/external_files/cebe/Schema.php b/external_files/cebe/Schema.php new file mode 100644 index 0000000..4b41309 --- /dev/null +++ b/external_files/cebe/Schema.php @@ -0,0 +1,157 @@ + and contributors + * @license https://github.com/cebe/php-openapi/blob/master/LICENSE + */ + +namespace cebe\openapi\spec; + +use cebe\openapi\exceptions\TypeErrorException; +use cebe\openapi\SpecBaseObject; + +/** + * The Schema Object allows the definition of input and output data types. + * + * These types can be objects, but also primitives and arrays. This object is an extended subset of the + * [JSON Schema Specification Wright Draft 00](http://json-schema.org/). + * + * For more information about the properties, see + * [JSON Schema Core](https://tools.ietf.org/html/draft-wright-json-schema-00) and + * [JSON Schema Validation](https://tools.ietf.org/html/draft-wright-json-schema-validation-00). + * Unless stated otherwise, the property definitions follow the JSON Schema. + * + * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#schemaObject + * + * @property string $title + * @property int|float $multipleOf + * @property int|float $maximum + * @property bool $exclusiveMaximum + * @property int|float $minimum + * @property bool $exclusiveMinimum + * @property int $maxLength + * @property int $minLength + * @property string $pattern (This string SHOULD be a valid regular expression, according to the [ECMA 262 regular expression dialect](https://www.ecma-international.org/ecma-262/5.1/#sec-7.8.5)) + * @property int $maxItems + * @property int $minItems + * @property bool $uniqueItems + * @property int $maxProperties + * @property int $minProperties + * @property string[] $required list of required properties + * @property array $enum + * + * @property string|string[] $type + * @property Schema[]|Reference[] $allOf + * @property Schema[]|Reference[] $oneOf + * @property Schema[]|Reference[] $anyOf + * @property Schema|Reference|null $not + * @property Schema|Reference|null $items + * @property Schema[]|Reference[] $properties + * @property Schema|Reference|bool $additionalProperties + * @property string $description + * @property string $format + * @property mixed $default + * + * @property bool $nullable + * @property Discriminator|null $discriminator + * @property bool $readOnly + * @property bool $writeOnly + * @property Xml|null $xml + * @property ExternalDocumentation|null $externalDocs + * @property mixed $example + * @property bool $deprecated + * + */ +class Schema extends SpecBaseObject +{ + /** + * @return array array of attributes available in this object. + */ + protected function attributes(): array + { + return [ + // The following properties are taken directly from the JSON Schema definition and follow the same specifications: + // types from https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-4 ff. + 'title' => Type::STRING, + 'multipleOf' => Type::NUMBER, + 'maximum' => Type::NUMBER, + 'exclusiveMaximum' => Type::BOOLEAN, + 'minimum' => Type::NUMBER, + 'exclusiveMinimum' => Type::BOOLEAN, + 'maxLength' => Type::INTEGER, + 'minLength' => Type::INTEGER, + 'pattern' => Type::STRING, + 'maxItems' => Type::INTEGER, + 'minItems' => Type::INTEGER, + 'uniqueItems' => Type::BOOLEAN, + 'maxProperties' => Type::INTEGER, + 'minProperties' => Type::INTEGER, + 'required' => [Type::STRING], + 'enum' => [Type::ANY], + // The following properties are taken from the JSON Schema definition but their definitions were adjusted to the OpenAPI Specification. + 'type' => Type::STRING, + 'allOf' => [Schema::class], + 'oneOf' => [Schema::class], + 'anyOf' => [Schema::class], + 'not' => Schema::class, + 'items' => Schema::class, + 'properties' => [Type::STRING, Schema::class], + //'additionalProperties' => 'boolean' | ['string', Schema::class], handled in constructor + 'description' => Type::STRING, + 'format' => Type::STRING, + 'default' => Type::ANY, + // Other than the JSON Schema subset fields, the following fields MAY be used for further schema documentation: + 'nullable' => Type::BOOLEAN, + 'discriminator' => Discriminator::class, + 'readOnly' => Type::BOOLEAN, + 'writeOnly' => Type::BOOLEAN, + 'xml' => Xml::class, + 'externalDocs' => ExternalDocumentation::class, + 'example' => Type::ANY, + 'deprecated' => Type::BOOLEAN, + ]; + } + + /** + * @return array array of attributes default values. + */ + protected function attributeDefaults(): array + { + return [ + 'additionalProperties' => true, + 'required' => null, + 'enum' => null, + 'allOf' => null, + 'oneOf' => null, + 'anyOf' => null, + ]; + } + + /** + * Create an object from spec data. + * @param array $data spec data read from YAML or JSON + * @throws TypeErrorException in case invalid data is supplied. + */ + public function __construct(array $data) + { + if (isset($data['additionalProperties'])) { + if (is_array($data['additionalProperties'])) { + $data['additionalProperties'] = $this->instantiate(Schema::class, $data['additionalProperties']); + } elseif (!($data['additionalProperties'] instanceof Schema || $data['additionalProperties'] instanceof Reference || is_bool($data['additionalProperties']))) { + $givenType = gettype($data['additionalProperties']); + if ($givenType === 'object') { + $givenType = get_class($data['additionalProperties']); + } + throw new TypeErrorException(sprintf('Schema::$additionalProperties MUST be either boolean or a Schema/Reference object, "%s" given', $givenType)); + } + } + parent::__construct($data); + } + + /** + * Perform validation on this object, check data against OpenAPI Specification rules. + */ + protected function performValidation() + { + } +} diff --git a/external_files/cebe/SpecBaseObject.php b/external_files/cebe/SpecBaseObject.php new file mode 100644 index 0000000..6e87c1f --- /dev/null +++ b/external_files/cebe/SpecBaseObject.php @@ -0,0 +1,515 @@ + and contributors + * @license https://github.com/cebe/php-openapi/blob/master/LICENSE + */ + +namespace cebe\openapi; + +use cebe\openapi\exceptions\TypeErrorException; +use cebe\openapi\exceptions\UnknownPropertyException; +use cebe\openapi\json\JsonPointer; +use cebe\openapi\json\JsonReference; +use cebe\openapi\spec\Reference; +use cebe\openapi\spec\Type; + +/** + * Base class for all spec objects. + * + * Implements property management and validation basics. + * + */ +abstract class SpecBaseObject implements SpecObjectInterface, DocumentContextInterface +{ + private $_properties = []; + private $_errors = []; + + private $_recursingSerializableData = false; + private $_recursingValidate = false; + private $_recursingErrors = false; + private $_recursingReferences = false; + private $_recursingReferenceContext = false; + private $_recursingDocumentContext = false; + + private $_baseDocument; + private $_jsonPointer; + + + /** + * @return array array of attributes available in this object. + */ + abstract protected function attributes(): array; + + /** + * @return array array of attributes default values. + */ + protected function attributeDefaults(): array + { + return []; + } + + /** + * Perform validation on this object, check data against OpenAPI Specification rules. + * + * Call `addError()` in case of validation errors. + */ + abstract protected function performValidation(); + + /** + * Create an object from spec data. + * @param array $data spec data read from YAML or JSON + * @throws TypeErrorException in case invalid data is supplied. + */ + public function __construct(array $data) + { + foreach ($this->attributes() as $property => $type) { + if (!isset($data[$property])) { + continue; + } + + if ($type === Type::BOOLEAN) { + if (!\is_bool($data[$property])) { + $this->_errors[] = "property '$property' must be boolean, but " . gettype($data[$property]) . " given."; + continue; + } + $this->_properties[$property] = (bool) $data[$property]; + } elseif (\is_array($type)) { + if (!\is_array($data[$property])) { + $this->_errors[] = "property '$property' must be array, but " . gettype($data[$property]) . " given."; + continue; + } + switch (\count($type)) { + case 1: + if (isset($data[$property]['$ref'])) { + $this->_properties[$property] = new Reference($data[$property], null); + } else { + // array + $this->_properties[$property] = []; + foreach ($data[$property] as $item) { + if ($type[0] === Type::STRING) { + if (!is_string($item)) { + $this->_errors[] = "property '$property' must be array of strings, but array has " . gettype($item) . " element."; + } + $this->_properties[$property][] = $item; + } elseif (Type::isScalar($type[0])) { + $this->_properties[$property][] = $item; + } elseif ($type[0] === Type::ANY) { + if (is_array($item) && isset($item['$ref'])) { + $this->_properties[$property][] = new Reference($item, null); + } else { + $this->_properties[$property][] = $item; + } + } else { + $this->_properties[$property][] = $this->instantiate($type[0], $item); + } + } + } + break; + case 2: + // map + if ($type[0] !== Type::STRING) { + throw new TypeErrorException('Invalid map key type: ' . $type[0]); + } + $this->_properties[$property] = []; + foreach ($data[$property] as $key => $item) { + if ($type[1] === Type::STRING) { + if (!is_string($item)) { + $this->_errors[] = "property '$property' must be map, but entry '$key' is of type " . \gettype($item) . '.'; + } + $this->_properties[$property][$key] = $item; + } elseif ($type[1] === Type::ANY || Type::isScalar($type[1])) { + $this->_properties[$property][$key] = $item; + } else { + $this->_properties[$property][$key] = $this->instantiate($type[1], $item); + } + } + break; + } + } elseif (Type::isScalar($type)) { + $this->_properties[$property] = $data[$property]; + } elseif ($type === Type::ANY) { + if (is_array($data[$property]) && isset($data[$property]['$ref'])) { + $this->_properties[$property] = new Reference($data[$property], null); + } else { + $this->_properties[$property] = $data[$property]; + } + } else { + $this->_properties[$property] = $this->instantiate($type, $data[$property]); + } + unset($data[$property]); + } + foreach ($data as $additionalProperty => $value) { + $this->_properties[$additionalProperty] = $value; + } + } + + /** + * @throws TypeErrorException + */ + protected function instantiate($type, $data) + { + if ($data instanceof $type || $data instanceof Reference) { + return $data; + } + + if (is_array($data) && isset($data['$ref'])) { + return new Reference($data, $type); + } + + if (!is_array($data)) { + throw new TypeErrorException( + "Unable to instantiate {$type} Object with data '" . print_r($data, true) . "' at " . $this->getDocumentPosition() + ); + } + try { + return new $type($data); + } catch (\TypeError $e) { + throw new TypeErrorException( + "Unable to instantiate {$type} Object with data '" . print_r($data, true) . "' at " . $this->getDocumentPosition(), + $e->getCode(), + $e + ); + } + } + + /** + * @return mixed returns the serializable data of this object for converting it + * to JSON or YAML. + */ + public function getSerializableData() + { + if ($this->_recursingSerializableData) { + // return a reference + return (object) ['$ref' => JsonReference::createFromUri('', $this->getDocumentPosition())->getReference()]; + } + $this->_recursingSerializableData = true; + + $data = $this->_properties; + foreach ($data as $k => $v) { + if ($v instanceof SpecObjectInterface) { + $data[$k] = $v->getSerializableData(); + } elseif (is_array($v)) { + $toObject = false; + $j = 0; + foreach ($v as $i => $d) { + if ($j++ !== $i) { + $toObject = true; + } + if ($d instanceof SpecObjectInterface) { + $data[$k][$i] = $d->getSerializableData(); + } + } + if ($toObject) { + $data[$k] = (object) $data[$k]; + } + } + } + + $this->_recursingSerializableData = false; + + return (object) $data; + } + + /** + * Validate object data according to OpenAPI spec. + * @return bool whether the loaded data is valid according to OpenAPI spec + * @see getErrors() + */ + public function validate(): bool + { + // avoid recursion to get stuck in a loop + if ($this->_recursingValidate) { + return true; + } + $this->_recursingValidate = true; + $valid = true; + foreach ($this->_properties as $v) { + if ($v instanceof SpecObjectInterface) { + if (!$v->validate()) { + $valid = false; + } + } elseif (is_array($v)) { + foreach ($v as $item) { + if ($item instanceof SpecObjectInterface) { + if (!$item->validate()) { + $valid = false; + } + } + } + } + } + $this->_recursingValidate = false; + + $this->performValidation(); + + if (!empty($this->_errors)) { + $valid = false; + } + + return $valid; + } + + /** + * @return string[] list of validation errors according to OpenAPI spec. + * @see validate() + */ + public function getErrors(): array + { + // avoid recursion to get stuck in a loop + if ($this->_recursingErrors) { + return []; + } + $this->_recursingErrors = true; + + if (($pos = $this->getDocumentPosition()) !== null) { + $errors = [ + array_map(function ($e) use ($pos) { + return "[{$pos->getPointer()}] $e"; + }, $this->_errors) + ]; + } else { + $errors = [$this->_errors]; + } + foreach ($this->_properties as $v) { + if ($v instanceof SpecObjectInterface) { + $errors[] = $v->getErrors(); + } elseif (is_array($v)) { + foreach ($v as $item) { + if ($item instanceof SpecObjectInterface) { + $errors[] = $item->getErrors(); + } + } + } + } + + $this->_recursingErrors = false; + + return array_merge(...$errors); + } + + /** + * @param string $error error message to add. + */ + protected function addError(string $error, $class = '') + { + $shortName = explode('\\', $class); + $this->_errors[] = end($shortName).$error; + } + + protected function hasProperty(string $name): bool + { + return isset($this->_properties[$name]) || isset($this->attributes()[$name]); + } + + protected function requireProperties(array $names, array $atLeastOne = []) + { + foreach ($names as $name) { + if (!isset($this->_properties[$name])) { + $this->addError(" is missing required property: $name", get_class($this)); + } + } + + if (count($atLeastOne) > 0) { + foreach ($atLeastOne as $name) { + if (array_key_exists($name, $this->_properties)) { + return; + } + } + + $this->addError(" is missing at least one of the following required properties: " . implode(', ', $atLeastOne), get_class($this)); + } + } + + protected function validateEmail(string $property) + { + if (!empty($this->$property) && strpos($this->$property, '@') === false) { + $this->addError('::$'.$property.' does not seem to be a valid email address: ' . $this->$property, get_class($this)); + } + } + + protected function validateUrl(string $property) + { + if (!empty($this->$property) && strpos($this->$property, '//') === false) { + $this->addError('::$'.$property.' does not seem to be a valid URL: ' . $this->$property, get_class($this)); + } + } + + public function __get($name) + { + if (isset($this->_properties[$name])) { + return $this->_properties[$name]; + } + $defaults = $this->attributeDefaults(); + if (array_key_exists($name, $defaults)) { + return $defaults[$name]; + } + if (isset($this->attributes()[$name])) { + if (is_array($this->attributes()[$name])) { + return []; + } elseif ($this->attributes()[$name] === Type::BOOLEAN) { + return false; + } + return null; + } + throw new UnknownPropertyException('Getting unknown property: ' . \get_class($this) . '::' . $name); + } + + public function __set($name, $value) + { + $this->_properties[$name] = $value; + } + + public function __isset($name) + { + if (isset($this->_properties[$name]) || isset($this->attributeDefaults()[$name]) || isset($this->attributes()[$name])) { + return $this->__get($name) !== null; + } + + return false; + } + + public function __unset($name) + { + unset($this->_properties[$name]); + } + + /** + * Resolves all Reference Objects in this object and replaces them with their resolution. + * @throws exceptions\UnresolvableReferenceException in case resolving a reference fails. + */ + public function resolveReferences(ReferenceContext $context = null) + { + // avoid recursion to get stuck in a loop + if ($this->_recursingReferences) { + return; + } + $this->_recursingReferences = true; + + foreach ($this->_properties as $property => $value) { + if ($value instanceof Reference) { + $referencedObject = $value->resolve($context); + $this->_properties[$property] = $referencedObject; + if (!$referencedObject instanceof Reference && $referencedObject instanceof SpecObjectInterface) { + $referencedObject->resolveReferences(); + } + } elseif ($value instanceof SpecObjectInterface) { + $value->resolveReferences($context); + } elseif (is_array($value)) { + foreach ($value as $k => $item) { + if ($item instanceof Reference) { + $referencedObject = $item->resolve($context); + $this->_properties[$property][$k] = $referencedObject; + if (!$referencedObject instanceof Reference && $referencedObject instanceof SpecObjectInterface) { + $referencedObject->resolveReferences(); + } + } elseif ($item instanceof SpecObjectInterface) { + $item->resolveReferences($context); + } + } + } + } + + $this->_recursingReferences = false; + } + + /** + * Set context for all Reference Objects in this object. + */ + public function setReferenceContext(ReferenceContext $context) + { + // avoid recursion to get stuck in a loop + if ($this->_recursingReferenceContext) { + return; + } + $this->_recursingReferenceContext = true; + + foreach ($this->_properties as $property => $value) { + if ($value instanceof Reference) { + $value->setContext($context); + } elseif ($value instanceof SpecObjectInterface) { + $value->setReferenceContext($context); + } elseif (is_array($value)) { + foreach ($value as $k => $item) { + if ($item instanceof Reference) { + $item->setContext($context); + } elseif ($item instanceof SpecObjectInterface) { + $item->setReferenceContext($context); + } + } + } + } + + $this->_recursingReferenceContext = false; + } + + /** + * Provide context information to the object. + * + * Context information contains a reference to the base object where it is contained in + * as well as a JSON pointer to its position. + * @param SpecObjectInterface $baseDocument + * @param JsonPointer $jsonPointer + */ + public function setDocumentContext(SpecObjectInterface $baseDocument, JsonPointer $jsonPointer) + { + $this->_baseDocument = $baseDocument; + $this->_jsonPointer = $jsonPointer; + + // avoid recursion to get stuck in a loop + if ($this->_recursingDocumentContext) { + return; + } + $this->_recursingDocumentContext = true; + + foreach ($this->_properties as $property => $value) { + if ($value instanceof DocumentContextInterface) { + $value->setDocumentContext($baseDocument, $jsonPointer->append($property)); + } elseif (is_array($value)) { + foreach ($value as $k => $item) { + if ($item instanceof DocumentContextInterface) { + $item->setDocumentContext($baseDocument, $jsonPointer->append($property)->append($k)); + } + } + } + } + + $this->_recursingDocumentContext = false; + } + + /** + * @return SpecObjectInterface|null returns the base document where this object is located in. + * Returns `null` if no context information was provided by [[setDocumentContext]]. + */ + public function getBaseDocument(): ?SpecObjectInterface + { + return $this->_baseDocument; + } + + /** + * @return JsonPointer|null returns a JSON pointer describing the position of this object in the base document. + * Returns `null` if no context information was provided by [[setDocumentContext]]. + */ + public function getDocumentPosition(): ?JsonPointer + { + return $this->_jsonPointer; + } + + /** + * Returns extension properties with `x-` prefix. + * @see https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#specificationExtensions + * @return array + * @since 1.5.3 + */ + public function getExtensions(): array + { + $extensions = []; + foreach ($this->_properties as $propertyKey => $extension) { + if (strpos($propertyKey, 'x-') !== 0) { + continue; + } + $extensions[$propertyKey] = $extension; + } + return $extensions; + } +} diff --git a/external_files/cebe/Type.php b/external_files/cebe/Type.php new file mode 100644 index 0000000..2cdcb91 --- /dev/null +++ b/external_files/cebe/Type.php @@ -0,0 +1,45 @@ + and contributors + * @license https://github.com/cebe/php-openapi/blob/master/LICENSE + */ + +namespace cebe\openapi\spec; + +/** + * Data Types + * + * @link https://github.com/OAI/OpenAPI-Specification/blob/3.0.2/versions/3.0.2.md#dataTypes + */ +class Type +{ + const ANY = 'any'; + const INTEGER = 'integer'; + const NUMBER = 'number'; + const STRING = 'string'; + const BOOLEAN = 'boolean'; + const OBJECT = 'object'; + const ARRAY = 'array'; + const NULL = 'null'; + + /** + * Indicate whether a type is a scalar type, i.e. not an array or object. + * + * For ANY this will return false. + * + * @param string $type value from one of the type constants defined in this class. + * @return bool whether the type is a scalar type. + * @since 1.2.1 + */ + public static function isScalar(string $type): bool + { + return in_array($type, [ + self::INTEGER, + self::NUMBER, + self::STRING, + self::BOOLEAN, + self::NULL, + ]); + } +} diff --git a/external_files/cebe/WebHooks.php b/external_files/cebe/WebHooks.php new file mode 100644 index 0000000..640bbd4 --- /dev/null +++ b/external_files/cebe/WebHooks.php @@ -0,0 +1,302 @@ + and contributors + * @license https://github.com/cebe/php-openapi/blob/master/LICENSE + */ + +namespace cebe\openapi\spec; + +use ArrayAccess; +use ArrayIterator; +use cebe\openapi\DocumentContextInterface; +use cebe\openapi\exceptions\TypeErrorException; +use cebe\openapi\exceptions\UnresolvableReferenceException; +use cebe\openapi\json\JsonPointer; +use cebe\openapi\ReferenceContext; +use cebe\openapi\SpecObjectInterface; +use Countable; +use IteratorAggregate; +use Traversable; + +/** + * Holds the webhook events to the individual endpoints and their operations. + * + * @link https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.1.0.md#oasWebhooks + * + */ +class WebHooks implements SpecObjectInterface, DocumentContextInterface, ArrayAccess, Countable, IteratorAggregate +{ + /** + * @var (PathItem|null)[] + */ + private $_webHooks = []; + /** + * @var array + */ + private $_errors = []; + /** + * @var SpecObjectInterface|null + */ + private $_baseDocument; + /** + * @var JsonPointer|null + */ + private $_jsonPointer; + + + /** + * Create an object from spec data. + * @param (PathItem|array|null)[] $data spec data read from YAML or JSON + * @throws TypeErrorException in case invalid data is supplied. + */ + public function __construct(array $data) + { + foreach ($data as $path => $object) { + if ($object === null) { + $this->_webHooks[$path] = null; + } elseif (is_array($object)) { + $this->_webHooks[$path] = new PathItem($object); + } elseif ($object instanceof PathItem) { + $this->_webHooks[$path] = $object; + } else { + $givenType = gettype($object); + if ($givenType === 'object') { + $givenType = get_class($object); + } + throw new TypeErrorException(sprintf('Path MUST be either array or PathItem object, "%s" given', $givenType)); + } + } + } + + /** + * @return mixed returns the serializable data of this object for converting it + * to JSON or YAML. + */ + public function getSerializableData() + { + $data = []; + foreach ($this->_webHooks as $path => $pathItem) { + $data[$path] = ($pathItem === null) ? null : $pathItem->getSerializableData(); + } + return (object) $data; + } + + /** + * @param string $name path name + * @return bool + */ + public function hasWebHook(string $name): bool + { + return isset($this->_webHooks[$name]); + } + + /** + * @param string $name path name + * @return PathItem + */ + public function getWebHook(string $name): ?PathItem + { + return $this->_webHooks[$name] ?? null; + } + + /** + * @param string $name path name + * @param PathItem $pathItem the path item to add + */ + public function addWebHook(string $name, PathItem $pathItem): void + { + $this->_webHooks[$name] = $pathItem; + } + + /** + * @param string $name path name + */ + public function removeWebHook(string $name): void + { + unset($this->_webHooks[$name]); + } + + /** + * @return PathItem[] + */ + public function getWebHooks(): array + { + return $this->_webHooks; + } + + /** + * Validate object data according to OpenAPI spec. + * @return bool whether the loaded data is valid according to OpenAPI spec + * @see getErrors() + */ + public function validate(): bool + { + $valid = true; + $this->_errors = []; + foreach ($this->_webHooks as $key => $path) { + if ($path === null) { + continue; + } + if (!$path->validate()) { + $valid = false; + } + } + return $valid && empty($this->_errors); + } + + /** + * @return string[] list of validation errors according to OpenAPI spec. + * @see validate() + */ + public function getErrors(): array + { + if (($pos = $this->getDocumentPosition()) !== null) { + $errors = [ + array_map(function ($e) use ($pos) { + return "[{$pos}] $e"; + }, $this->_errors) + ]; + } else { + $errors = [$this->_errors]; + } + + foreach ($this->_webHooks as $path) { + if ($path === null) { + continue; + } + $errors[] = $path->getErrors(); + } + return array_merge(...$errors); + } + + /** + * Whether a offset exists + * @link http://php.net/manual/en/arrayaccess.offsetexists.php + * @param mixed $offset An offset to check for. + * @return boolean true on success or false on failure. + * The return value will be casted to boolean if non-boolean was returned. + */ + public function offsetExists($offset) + { + return $this->hasWebHook($offset); + } + + /** + * Offset to retrieve + * @link http://php.net/manual/en/arrayaccess.offsetget.php + * @param mixed $offset The offset to retrieve. + * @return PathItem Can return all value types. + */ + public function offsetGet($offset) + { + return $this->getWebHook($offset); + } + + /** + * Offset to set + * @link http://php.net/manual/en/arrayaccess.offsetset.php + * @param mixed $offset The offset to assign the value to. + * @param mixed $value The value to set. + */ + public function offsetSet($offset, $value) + { + $this->addWebHook($offset, $value); + } + + /** + * Offset to unset + * @link http://php.net/manual/en/arrayaccess.offsetunset.php + * @param mixed $offset The offset to unset. + */ + public function offsetUnset($offset) + { + $this->removeWebHook($offset); + } + + /** + * Count elements of an object + * @link http://php.net/manual/en/countable.count.php + * @return int The custom count as an integer. + * The return value is cast to an integer. + */ + public function count() + { + return count($this->_webHooks); + } + + /** + * Retrieve an external iterator + * @link http://php.net/manual/en/iteratoraggregate.getiterator.php + * @return Traversable An instance of an object implementing Iterator or Traversable + */ + public function getIterator() + { + return new ArrayIterator($this->_webHooks); + } + + /** + * Resolves all Reference Objects in this object and replaces them with their resolution. + * @throws UnresolvableReferenceException + */ + public function resolveReferences(ReferenceContext $context = null) + { + foreach ($this->_webHooks as $key => $path) { + if ($path === null) { + continue; + } + $path->resolveReferences($context); + } + } + + /** + * Set context for all Reference Objects in this object. + */ + public function setReferenceContext(ReferenceContext $context) + { + foreach ($this->_webHooks as $key => $path) { + if ($path === null) { + continue; + } + $path->setReferenceContext($context); + } + } + + /** + * Provide context information to the object. + * + * Context information contains a reference to the base object where it is contained in + * as well as a JSON pointer to its position. + * @param SpecObjectInterface $baseDocument + * @param JsonPointer $jsonPointer + */ + public function setDocumentContext(SpecObjectInterface $baseDocument, JsonPointer $jsonPointer) + { + $this->_baseDocument = $baseDocument; + $this->_jsonPointer = $jsonPointer; + + foreach ($this->_webHooks as $key => $path) { + if ($path instanceof DocumentContextInterface) { + $path->setDocumentContext($baseDocument, $jsonPointer->append($key)); + } + } + } + + /** + * @return SpecObjectInterface|null returns the base document where this object is located in. + * Returns `null` if no context information was provided by [[setDocumentContext]]. + */ + public function getBaseDocument(): ?SpecObjectInterface + { + return $this->_baseDocument; + } + + /** + * @return JsonPointer|null returns a JSON pointer describing the position of this object in the base document. + * Returns `null` if no context information was provided by [[setDocumentContext]]. + */ + public function getDocumentPosition(): ?JsonPointer + { + return $this->_jsonPointer; + } +} diff --git a/external_files/thephpleague/SchemaValidator.php b/external_files/thephpleague/SchemaValidator.php new file mode 100644 index 0000000..4daae60 --- /dev/null +++ b/external_files/thephpleague/SchemaValidator.php @@ -0,0 +1,176 @@ +assert($validationStrategy); + + $this->validationStrategy = $validationStrategy; + } + + /** {@inheritdoc} */ + public function validate($data, CebeSchema $schema, ?BreadCrumb $breadCrumb = null): void + { + $breadCrumb = $breadCrumb ?? new BreadCrumb(); + + try { + // These keywords are not part of the JSON Schema at all (new to OAS) + (new Nullable($schema))->validate($data, $schema->nullable); + + // We don't want to validate any more if the value is a valid Null + if ($data === null) { + return; + } + + // The following properties are taken from the JSON Schema definition but their definitions were adjusted to the OpenAPI Specification. + if (isset($schema->type)) { + if (is_string($schema->type)) { + (new Type($schema))->validate($data, $schema->type, $schema->format); + } else if (is_array($schema->type)) { + foreach ($schema->type as $schemaType) { + (new Type($schema))->validate($data, $schemaType, $schema->format); + break; + } + } + } + + // This keywords come directly from JSON Schema Validation, they are the same as in JSON schema + // https://tools.ietf.org/html/draft-wright-json-schema-validation-00#section-5 + if (isset($schema->multipleOf)) { + (new MultipleOf($schema))->validate($data, $schema->multipleOf); + } + + if (isset($schema->maximum)) { + $exclusiveMaximum = (bool) ($schema->exclusiveMaximum ?? false); + (new Maximum($schema))->validate($data, $schema->maximum, $exclusiveMaximum); + } + + if (isset($schema->minimum)) { + $exclusiveMinimum = (bool) ($schema->exclusiveMinimum ?? false); + (new Minimum($schema))->validate($data, $schema->minimum, $exclusiveMinimum); + } + + if (isset($schema->maxLength)) { + (new MaxLength($schema))->validate($data, $schema->maxLength); + } + + if (isset($schema->minLength)) { + (new MinLength($schema))->validate($data, $schema->minLength); + } + + if (isset($schema->pattern)) { + (new Pattern($schema))->validate($data, $schema->pattern); + } + + if (isset($schema->maxItems)) { + (new MaxItems($schema))->validate($data, $schema->maxItems); + } + + if (isset($schema->minItems)) { + (new MinItems($schema))->validate($data, $schema->minItems); + } + + if (isset($schema->uniqueItems)) { + (new UniqueItems($schema))->validate($data, $schema->uniqueItems); + } + + if (isset($schema->maxProperties)) { + (new MaxProperties($schema))->validate($data, $schema->maxProperties); + } + + if (isset($schema->minProperties)) { + (new MinProperties($schema))->validate($data, $schema->minProperties); + } + + if (isset($schema->required)) { + (new Required($schema, $this->validationStrategy, $breadCrumb))->validate($data, $schema->required); + } + + if (isset($schema->enum)) { + (new Enum($schema))->validate($data, $schema->enum); + } + + if (isset($schema->items)) { + (new Items($schema, $this->validationStrategy, $breadCrumb))->validate($data, $schema->items); + } + + if ( + $schema->type === CebeType::OBJECT + || (isset($schema->properties) && is_array($data) && ArrayHelper::isAssoc($data)) + ) { + $additionalProperties = $schema->additionalProperties ?? null; // defaults to true + if ((isset($schema->properties) && count($schema->properties)) || $additionalProperties) { + (new Properties($schema, $this->validationStrategy, $breadCrumb))->validate( + $data, + $schema->properties, + $additionalProperties + ); + } + } + + if (isset($schema->allOf) && count($schema->allOf)) { + (new AllOf($schema, $this->validationStrategy, $breadCrumb))->validate($data, $schema->allOf); + } + + if (isset($schema->oneOf) && count($schema->oneOf)) { + (new OneOf($schema, $this->validationStrategy, $breadCrumb))->validate($data, $schema->oneOf); + } + + if (isset($schema->anyOf) && count($schema->anyOf)) { + (new AnyOf($schema, $this->validationStrategy, $breadCrumb))->validate($data, $schema->anyOf); + } + + if (isset($schema->not)) { + (new Not($schema, $this->validationStrategy, $breadCrumb))->validate($data, $schema->not); + } + // ✓ ok, all checks are done + } catch (SchemaMismatch $e) { + $e->hydrateDataBreadCrumb($breadCrumb); + + throw $e; + } + } +} diff --git a/external_files/thephpleague/Type.php b/external_files/thephpleague/Type.php new file mode 100644 index 0000000..3cc34e7 --- /dev/null +++ b/external_files/thephpleague/Type.php @@ -0,0 +1,111 @@ +