diff --git a/composer.json b/composer.json index 73b3b2d..24c5091 100644 --- a/composer.json +++ b/composer.json @@ -39,5 +39,8 @@ "NineteenEightyFour\\NinteenEightyWoo\\": "src/" } }, - "minimum-stability": "dev" + "minimum-stability": "dev", + "require": { + "opis/json-schema": "^2.0@dev" + } } diff --git a/composer.lock b/composer.lock index f8028cb..33b5ffd 100644 --- a/composer.lock +++ b/composer.lock @@ -4,8 +4,202 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "797d6844b7e8efa35cf10f54d7277321", - "packages": [], + "content-hash": "d830ae7ed092c8c7a2756ace1d118cd3", + "packages": [ + { + "name": "opis/json-schema", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/opis/json-schema.git", + "reference": "c48df6d7089a45f01e1c82432348f2d5976f9bfb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opis/json-schema/zipball/c48df6d7089a45f01e1c82432348f2d5976f9bfb", + "reference": "c48df6d7089a45f01e1c82432348f2d5976f9bfb", + "shasum": "" + }, + "require": { + "ext-json": "*", + "opis/string": "^2.0", + "opis/uri": "^1.0", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "ext-bcmath": "*", + "ext-intl": "*", + "phpunit/phpunit": "^9.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opis\\JsonSchema\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + }, + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + } + ], + "description": "Json Schema Validator for PHP", + "homepage": "https://opis.io/json-schema", + "keywords": [ + "json", + "json-schema", + "schema", + "validation", + "validator" + ], + "support": { + "issues": "https://github.com/opis/json-schema/issues", + "source": "https://github.com/opis/json-schema/tree/2.3.0" + }, + "time": "2022-01-08T20:38:03+00:00" + }, + { + "name": "opis/string", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/opis/string.git", + "reference": "9ebf1a1f873f502f6859d11210b25a4bf5d141e7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opis/string/zipball/9ebf1a1f873f502f6859d11210b25a4bf5d141e7", + "reference": "9ebf1a1f873f502f6859d11210b25a4bf5d141e7", + "shasum": "" + }, + "require": { + "ext-iconv": "*", + "ext-json": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9.0" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opis\\String\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + }, + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + } + ], + "description": "Multibyte strings as objects", + "homepage": "https://opis.io/string", + "keywords": [ + "multi-byte", + "opis", + "string", + "string manipulation", + "utf-8" + ], + "support": { + "issues": "https://github.com/opis/string/issues", + "source": "https://github.com/opis/string/tree/2.0.1" + }, + "time": "2022-01-14T15:42:23+00:00" + }, + { + "name": "opis/uri", + "version": "dev-master", + "source": { + "type": "git", + "url": "https://github.com/opis/uri.git", + "reference": "0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/opis/uri/zipball/0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a", + "reference": "0f3ca49ab1a5e4a6681c286e0b2cc081b93a7d5a", + "shasum": "" + }, + "require": { + "opis/string": "^2.0", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpunit/phpunit": "^9" + }, + "default-branch": true, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Opis\\Uri\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "Apache-2.0" + ], + "authors": [ + { + "name": "Marius Sarca", + "email": "marius.sarca@gmail.com" + }, + { + "name": "Sorin Sarca", + "email": "sarca_sorin@hotmail.com" + } + ], + "description": "Build, parse and validate URIs and URI-templates", + "homepage": "https://opis.io", + "keywords": [ + "URI Template", + "parse url", + "punycode", + "uri", + "uri components", + "url", + "validate uri" + ], + "support": { + "issues": "https://github.com/opis/uri/issues", + "source": "https://github.com/opis/uri/tree/1.1.0" + }, + "time": "2021-05-22T15:57:08+00:00" + } + ], "packages-dev": [ { "name": "aldavigdis/wp-tests-strapon", diff --git a/src/Rest/Settings.php b/src/Rest/Settings.php index 7e21fbe..90cb1e9 100644 --- a/src/Rest/Settings.php +++ b/src/Rest/Settings.php @@ -7,6 +7,7 @@ use WP_Error; use WP_REST_Request; use WP_REST_Response; +use Opis\JsonSchema\Validator; /** * The REST API Settings endpoint class @@ -14,6 +15,29 @@ * Handles the `NinteenEightyWoo/v1/settings/` REST endpoint. */ class Settings { + const JSON_SCHEMA = <<<'JSON' + { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "api_key": { "type": "string" }, + "payment_methods": { + "type": "array", + "items": { + "type": "object", + "properties": { + "woo_id": { "type": "string" }, + "dk_id": { "type": "number" }, + "dk_name": { "type": "string" } + }, + "required": ["woo_id", "dk_id", "dk_name" ] + } + } + }, + "required": ["api_key", "payment_methods" ] + } + JSON; + /** * The Constructor for the Settings REST endpoint * @@ -52,10 +76,10 @@ public static function rest_api_callback( $rest_body = $request->get_body(); $rest_json = json_decode( $rest_body ); - if ( - false === is_object( $rest_json ) || - ( false === self::validate_post_schema( $rest_json ) ) - ) { + $validator = new Validator(); + $validation = $validator->validate( $rest_json, self::JSON_SCHEMA ); + + if ( true === $validation->hasError() ) { return new WP_Error( 'bad_request', 'Bad Request', diff --git a/tests/RestSettingsTest.php b/tests/RestSettingsTest.php index 5d081e3..53dd869 100644 --- a/tests/RestSettingsTest.php +++ b/tests/RestSettingsTest.php @@ -15,6 +15,17 @@ #[TestDox( 'The Rest Settings JSON API endpoint class' )] final class RestSettingstest extends TestCase { + const VALID_POST_BODY = [ + 'api_key' => 'MwmTtCTfhVQvlEOKCqFxfRAuPYHgy17E', + 'payment_methods' => [ + [ + 'woo_id' => 'bacs', + 'dk_id' => 10, + 'dk_name' => 'Direct bank transfer', + ], + ], + ]; + /** * The WordPress administrator user * @@ -43,21 +54,10 @@ final class RestSettingstest extends TestCase { public array $valid_post_body; public function setUp(): void { - $settings = new Settings(); + new Settings(); do_action( 'rest_api_init' ); - $this->valid_post_body = [ - 'api_key' => 'MwmTtCTfhVQvlEOKCqFxfRAuPYHgy17E', - 'payment_methods' => [ - [ - 'woo_id' => 'bacs', - 'dk_id' => 10, - 'dk_name' => 'Direct bank transfer', - ], - ], - ]; - $user_factory = new WP_UnitTest_Factory_For_User(); $this->admin_user = $user_factory->create_and_get(); $this->admin_user->add_role( 'administrator' ); @@ -72,7 +72,10 @@ public function tearDown(): void { #[TestDox( 'creates the NinteenEightyWoo/v1 namespace in the WP REST API' )] public function testNamespaceExsists(): void { - $request = new WP_REST_Request( 'GET', '/NinteenEightyWoo/v1' ); + $request = new WP_REST_Request( + 'GET', + '/NinteenEightyWoo/v1' + ); $response = rest_do_request( $request ); assertEquals( 200, $response->status ); @@ -81,7 +84,10 @@ public function testNamespaceExsists(): void { #[TestDox( 'protects the settings endpoint from outside requests' )] public function tesEndpointIsSecureFromOutsiders(): void { // We're assuming an external request here, so we're not using a nonce value. - $request = new WP_REST_Request( 'POST', '/NinteenEightyWoo/v1/settings' ); + $request = new WP_REST_Request( + 'POST', + '/NinteenEightyWoo/v1/settings' + ); $response = rest_do_request( $request ); assertEquals( 401, $response->status ); @@ -92,10 +98,13 @@ public function testEndpointProcessesValidRequest(): void { // Start by setting the current user to our admin user and creating a nonce // for that user for the REST API request. wp_set_current_user( $this->admin_user_id ); - $request = new WP_REST_Request( 'POST', '/NinteenEightyWoo/v1/settings' ); + $request = new WP_REST_Request( + 'POST', + '/NinteenEightyWoo/v1/settings' + ); $request->add_header( 'X-WP-Nonce', wp_create_nonce( 'wp_rest' ) ); - $request->set_body( wp_json_encode( $this->valid_post_body ) ); + $request->set_body( wp_json_encode( self::VALID_POST_BODY ) ); $response = rest_do_request( $request ); @@ -110,26 +119,41 @@ public function testEndpointRejectsInvalidTypeRequest(): void { '/NinteenEightyWoo/v1/settings' ); - $string_request->add_header( 'X-WP-Nonce', wp_create_nonce( 'wp_rest' ) ); + $string_request->add_header( + 'X-WP-Nonce', + wp_create_nonce( 'wp_rest' ) + ); $string_request->set_body( wp_json_encode( 'This string is invalid' ) ); $string_response = rest_do_request( $string_request ); assertEquals( 400, $string_response->status ); - $object_request = new WP_REST_Request( 'POST', '/NinteenEightyWoo/v1/settings' ); - $object_request->add_header( 'X-WP-Nonce', wp_create_nonce( 'wp_rest' ) ); + $object_request = new WP_REST_Request( + 'POST', + '/NinteenEightyWoo/v1/settings' + ); + $object_request->add_header( + 'X-WP-Nonce', + wp_create_nonce( 'wp_rest' ) + ); $object_request->set_body( wp_json_encode( - [ 'foo' => 'This is an object, but has none of the required keys.' ] + [ 'foo' => 'This is an object with no valid keys.' ] ) ); $object_response = rest_do_request( $object_request ); assertEquals( 400, $object_response->status ); - $api_key_unset_request = new WP_REST_Request( 'POST', '/NinteenEightyWoo/v1/settings' ); - $api_key_unset_request->add_header( 'X-WP-Nonce', wp_create_nonce( 'wp_rest' ) ); + $api_key_unset_request = new WP_REST_Request( + 'POST', + '/NinteenEightyWoo/v1/settings' + ); + $api_key_unset_request->add_header( + 'X-WP-Nonce', + wp_create_nonce( 'wp_rest' ) + ); $api_key_unset_request->set_body( wp_json_encode( - array( 'payment_methods' => $this->valid_post_body['payment_methods'] ) + [ 'payment_methods' => self::VALID_POST_BODY['payment_methods'] ] ) ); $api_key_unset_response = rest_do_request( $api_key_unset_request );