├── .gitignore ├── LICENSE ├── README.md ├── bin └── run-tests.sh ├── composer.json ├── composer.lock ├── phpunit.xml ├── src ├── FormatConstraint.php ├── LazyRetriever.php ├── Middleware.php ├── TypeConstraint.php └── UndefinedConstraint.php └── tests ├── TestCase.php ├── TestMiddleware.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Iron Bound Designs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # WP REST API Schema Validator 2 | Validate WP REST API requests using a complete JSON Schema validator. 3 | 4 | WordPress ships with a validator, `rest_validate_request_arg()`, that supports a limited subset of the JSON Schema spec. This library allows the full JSON Schema spec to be used when writing endpoint schemas with minimal configuration. 5 | 6 | This library relies upon the [justinrainbow/json-schema](https://github.com/justinrainbow/json-schema) package to do the actual schema validation. This simply bridges the gap between the two. 7 | 8 | ## Requirements 9 | - PHP 5.3+ 10 | - WordPress 4.5+ 11 | 12 | ## Installation 13 | `composer require ironbound/wp-rest-api-schema-validator` 14 | 15 | ## Usage 16 | Initialize a `Middleware` instance with your REST route `namespace` and an array of localized strings. This middleware should be initialized before the `rest_api_init` hook is fired. For example, `plugins_loaded`. 17 | 18 | Additionally, schemas must be created with a `title` attribute on the top level. This title should be unique within the versioned namespace. 19 | 20 | ```php 21 | $middleware = new \IronBound\WP_REST_API\SchemaValidator\Middleware( 'namespace/v1', [ 22 | 'methodParamDescription' => __( 'HTTP method to get the schema for. If not provided, will use the base schema.', 'text-domain' ), 23 | 'schemaNotFound' => __( 'Schema not found.', 'text-domain' ), 24 | ] ); 25 | $middleware->initialize(); 26 | ``` 27 | 28 | That's it! 29 | 30 | ## Advanced 31 | 32 | ### GET and DELETE Requests 33 | 34 | Query parameters passed with GET or DELETE requests are validated against the `args` option that is passed when registering the route. 35 | 36 | ### Technical Details 37 | 38 | On `rest_api_init#100`, the middleware will iterate over the registered routes in the provided namespace. The default WordPress core validation and sanitization functions will be disabled. 39 | 40 | Schema validation will be performed on the `rest_dispatch_request#10` hook. 41 | 42 | `WP_Error` objects will be returned that match the format in `WP_REST_Request`. Mainly, an error code of `rest_missing_callback_param` or `rest_invalid_param`, a `400` response status code, and detailed error information in `data.params`. 43 | 44 | For missing parameters, `data.params` will contain a list of the missing parameter names. For invalid parameters, 45 | a map of parameter names to a specific validation error message. 46 | 47 | ### Procedural Validation 48 | In the vast majority of cases, validation should be configured using JSON Schema definitions. However, this is not always the case. For example, verifying that a username is not taken requires making calls to the database that would be impossible to replicate in the schema definition. In these cases, a `validate_callback` can still be provided and will be executed before JSON Schema validation takes place. 49 | 50 | ```php 51 | return [ 52 | '$schema' => 'http://json-schema.org/schema#', 53 | 'title' => 'users', 54 | 'type' => 'object', 55 | 'properties' => [ 56 | 'username' => [ 57 | 'description' => __( 'Login name for the user.', 'text-domain' ), 58 | 'type' => 'string', 59 | 'context' => [ 'view', 'edit', 'embed' ], 60 | 'arg_options' => [ 61 | 'validate_callback' => function( $value ) { 62 | return ! username_exists( $value ); 63 | }, 64 | ], 65 | ], 66 | ], 67 | ]; 68 | ``` 69 | 70 | ### Variable Schemas 71 | In most cases, the schema document should be the same for all HTTP methods on a given endpoint. In the rare case that a separate schema document is provided, a `schema` option can be provided to the route args for that HTTP method. The `title` for the separate schema document MUST be the same as the base schema. 72 | 73 | ```php 74 | register_rest_route( 'namespace/v1', 'route', [ 75 | [ 76 | 'methods' => 'GET', 77 | 'callback' => [ $this, 'get_item' ], 78 | 'args' => $this->get_endpoint_args_for_item_schema( 'GET' ), 79 | ], 80 | [ 81 | 'methods' => 'POST', 82 | 'callback' => array( $this, 'create_item' ), 83 | // See WP_REST_Controller::get_endpoint_args_for_item_schema() for reference. 84 | 'args' => $this->get_endpoint_args_for_post_schema(), 85 | 'schema' => [ $this, 'get_public_item_post_schema' ], 86 | ], 87 | [ 88 | 'methods' => 'PUT', 89 | 'callback' => [ $this, 'update_item' ], 90 | 'args' => $this->get_endpoint_args_for_item_schema( 'PUT' ), 91 | ], 92 | 'schema' => [ $this, 'get_public_item_schema' ], 93 | ] ); 94 | ``` 95 | 96 | ### Reusing Schemas 97 | JSON Schema provides a mechanism to utilize a referenced Schema document for validation. This package allows you to accomplish this by using the `Middleware::get_url_for_schema( $title )` method. 98 | 99 | For example, this Schema will validate the `card` property according to the Schema document with the title `card`. 100 | ```php 101 | [ 102 | '$schema' => 'http://json-schema.org/schema#', 103 | 'title' => 'transaction', 104 | 'type' => 'object', 105 | 'properties' => [ 106 | 'card' => [ 107 | '$ref' => $middleware->get_url_for_schema( 'card' ) 108 | ], 109 | ], 110 | ]; 111 | ``` 112 | 113 | But what if there is no `/cards` route? Or a more general schema is required? In this case, a shared schema can be used. 114 | ```php 115 | $middleware->add_shared_schema( [ 116 | '$schema' => 'http://json-schema.org/schema#', 117 | 'title' => 'card', 118 | 'type' => 'object', 119 | 'properties' => [ 120 | 'card_number' => [ 121 | 'type' => 'string', 122 | 'pattern' => '^[0-9]{11,19}$', 123 | ], 124 | 'exp_year' => [ 'type' => 'integer' ], 125 | 'exp_month' => [ 126 | 'type' => 'integer', 127 | 'minimum' => 1, 128 | 'maximum' => 12, 129 | ], 130 | ], 131 | ] ); 132 | ``` 133 | 134 | ### Schema Routes 135 | 136 | After all routes have been registered, the middleware will register its own route. 137 | 138 | ``` 139 | namespace/v1/schemas/(?P[\S+]) 140 | ``` 141 | 142 | This route returns the plain schema document for the given title. To retrieve a schema for a given HTTP method, pass the desired upper-cased HTTP method to the `method` query param. 143 | 144 | ```HTTP 145 | GET https://example.org/wp-json/namespace/v1/schemas/transaction?method=POST 146 | ``` -------------------------------------------------------------------------------- /bin/run-tests.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | export WP_DEVELOP_DIR="$1" 4 | 5 | shift 6 | 7 | phpunit "$@" -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ironbound/wp-rest-api-schema-validator", 3 | "description": "Validate WP REST API requests using a complete JSON Schema validator.", 4 | "type": "library", 5 | "require": { 6 | "php": ">=5.3.29|>=7.0.3", 7 | "justinrainbow/json-schema": "^5.1" 8 | }, 9 | "require-dev": { 10 | "phpunit/phpunit": "^4.8|^5.7" 11 | }, 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Timothy Jacobs", 16 | "email": "timothy@ironbounddesigns.com" 17 | } 18 | ], 19 | "minimum-stability": "stable", 20 | "autoload": { 21 | "psr-4": { 22 | "IronBound\\WP_REST_API\\SchemaValidator\\": "src/", 23 | "IronBound\\WP_REST_API\\SchemaValidator\\Tests\\": "tests/" 24 | } 25 | }, 26 | "config": { 27 | "platform": { 28 | "php": "5.3.29" 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /composer.lock: -------------------------------------------------------------------------------- 1 | { 2 | "_readme": [ 3 | "This file locks the dependencies of your project to a known state", 4 | "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file", 5 | "This file is @generated automatically" 6 | ], 7 | "content-hash": "a518726e9a9bc1df9d0105303a74e2a4", 8 | "packages": [ 9 | { 10 | "name": "justinrainbow/json-schema", 11 | "version": "5.2.0", 12 | "source": { 13 | "type": "git", 14 | "url": "https://github.com/justinrainbow/json-schema.git", 15 | "reference": "e3c9bccdc38bbd09bcac0131c00f3be58368b416" 16 | }, 17 | "dist": { 18 | "type": "zip", 19 | "url": "https://api.github.com/repos/justinrainbow/json-schema/zipball/e3c9bccdc38bbd09bcac0131c00f3be58368b416", 20 | "reference": "e3c9bccdc38bbd09bcac0131c00f3be58368b416", 21 | "shasum": "" 22 | }, 23 | "require": { 24 | "php": ">=5.3.3" 25 | }, 26 | "require-dev": { 27 | "friendsofphp/php-cs-fixer": "^2.1", 28 | "json-schema/json-schema-test-suite": "1.2.0", 29 | "phpdocumentor/phpdocumentor": "~2", 30 | "phpunit/phpunit": "^4.8.22" 31 | }, 32 | "bin": [ 33 | "bin/validate-json" 34 | ], 35 | "type": "library", 36 | "extra": { 37 | "branch-alias": { 38 | "dev-master": "5.0.x-dev" 39 | } 40 | }, 41 | "autoload": { 42 | "psr-4": { 43 | "JsonSchema\\": "src/JsonSchema/" 44 | } 45 | }, 46 | "notification-url": "https://packagist.org/downloads/", 47 | "license": [ 48 | "MIT" 49 | ], 50 | "authors": [ 51 | { 52 | "name": "Bruno Prieto Reis", 53 | "email": "bruno.p.reis@gmail.com" 54 | }, 55 | { 56 | "name": "Justin Rainbow", 57 | "email": "justin.rainbow@gmail.com" 58 | }, 59 | { 60 | "name": "Igor Wiedler", 61 | "email": "igor@wiedler.ch" 62 | }, 63 | { 64 | "name": "Robert Schönthal", 65 | "email": "seroscho@googlemail.com" 66 | } 67 | ], 68 | "description": "A library to validate a json schema.", 69 | "homepage": "https://github.com/justinrainbow/json-schema", 70 | "keywords": [ 71 | "json", 72 | "schema" 73 | ], 74 | "time": "2017-03-22T22:43:35+00:00" 75 | } 76 | ], 77 | "packages-dev": [ 78 | { 79 | "name": "doctrine/instantiator", 80 | "version": "1.0.5", 81 | "source": { 82 | "type": "git", 83 | "url": "https://github.com/doctrine/instantiator.git", 84 | "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d" 85 | }, 86 | "dist": { 87 | "type": "zip", 88 | "url": "https://api.github.com/repos/doctrine/instantiator/zipball/8e884e78f9f0eb1329e445619e04456e64d8051d", 89 | "reference": "8e884e78f9f0eb1329e445619e04456e64d8051d", 90 | "shasum": "" 91 | }, 92 | "require": { 93 | "php": ">=5.3,<8.0-DEV" 94 | }, 95 | "require-dev": { 96 | "athletic/athletic": "~0.1.8", 97 | "ext-pdo": "*", 98 | "ext-phar": "*", 99 | "phpunit/phpunit": "~4.0", 100 | "squizlabs/php_codesniffer": "~2.0" 101 | }, 102 | "type": "library", 103 | "extra": { 104 | "branch-alias": { 105 | "dev-master": "1.0.x-dev" 106 | } 107 | }, 108 | "autoload": { 109 | "psr-4": { 110 | "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" 111 | } 112 | }, 113 | "notification-url": "https://packagist.org/downloads/", 114 | "license": [ 115 | "MIT" 116 | ], 117 | "authors": [ 118 | { 119 | "name": "Marco Pivetta", 120 | "email": "ocramius@gmail.com", 121 | "homepage": "http://ocramius.github.com/" 122 | } 123 | ], 124 | "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", 125 | "homepage": "https://github.com/doctrine/instantiator", 126 | "keywords": [ 127 | "constructor", 128 | "instantiate" 129 | ], 130 | "time": "2015-06-14T21:17:01+00:00" 131 | }, 132 | { 133 | "name": "myclabs/deep-copy", 134 | "version": "1.6.1", 135 | "source": { 136 | "type": "git", 137 | "url": "https://github.com/myclabs/DeepCopy.git", 138 | "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102" 139 | }, 140 | "dist": { 141 | "type": "zip", 142 | "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/8e6e04167378abf1ddb4d3522d8755c5fd90d102", 143 | "reference": "8e6e04167378abf1ddb4d3522d8755c5fd90d102", 144 | "shasum": "" 145 | }, 146 | "require": { 147 | "php": ">=5.4.0" 148 | }, 149 | "require-dev": { 150 | "doctrine/collections": "1.*", 151 | "phpunit/phpunit": "~4.1" 152 | }, 153 | "type": "library", 154 | "autoload": { 155 | "psr-4": { 156 | "DeepCopy\\": "src/DeepCopy/" 157 | } 158 | }, 159 | "notification-url": "https://packagist.org/downloads/", 160 | "license": [ 161 | "MIT" 162 | ], 163 | "description": "Create deep copies (clones) of your objects", 164 | "homepage": "https://github.com/myclabs/DeepCopy", 165 | "keywords": [ 166 | "clone", 167 | "copy", 168 | "duplicate", 169 | "object", 170 | "object graph" 171 | ], 172 | "time": "2017-04-12T18:52:22+00:00" 173 | }, 174 | { 175 | "name": "phpdocumentor/reflection-common", 176 | "version": "1.0", 177 | "source": { 178 | "type": "git", 179 | "url": "https://github.com/phpDocumentor/ReflectionCommon.git", 180 | "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c" 181 | }, 182 | "dist": { 183 | "type": "zip", 184 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/144c307535e82c8fdcaacbcfc1d6d8eeb896687c", 185 | "reference": "144c307535e82c8fdcaacbcfc1d6d8eeb896687c", 186 | "shasum": "" 187 | }, 188 | "require": { 189 | "php": ">=5.5" 190 | }, 191 | "require-dev": { 192 | "phpunit/phpunit": "^4.6" 193 | }, 194 | "type": "library", 195 | "extra": { 196 | "branch-alias": { 197 | "dev-master": "1.0.x-dev" 198 | } 199 | }, 200 | "autoload": { 201 | "psr-4": { 202 | "phpDocumentor\\Reflection\\": [ 203 | "src" 204 | ] 205 | } 206 | }, 207 | "notification-url": "https://packagist.org/downloads/", 208 | "license": [ 209 | "MIT" 210 | ], 211 | "authors": [ 212 | { 213 | "name": "Jaap van Otterdijk", 214 | "email": "opensource@ijaap.nl" 215 | } 216 | ], 217 | "description": "Common reflection classes used by phpdocumentor to reflect the code structure", 218 | "homepage": "http://www.phpdoc.org", 219 | "keywords": [ 220 | "FQSEN", 221 | "phpDocumentor", 222 | "phpdoc", 223 | "reflection", 224 | "static analysis" 225 | ], 226 | "time": "2015-12-27T11:43:31+00:00" 227 | }, 228 | { 229 | "name": "phpdocumentor/reflection-docblock", 230 | "version": "3.1.1", 231 | "source": { 232 | "type": "git", 233 | "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", 234 | "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e" 235 | }, 236 | "dist": { 237 | "type": "zip", 238 | "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/8331b5efe816ae05461b7ca1e721c01b46bafb3e", 239 | "reference": "8331b5efe816ae05461b7ca1e721c01b46bafb3e", 240 | "shasum": "" 241 | }, 242 | "require": { 243 | "php": ">=5.5", 244 | "phpdocumentor/reflection-common": "^1.0@dev", 245 | "phpdocumentor/type-resolver": "^0.2.0", 246 | "webmozart/assert": "^1.0" 247 | }, 248 | "require-dev": { 249 | "mockery/mockery": "^0.9.4", 250 | "phpunit/phpunit": "^4.4" 251 | }, 252 | "type": "library", 253 | "autoload": { 254 | "psr-4": { 255 | "phpDocumentor\\Reflection\\": [ 256 | "src/" 257 | ] 258 | } 259 | }, 260 | "notification-url": "https://packagist.org/downloads/", 261 | "license": [ 262 | "MIT" 263 | ], 264 | "authors": [ 265 | { 266 | "name": "Mike van Riel", 267 | "email": "me@mikevanriel.com" 268 | } 269 | ], 270 | "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", 271 | "time": "2016-09-30T07:12:33+00:00" 272 | }, 273 | { 274 | "name": "phpdocumentor/type-resolver", 275 | "version": "0.2.1", 276 | "source": { 277 | "type": "git", 278 | "url": "https://github.com/phpDocumentor/TypeResolver.git", 279 | "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb" 280 | }, 281 | "dist": { 282 | "type": "zip", 283 | "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb", 284 | "reference": "e224fb2ea2fba6d3ad6fdaef91cd09a172155ccb", 285 | "shasum": "" 286 | }, 287 | "require": { 288 | "php": ">=5.5", 289 | "phpdocumentor/reflection-common": "^1.0" 290 | }, 291 | "require-dev": { 292 | "mockery/mockery": "^0.9.4", 293 | "phpunit/phpunit": "^5.2||^4.8.24" 294 | }, 295 | "type": "library", 296 | "extra": { 297 | "branch-alias": { 298 | "dev-master": "1.0.x-dev" 299 | } 300 | }, 301 | "autoload": { 302 | "psr-4": { 303 | "phpDocumentor\\Reflection\\": [ 304 | "src/" 305 | ] 306 | } 307 | }, 308 | "notification-url": "https://packagist.org/downloads/", 309 | "license": [ 310 | "MIT" 311 | ], 312 | "authors": [ 313 | { 314 | "name": "Mike van Riel", 315 | "email": "me@mikevanriel.com" 316 | } 317 | ], 318 | "time": "2016-11-25T06:54:22+00:00" 319 | }, 320 | { 321 | "name": "phpspec/prophecy", 322 | "version": "v1.7.0", 323 | "source": { 324 | "type": "git", 325 | "url": "https://github.com/phpspec/prophecy.git", 326 | "reference": "93d39f1f7f9326d746203c7c056f300f7f126073" 327 | }, 328 | "dist": { 329 | "type": "zip", 330 | "url": "https://api.github.com/repos/phpspec/prophecy/zipball/93d39f1f7f9326d746203c7c056f300f7f126073", 331 | "reference": "93d39f1f7f9326d746203c7c056f300f7f126073", 332 | "shasum": "" 333 | }, 334 | "require": { 335 | "doctrine/instantiator": "^1.0.2", 336 | "php": "^5.3|^7.0", 337 | "phpdocumentor/reflection-docblock": "^2.0|^3.0.2", 338 | "sebastian/comparator": "^1.1|^2.0", 339 | "sebastian/recursion-context": "^1.0|^2.0|^3.0" 340 | }, 341 | "require-dev": { 342 | "phpspec/phpspec": "^2.5|^3.2", 343 | "phpunit/phpunit": "^4.8 || ^5.6.5" 344 | }, 345 | "type": "library", 346 | "extra": { 347 | "branch-alias": { 348 | "dev-master": "1.6.x-dev" 349 | } 350 | }, 351 | "autoload": { 352 | "psr-0": { 353 | "Prophecy\\": "src/" 354 | } 355 | }, 356 | "notification-url": "https://packagist.org/downloads/", 357 | "license": [ 358 | "MIT" 359 | ], 360 | "authors": [ 361 | { 362 | "name": "Konstantin Kudryashov", 363 | "email": "ever.zet@gmail.com", 364 | "homepage": "http://everzet.com" 365 | }, 366 | { 367 | "name": "Marcello Duarte", 368 | "email": "marcello.duarte@gmail.com" 369 | } 370 | ], 371 | "description": "Highly opinionated mocking framework for PHP 5.3+", 372 | "homepage": "https://github.com/phpspec/prophecy", 373 | "keywords": [ 374 | "Double", 375 | "Dummy", 376 | "fake", 377 | "mock", 378 | "spy", 379 | "stub" 380 | ], 381 | "time": "2017-03-02T20:05:34+00:00" 382 | }, 383 | { 384 | "name": "phpunit/php-code-coverage", 385 | "version": "4.0.8", 386 | "source": { 387 | "type": "git", 388 | "url": "https://github.com/sebastianbergmann/php-code-coverage.git", 389 | "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d" 390 | }, 391 | "dist": { 392 | "type": "zip", 393 | "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/ef7b2f56815df854e66ceaee8ebe9393ae36a40d", 394 | "reference": "ef7b2f56815df854e66ceaee8ebe9393ae36a40d", 395 | "shasum": "" 396 | }, 397 | "require": { 398 | "ext-dom": "*", 399 | "ext-xmlwriter": "*", 400 | "php": "^5.6 || ^7.0", 401 | "phpunit/php-file-iterator": "^1.3", 402 | "phpunit/php-text-template": "^1.2", 403 | "phpunit/php-token-stream": "^1.4.2 || ^2.0", 404 | "sebastian/code-unit-reverse-lookup": "^1.0", 405 | "sebastian/environment": "^1.3.2 || ^2.0", 406 | "sebastian/version": "^1.0 || ^2.0" 407 | }, 408 | "require-dev": { 409 | "ext-xdebug": "^2.1.4", 410 | "phpunit/phpunit": "^5.7" 411 | }, 412 | "suggest": { 413 | "ext-xdebug": "^2.5.1" 414 | }, 415 | "type": "library", 416 | "extra": { 417 | "branch-alias": { 418 | "dev-master": "4.0.x-dev" 419 | } 420 | }, 421 | "autoload": { 422 | "classmap": [ 423 | "src/" 424 | ] 425 | }, 426 | "notification-url": "https://packagist.org/downloads/", 427 | "license": [ 428 | "BSD-3-Clause" 429 | ], 430 | "authors": [ 431 | { 432 | "name": "Sebastian Bergmann", 433 | "email": "sb@sebastian-bergmann.de", 434 | "role": "lead" 435 | } 436 | ], 437 | "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", 438 | "homepage": "https://github.com/sebastianbergmann/php-code-coverage", 439 | "keywords": [ 440 | "coverage", 441 | "testing", 442 | "xunit" 443 | ], 444 | "time": "2017-04-02T07:44:40+00:00" 445 | }, 446 | { 447 | "name": "phpunit/php-file-iterator", 448 | "version": "1.4.2", 449 | "source": { 450 | "type": "git", 451 | "url": "https://github.com/sebastianbergmann/php-file-iterator.git", 452 | "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5" 453 | }, 454 | "dist": { 455 | "type": "zip", 456 | "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3cc8f69b3028d0f96a9078e6295d86e9bf019be5", 457 | "reference": "3cc8f69b3028d0f96a9078e6295d86e9bf019be5", 458 | "shasum": "" 459 | }, 460 | "require": { 461 | "php": ">=5.3.3" 462 | }, 463 | "type": "library", 464 | "extra": { 465 | "branch-alias": { 466 | "dev-master": "1.4.x-dev" 467 | } 468 | }, 469 | "autoload": { 470 | "classmap": [ 471 | "src/" 472 | ] 473 | }, 474 | "notification-url": "https://packagist.org/downloads/", 475 | "license": [ 476 | "BSD-3-Clause" 477 | ], 478 | "authors": [ 479 | { 480 | "name": "Sebastian Bergmann", 481 | "email": "sb@sebastian-bergmann.de", 482 | "role": "lead" 483 | } 484 | ], 485 | "description": "FilterIterator implementation that filters files based on a list of suffixes.", 486 | "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", 487 | "keywords": [ 488 | "filesystem", 489 | "iterator" 490 | ], 491 | "time": "2016-10-03T07:40:28+00:00" 492 | }, 493 | { 494 | "name": "phpunit/php-text-template", 495 | "version": "1.2.1", 496 | "source": { 497 | "type": "git", 498 | "url": "https://github.com/sebastianbergmann/php-text-template.git", 499 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686" 500 | }, 501 | "dist": { 502 | "type": "zip", 503 | "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 504 | "reference": "31f8b717e51d9a2afca6c9f046f5d69fc27c8686", 505 | "shasum": "" 506 | }, 507 | "require": { 508 | "php": ">=5.3.3" 509 | }, 510 | "type": "library", 511 | "autoload": { 512 | "classmap": [ 513 | "src/" 514 | ] 515 | }, 516 | "notification-url": "https://packagist.org/downloads/", 517 | "license": [ 518 | "BSD-3-Clause" 519 | ], 520 | "authors": [ 521 | { 522 | "name": "Sebastian Bergmann", 523 | "email": "sebastian@phpunit.de", 524 | "role": "lead" 525 | } 526 | ], 527 | "description": "Simple template engine.", 528 | "homepage": "https://github.com/sebastianbergmann/php-text-template/", 529 | "keywords": [ 530 | "template" 531 | ], 532 | "time": "2015-06-21T13:50:34+00:00" 533 | }, 534 | { 535 | "name": "phpunit/php-timer", 536 | "version": "1.0.9", 537 | "source": { 538 | "type": "git", 539 | "url": "https://github.com/sebastianbergmann/php-timer.git", 540 | "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f" 541 | }, 542 | "dist": { 543 | "type": "zip", 544 | "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", 545 | "reference": "3dcf38ca72b158baf0bc245e9184d3fdffa9c46f", 546 | "shasum": "" 547 | }, 548 | "require": { 549 | "php": "^5.3.3 || ^7.0" 550 | }, 551 | "require-dev": { 552 | "phpunit/phpunit": "^4.8.35 || ^5.7 || ^6.0" 553 | }, 554 | "type": "library", 555 | "extra": { 556 | "branch-alias": { 557 | "dev-master": "1.0-dev" 558 | } 559 | }, 560 | "autoload": { 561 | "classmap": [ 562 | "src/" 563 | ] 564 | }, 565 | "notification-url": "https://packagist.org/downloads/", 566 | "license": [ 567 | "BSD-3-Clause" 568 | ], 569 | "authors": [ 570 | { 571 | "name": "Sebastian Bergmann", 572 | "email": "sb@sebastian-bergmann.de", 573 | "role": "lead" 574 | } 575 | ], 576 | "description": "Utility class for timing", 577 | "homepage": "https://github.com/sebastianbergmann/php-timer/", 578 | "keywords": [ 579 | "timer" 580 | ], 581 | "time": "2017-02-26T11:10:40+00:00" 582 | }, 583 | { 584 | "name": "phpunit/php-token-stream", 585 | "version": "1.4.11", 586 | "source": { 587 | "type": "git", 588 | "url": "https://github.com/sebastianbergmann/php-token-stream.git", 589 | "reference": "e03f8f67534427a787e21a385a67ec3ca6978ea7" 590 | }, 591 | "dist": { 592 | "type": "zip", 593 | "url": "https://api.github.com/repos/sebastianbergmann/php-token-stream/zipball/e03f8f67534427a787e21a385a67ec3ca6978ea7", 594 | "reference": "e03f8f67534427a787e21a385a67ec3ca6978ea7", 595 | "shasum": "" 596 | }, 597 | "require": { 598 | "ext-tokenizer": "*", 599 | "php": ">=5.3.3" 600 | }, 601 | "require-dev": { 602 | "phpunit/phpunit": "~4.2" 603 | }, 604 | "type": "library", 605 | "extra": { 606 | "branch-alias": { 607 | "dev-master": "1.4-dev" 608 | } 609 | }, 610 | "autoload": { 611 | "classmap": [ 612 | "src/" 613 | ] 614 | }, 615 | "notification-url": "https://packagist.org/downloads/", 616 | "license": [ 617 | "BSD-3-Clause" 618 | ], 619 | "authors": [ 620 | { 621 | "name": "Sebastian Bergmann", 622 | "email": "sebastian@phpunit.de" 623 | } 624 | ], 625 | "description": "Wrapper around PHP's tokenizer extension.", 626 | "homepage": "https://github.com/sebastianbergmann/php-token-stream/", 627 | "keywords": [ 628 | "tokenizer" 629 | ], 630 | "time": "2017-02-27T10:12:30+00:00" 631 | }, 632 | { 633 | "name": "phpunit/phpunit", 634 | "version": "5.7.19", 635 | "source": { 636 | "type": "git", 637 | "url": "https://github.com/sebastianbergmann/phpunit.git", 638 | "reference": "69c4f49ff376af2692bad9cebd883d17ebaa98a1" 639 | }, 640 | "dist": { 641 | "type": "zip", 642 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/69c4f49ff376af2692bad9cebd883d17ebaa98a1", 643 | "reference": "69c4f49ff376af2692bad9cebd883d17ebaa98a1", 644 | "shasum": "" 645 | }, 646 | "require": { 647 | "ext-dom": "*", 648 | "ext-json": "*", 649 | "ext-libxml": "*", 650 | "ext-mbstring": "*", 651 | "ext-xml": "*", 652 | "myclabs/deep-copy": "~1.3", 653 | "php": "^5.6 || ^7.0", 654 | "phpspec/prophecy": "^1.6.2", 655 | "phpunit/php-code-coverage": "^4.0.4", 656 | "phpunit/php-file-iterator": "~1.4", 657 | "phpunit/php-text-template": "~1.2", 658 | "phpunit/php-timer": "^1.0.6", 659 | "phpunit/phpunit-mock-objects": "^3.2", 660 | "sebastian/comparator": "^1.2.4", 661 | "sebastian/diff": "~1.2", 662 | "sebastian/environment": "^1.3.4 || ^2.0", 663 | "sebastian/exporter": "~2.0", 664 | "sebastian/global-state": "^1.1", 665 | "sebastian/object-enumerator": "~2.0", 666 | "sebastian/resource-operations": "~1.0", 667 | "sebastian/version": "~1.0.3|~2.0", 668 | "symfony/yaml": "~2.1|~3.0" 669 | }, 670 | "conflict": { 671 | "phpdocumentor/reflection-docblock": "3.0.2" 672 | }, 673 | "require-dev": { 674 | "ext-pdo": "*" 675 | }, 676 | "suggest": { 677 | "ext-xdebug": "*", 678 | "phpunit/php-invoker": "~1.1" 679 | }, 680 | "bin": [ 681 | "phpunit" 682 | ], 683 | "type": "library", 684 | "extra": { 685 | "branch-alias": { 686 | "dev-master": "5.7.x-dev" 687 | } 688 | }, 689 | "autoload": { 690 | "classmap": [ 691 | "src/" 692 | ] 693 | }, 694 | "notification-url": "https://packagist.org/downloads/", 695 | "license": [ 696 | "BSD-3-Clause" 697 | ], 698 | "authors": [ 699 | { 700 | "name": "Sebastian Bergmann", 701 | "email": "sebastian@phpunit.de", 702 | "role": "lead" 703 | } 704 | ], 705 | "description": "The PHP Unit Testing framework.", 706 | "homepage": "https://phpunit.de/", 707 | "keywords": [ 708 | "phpunit", 709 | "testing", 710 | "xunit" 711 | ], 712 | "time": "2017-04-03T02:22:27+00:00" 713 | }, 714 | { 715 | "name": "phpunit/phpunit-mock-objects", 716 | "version": "3.4.3", 717 | "source": { 718 | "type": "git", 719 | "url": "https://github.com/sebastianbergmann/phpunit-mock-objects.git", 720 | "reference": "3ab72b65b39b491e0c011e2e09bb2206c2aa8e24" 721 | }, 722 | "dist": { 723 | "type": "zip", 724 | "url": "https://api.github.com/repos/sebastianbergmann/phpunit-mock-objects/zipball/3ab72b65b39b491e0c011e2e09bb2206c2aa8e24", 725 | "reference": "3ab72b65b39b491e0c011e2e09bb2206c2aa8e24", 726 | "shasum": "" 727 | }, 728 | "require": { 729 | "doctrine/instantiator": "^1.0.2", 730 | "php": "^5.6 || ^7.0", 731 | "phpunit/php-text-template": "^1.2", 732 | "sebastian/exporter": "^1.2 || ^2.0" 733 | }, 734 | "conflict": { 735 | "phpunit/phpunit": "<5.4.0" 736 | }, 737 | "require-dev": { 738 | "phpunit/phpunit": "^5.4" 739 | }, 740 | "suggest": { 741 | "ext-soap": "*" 742 | }, 743 | "type": "library", 744 | "extra": { 745 | "branch-alias": { 746 | "dev-master": "3.2.x-dev" 747 | } 748 | }, 749 | "autoload": { 750 | "classmap": [ 751 | "src/" 752 | ] 753 | }, 754 | "notification-url": "https://packagist.org/downloads/", 755 | "license": [ 756 | "BSD-3-Clause" 757 | ], 758 | "authors": [ 759 | { 760 | "name": "Sebastian Bergmann", 761 | "email": "sb@sebastian-bergmann.de", 762 | "role": "lead" 763 | } 764 | ], 765 | "description": "Mock Object library for PHPUnit", 766 | "homepage": "https://github.com/sebastianbergmann/phpunit-mock-objects/", 767 | "keywords": [ 768 | "mock", 769 | "xunit" 770 | ], 771 | "time": "2016-12-08T20:27:08+00:00" 772 | }, 773 | { 774 | "name": "sebastian/code-unit-reverse-lookup", 775 | "version": "1.0.1", 776 | "source": { 777 | "type": "git", 778 | "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", 779 | "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18" 780 | }, 781 | "dist": { 782 | "type": "zip", 783 | "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", 784 | "reference": "4419fcdb5eabb9caa61a27c7a1db532a6b55dd18", 785 | "shasum": "" 786 | }, 787 | "require": { 788 | "php": "^5.6 || ^7.0" 789 | }, 790 | "require-dev": { 791 | "phpunit/phpunit": "^5.7 || ^6.0" 792 | }, 793 | "type": "library", 794 | "extra": { 795 | "branch-alias": { 796 | "dev-master": "1.0.x-dev" 797 | } 798 | }, 799 | "autoload": { 800 | "classmap": [ 801 | "src/" 802 | ] 803 | }, 804 | "notification-url": "https://packagist.org/downloads/", 805 | "license": [ 806 | "BSD-3-Clause" 807 | ], 808 | "authors": [ 809 | { 810 | "name": "Sebastian Bergmann", 811 | "email": "sebastian@phpunit.de" 812 | } 813 | ], 814 | "description": "Looks up which function or method a line of code belongs to", 815 | "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", 816 | "time": "2017-03-04T06:30:41+00:00" 817 | }, 818 | { 819 | "name": "sebastian/comparator", 820 | "version": "1.2.4", 821 | "source": { 822 | "type": "git", 823 | "url": "https://github.com/sebastianbergmann/comparator.git", 824 | "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be" 825 | }, 826 | "dist": { 827 | "type": "zip", 828 | "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", 829 | "reference": "2b7424b55f5047b47ac6e5ccb20b2aea4011d9be", 830 | "shasum": "" 831 | }, 832 | "require": { 833 | "php": ">=5.3.3", 834 | "sebastian/diff": "~1.2", 835 | "sebastian/exporter": "~1.2 || ~2.0" 836 | }, 837 | "require-dev": { 838 | "phpunit/phpunit": "~4.4" 839 | }, 840 | "type": "library", 841 | "extra": { 842 | "branch-alias": { 843 | "dev-master": "1.2.x-dev" 844 | } 845 | }, 846 | "autoload": { 847 | "classmap": [ 848 | "src/" 849 | ] 850 | }, 851 | "notification-url": "https://packagist.org/downloads/", 852 | "license": [ 853 | "BSD-3-Clause" 854 | ], 855 | "authors": [ 856 | { 857 | "name": "Jeff Welch", 858 | "email": "whatthejeff@gmail.com" 859 | }, 860 | { 861 | "name": "Volker Dusch", 862 | "email": "github@wallbash.com" 863 | }, 864 | { 865 | "name": "Bernhard Schussek", 866 | "email": "bschussek@2bepublished.at" 867 | }, 868 | { 869 | "name": "Sebastian Bergmann", 870 | "email": "sebastian@phpunit.de" 871 | } 872 | ], 873 | "description": "Provides the functionality to compare PHP values for equality", 874 | "homepage": "http://www.github.com/sebastianbergmann/comparator", 875 | "keywords": [ 876 | "comparator", 877 | "compare", 878 | "equality" 879 | ], 880 | "time": "2017-01-29T09:50:25+00:00" 881 | }, 882 | { 883 | "name": "sebastian/diff", 884 | "version": "1.4.1", 885 | "source": { 886 | "type": "git", 887 | "url": "https://github.com/sebastianbergmann/diff.git", 888 | "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e" 889 | }, 890 | "dist": { 891 | "type": "zip", 892 | "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/13edfd8706462032c2f52b4b862974dd46b71c9e", 893 | "reference": "13edfd8706462032c2f52b4b862974dd46b71c9e", 894 | "shasum": "" 895 | }, 896 | "require": { 897 | "php": ">=5.3.3" 898 | }, 899 | "require-dev": { 900 | "phpunit/phpunit": "~4.8" 901 | }, 902 | "type": "library", 903 | "extra": { 904 | "branch-alias": { 905 | "dev-master": "1.4-dev" 906 | } 907 | }, 908 | "autoload": { 909 | "classmap": [ 910 | "src/" 911 | ] 912 | }, 913 | "notification-url": "https://packagist.org/downloads/", 914 | "license": [ 915 | "BSD-3-Clause" 916 | ], 917 | "authors": [ 918 | { 919 | "name": "Kore Nordmann", 920 | "email": "mail@kore-nordmann.de" 921 | }, 922 | { 923 | "name": "Sebastian Bergmann", 924 | "email": "sebastian@phpunit.de" 925 | } 926 | ], 927 | "description": "Diff implementation", 928 | "homepage": "https://github.com/sebastianbergmann/diff", 929 | "keywords": [ 930 | "diff" 931 | ], 932 | "time": "2015-12-08T07:14:41+00:00" 933 | }, 934 | { 935 | "name": "sebastian/environment", 936 | "version": "2.0.0", 937 | "source": { 938 | "type": "git", 939 | "url": "https://github.com/sebastianbergmann/environment.git", 940 | "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac" 941 | }, 942 | "dist": { 943 | "type": "zip", 944 | "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/5795ffe5dc5b02460c3e34222fee8cbe245d8fac", 945 | "reference": "5795ffe5dc5b02460c3e34222fee8cbe245d8fac", 946 | "shasum": "" 947 | }, 948 | "require": { 949 | "php": "^5.6 || ^7.0" 950 | }, 951 | "require-dev": { 952 | "phpunit/phpunit": "^5.0" 953 | }, 954 | "type": "library", 955 | "extra": { 956 | "branch-alias": { 957 | "dev-master": "2.0.x-dev" 958 | } 959 | }, 960 | "autoload": { 961 | "classmap": [ 962 | "src/" 963 | ] 964 | }, 965 | "notification-url": "https://packagist.org/downloads/", 966 | "license": [ 967 | "BSD-3-Clause" 968 | ], 969 | "authors": [ 970 | { 971 | "name": "Sebastian Bergmann", 972 | "email": "sebastian@phpunit.de" 973 | } 974 | ], 975 | "description": "Provides functionality to handle HHVM/PHP environments", 976 | "homepage": "http://www.github.com/sebastianbergmann/environment", 977 | "keywords": [ 978 | "Xdebug", 979 | "environment", 980 | "hhvm" 981 | ], 982 | "time": "2016-11-26T07:53:53+00:00" 983 | }, 984 | { 985 | "name": "sebastian/exporter", 986 | "version": "2.0.0", 987 | "source": { 988 | "type": "git", 989 | "url": "https://github.com/sebastianbergmann/exporter.git", 990 | "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4" 991 | }, 992 | "dist": { 993 | "type": "zip", 994 | "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", 995 | "reference": "ce474bdd1a34744d7ac5d6aad3a46d48d9bac4c4", 996 | "shasum": "" 997 | }, 998 | "require": { 999 | "php": ">=5.3.3", 1000 | "sebastian/recursion-context": "~2.0" 1001 | }, 1002 | "require-dev": { 1003 | "ext-mbstring": "*", 1004 | "phpunit/phpunit": "~4.4" 1005 | }, 1006 | "type": "library", 1007 | "extra": { 1008 | "branch-alias": { 1009 | "dev-master": "2.0.x-dev" 1010 | } 1011 | }, 1012 | "autoload": { 1013 | "classmap": [ 1014 | "src/" 1015 | ] 1016 | }, 1017 | "notification-url": "https://packagist.org/downloads/", 1018 | "license": [ 1019 | "BSD-3-Clause" 1020 | ], 1021 | "authors": [ 1022 | { 1023 | "name": "Jeff Welch", 1024 | "email": "whatthejeff@gmail.com" 1025 | }, 1026 | { 1027 | "name": "Volker Dusch", 1028 | "email": "github@wallbash.com" 1029 | }, 1030 | { 1031 | "name": "Bernhard Schussek", 1032 | "email": "bschussek@2bepublished.at" 1033 | }, 1034 | { 1035 | "name": "Sebastian Bergmann", 1036 | "email": "sebastian@phpunit.de" 1037 | }, 1038 | { 1039 | "name": "Adam Harvey", 1040 | "email": "aharvey@php.net" 1041 | } 1042 | ], 1043 | "description": "Provides the functionality to export PHP variables for visualization", 1044 | "homepage": "http://www.github.com/sebastianbergmann/exporter", 1045 | "keywords": [ 1046 | "export", 1047 | "exporter" 1048 | ], 1049 | "time": "2016-11-19T08:54:04+00:00" 1050 | }, 1051 | { 1052 | "name": "sebastian/global-state", 1053 | "version": "1.1.1", 1054 | "source": { 1055 | "type": "git", 1056 | "url": "https://github.com/sebastianbergmann/global-state.git", 1057 | "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4" 1058 | }, 1059 | "dist": { 1060 | "type": "zip", 1061 | "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bc37d50fea7d017d3d340f230811c9f1d7280af4", 1062 | "reference": "bc37d50fea7d017d3d340f230811c9f1d7280af4", 1063 | "shasum": "" 1064 | }, 1065 | "require": { 1066 | "php": ">=5.3.3" 1067 | }, 1068 | "require-dev": { 1069 | "phpunit/phpunit": "~4.2" 1070 | }, 1071 | "suggest": { 1072 | "ext-uopz": "*" 1073 | }, 1074 | "type": "library", 1075 | "extra": { 1076 | "branch-alias": { 1077 | "dev-master": "1.0-dev" 1078 | } 1079 | }, 1080 | "autoload": { 1081 | "classmap": [ 1082 | "src/" 1083 | ] 1084 | }, 1085 | "notification-url": "https://packagist.org/downloads/", 1086 | "license": [ 1087 | "BSD-3-Clause" 1088 | ], 1089 | "authors": [ 1090 | { 1091 | "name": "Sebastian Bergmann", 1092 | "email": "sebastian@phpunit.de" 1093 | } 1094 | ], 1095 | "description": "Snapshotting of global state", 1096 | "homepage": "http://www.github.com/sebastianbergmann/global-state", 1097 | "keywords": [ 1098 | "global state" 1099 | ], 1100 | "time": "2015-10-12T03:26:01+00:00" 1101 | }, 1102 | { 1103 | "name": "sebastian/object-enumerator", 1104 | "version": "2.0.1", 1105 | "source": { 1106 | "type": "git", 1107 | "url": "https://github.com/sebastianbergmann/object-enumerator.git", 1108 | "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7" 1109 | }, 1110 | "dist": { 1111 | "type": "zip", 1112 | "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1311872ac850040a79c3c058bea3e22d0f09cbb7", 1113 | "reference": "1311872ac850040a79c3c058bea3e22d0f09cbb7", 1114 | "shasum": "" 1115 | }, 1116 | "require": { 1117 | "php": ">=5.6", 1118 | "sebastian/recursion-context": "~2.0" 1119 | }, 1120 | "require-dev": { 1121 | "phpunit/phpunit": "~5" 1122 | }, 1123 | "type": "library", 1124 | "extra": { 1125 | "branch-alias": { 1126 | "dev-master": "2.0.x-dev" 1127 | } 1128 | }, 1129 | "autoload": { 1130 | "classmap": [ 1131 | "src/" 1132 | ] 1133 | }, 1134 | "notification-url": "https://packagist.org/downloads/", 1135 | "license": [ 1136 | "BSD-3-Clause" 1137 | ], 1138 | "authors": [ 1139 | { 1140 | "name": "Sebastian Bergmann", 1141 | "email": "sebastian@phpunit.de" 1142 | } 1143 | ], 1144 | "description": "Traverses array structures and object graphs to enumerate all referenced objects", 1145 | "homepage": "https://github.com/sebastianbergmann/object-enumerator/", 1146 | "time": "2017-02-18T15:18:39+00:00" 1147 | }, 1148 | { 1149 | "name": "sebastian/recursion-context", 1150 | "version": "2.0.0", 1151 | "source": { 1152 | "type": "git", 1153 | "url": "https://github.com/sebastianbergmann/recursion-context.git", 1154 | "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a" 1155 | }, 1156 | "dist": { 1157 | "type": "zip", 1158 | "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/2c3ba150cbec723aa057506e73a8d33bdb286c9a", 1159 | "reference": "2c3ba150cbec723aa057506e73a8d33bdb286c9a", 1160 | "shasum": "" 1161 | }, 1162 | "require": { 1163 | "php": ">=5.3.3" 1164 | }, 1165 | "require-dev": { 1166 | "phpunit/phpunit": "~4.4" 1167 | }, 1168 | "type": "library", 1169 | "extra": { 1170 | "branch-alias": { 1171 | "dev-master": "2.0.x-dev" 1172 | } 1173 | }, 1174 | "autoload": { 1175 | "classmap": [ 1176 | "src/" 1177 | ] 1178 | }, 1179 | "notification-url": "https://packagist.org/downloads/", 1180 | "license": [ 1181 | "BSD-3-Clause" 1182 | ], 1183 | "authors": [ 1184 | { 1185 | "name": "Jeff Welch", 1186 | "email": "whatthejeff@gmail.com" 1187 | }, 1188 | { 1189 | "name": "Sebastian Bergmann", 1190 | "email": "sebastian@phpunit.de" 1191 | }, 1192 | { 1193 | "name": "Adam Harvey", 1194 | "email": "aharvey@php.net" 1195 | } 1196 | ], 1197 | "description": "Provides functionality to recursively process PHP variables", 1198 | "homepage": "http://www.github.com/sebastianbergmann/recursion-context", 1199 | "time": "2016-11-19T07:33:16+00:00" 1200 | }, 1201 | { 1202 | "name": "sebastian/resource-operations", 1203 | "version": "1.0.0", 1204 | "source": { 1205 | "type": "git", 1206 | "url": "https://github.com/sebastianbergmann/resource-operations.git", 1207 | "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52" 1208 | }, 1209 | "dist": { 1210 | "type": "zip", 1211 | "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", 1212 | "reference": "ce990bb21759f94aeafd30209e8cfcdfa8bc3f52", 1213 | "shasum": "" 1214 | }, 1215 | "require": { 1216 | "php": ">=5.6.0" 1217 | }, 1218 | "type": "library", 1219 | "extra": { 1220 | "branch-alias": { 1221 | "dev-master": "1.0.x-dev" 1222 | } 1223 | }, 1224 | "autoload": { 1225 | "classmap": [ 1226 | "src/" 1227 | ] 1228 | }, 1229 | "notification-url": "https://packagist.org/downloads/", 1230 | "license": [ 1231 | "BSD-3-Clause" 1232 | ], 1233 | "authors": [ 1234 | { 1235 | "name": "Sebastian Bergmann", 1236 | "email": "sebastian@phpunit.de" 1237 | } 1238 | ], 1239 | "description": "Provides a list of PHP built-in functions that operate on resources", 1240 | "homepage": "https://www.github.com/sebastianbergmann/resource-operations", 1241 | "time": "2015-07-28T20:34:47+00:00" 1242 | }, 1243 | { 1244 | "name": "sebastian/version", 1245 | "version": "2.0.1", 1246 | "source": { 1247 | "type": "git", 1248 | "url": "https://github.com/sebastianbergmann/version.git", 1249 | "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019" 1250 | }, 1251 | "dist": { 1252 | "type": "zip", 1253 | "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/99732be0ddb3361e16ad77b68ba41efc8e979019", 1254 | "reference": "99732be0ddb3361e16ad77b68ba41efc8e979019", 1255 | "shasum": "" 1256 | }, 1257 | "require": { 1258 | "php": ">=5.6" 1259 | }, 1260 | "type": "library", 1261 | "extra": { 1262 | "branch-alias": { 1263 | "dev-master": "2.0.x-dev" 1264 | } 1265 | }, 1266 | "autoload": { 1267 | "classmap": [ 1268 | "src/" 1269 | ] 1270 | }, 1271 | "notification-url": "https://packagist.org/downloads/", 1272 | "license": [ 1273 | "BSD-3-Clause" 1274 | ], 1275 | "authors": [ 1276 | { 1277 | "name": "Sebastian Bergmann", 1278 | "email": "sebastian@phpunit.de", 1279 | "role": "lead" 1280 | } 1281 | ], 1282 | "description": "Library that helps with managing the version number of Git-hosted PHP projects", 1283 | "homepage": "https://github.com/sebastianbergmann/version", 1284 | "time": "2016-10-03T07:35:21+00:00" 1285 | }, 1286 | { 1287 | "name": "symfony/yaml", 1288 | "version": "v3.2.8", 1289 | "source": { 1290 | "type": "git", 1291 | "url": "https://github.com/symfony/yaml.git", 1292 | "reference": "acec26fcf7f3031e094e910b94b002fa53d4e4d6" 1293 | }, 1294 | "dist": { 1295 | "type": "zip", 1296 | "url": "https://api.github.com/repos/symfony/yaml/zipball/acec26fcf7f3031e094e910b94b002fa53d4e4d6", 1297 | "reference": "acec26fcf7f3031e094e910b94b002fa53d4e4d6", 1298 | "shasum": "" 1299 | }, 1300 | "require": { 1301 | "php": ">=5.5.9" 1302 | }, 1303 | "require-dev": { 1304 | "symfony/console": "~2.8|~3.0" 1305 | }, 1306 | "suggest": { 1307 | "symfony/console": "For validating YAML files using the lint command" 1308 | }, 1309 | "type": "library", 1310 | "extra": { 1311 | "branch-alias": { 1312 | "dev-master": "3.2-dev" 1313 | } 1314 | }, 1315 | "autoload": { 1316 | "psr-4": { 1317 | "Symfony\\Component\\Yaml\\": "" 1318 | }, 1319 | "exclude-from-classmap": [ 1320 | "/Tests/" 1321 | ] 1322 | }, 1323 | "notification-url": "https://packagist.org/downloads/", 1324 | "license": [ 1325 | "MIT" 1326 | ], 1327 | "authors": [ 1328 | { 1329 | "name": "Fabien Potencier", 1330 | "email": "fabien@symfony.com" 1331 | }, 1332 | { 1333 | "name": "Symfony Community", 1334 | "homepage": "https://symfony.com/contributors" 1335 | } 1336 | ], 1337 | "description": "Symfony Yaml Component", 1338 | "homepage": "https://symfony.com", 1339 | "time": "2017-05-01T14:55:58+00:00" 1340 | }, 1341 | { 1342 | "name": "webmozart/assert", 1343 | "version": "1.2.0", 1344 | "source": { 1345 | "type": "git", 1346 | "url": "https://github.com/webmozart/assert.git", 1347 | "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f" 1348 | }, 1349 | "dist": { 1350 | "type": "zip", 1351 | "url": "https://api.github.com/repos/webmozart/assert/zipball/2db61e59ff05fe5126d152bd0655c9ea113e550f", 1352 | "reference": "2db61e59ff05fe5126d152bd0655c9ea113e550f", 1353 | "shasum": "" 1354 | }, 1355 | "require": { 1356 | "php": "^5.3.3 || ^7.0" 1357 | }, 1358 | "require-dev": { 1359 | "phpunit/phpunit": "^4.6", 1360 | "sebastian/version": "^1.0.1" 1361 | }, 1362 | "type": "library", 1363 | "extra": { 1364 | "branch-alias": { 1365 | "dev-master": "1.3-dev" 1366 | } 1367 | }, 1368 | "autoload": { 1369 | "psr-4": { 1370 | "Webmozart\\Assert\\": "src/" 1371 | } 1372 | }, 1373 | "notification-url": "https://packagist.org/downloads/", 1374 | "license": [ 1375 | "MIT" 1376 | ], 1377 | "authors": [ 1378 | { 1379 | "name": "Bernhard Schussek", 1380 | "email": "bschussek@gmail.com" 1381 | } 1382 | ], 1383 | "description": "Assertions to validate method input/output with nice error messages.", 1384 | "keywords": [ 1385 | "assert", 1386 | "check", 1387 | "validate" 1388 | ], 1389 | "time": "2016-11-23T20:04:58+00:00" 1390 | } 1391 | ], 1392 | "aliases": [], 1393 | "minimum-stability": "stable", 1394 | "stability-flags": [], 1395 | "prefer-stable": false, 1396 | "prefer-lowest": false, 1397 | "platform": [], 1398 | "platform-dev": [] 1399 | } 1400 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | <phpunit bootstrap="./tests/bootstrap.php" backupGlobals="false" colors="true" convertErrorsToExceptions="true" 2 | convertNoticesToExceptions="true" convertWarningsToExceptions="true"> 3 | 4 | <testsuites> 5 | <testsuite name="unit"> 6 | <directory prefix="Test" suffix=".php">./tests/</directory> 7 | </testsuite> 8 | </testsuites> 9 | 10 | <whitelist processUncoveredFilesFromWhitelist="true"> 11 | <directory suffix=".php">./src</directory> 12 | </whitelist> 13 | </phpunit> -------------------------------------------------------------------------------- /src/FormatConstraint.php: -------------------------------------------------------------------------------- 1 | <?php 2 | /** 3 | * Extended Format Constraint to add support for 'html' format. 4 | * 5 | * @author Iron Bound Designs 6 | * @since 1.0 7 | * @copyright 2017 (c) Iron Bound Designs. 8 | * @license MIT 9 | */ 10 | 11 | namespace IronBound\WP_REST_API\SchemaValidator; 12 | 13 | use JsonSchema\Entity\JsonPointer; 14 | 15 | /** 16 | * Class FormatConstraint 17 | * 18 | * @package IronBound\WP_REST_API\SchemaValidator 19 | */ 20 | class FormatConstraint extends \JsonSchema\Constraints\FormatConstraint { 21 | 22 | /** 23 | * @inheritDoc 24 | */ 25 | public function check( &$element, $schema = null, JsonPointer $path = null, $i = null ) { 26 | 27 | if ( ! isset( $schema->format ) || $this->factory->getConfig( self::CHECK_MODE_DISABLE_FORMAT ) ) { 28 | return; 29 | } 30 | 31 | if ( $schema->format === 'html' ) { 32 | $allowed = isset( $schema->formatAllowedHtml ) ? $schema->formatAllowedHtml : array(); 33 | 34 | if ( ! $this->validateHtml( $element, $allowed ) ) { 35 | $this->addError( $path, 'Invalid html', 'format', array( 'format' => $schema->format ) ); 36 | } 37 | } 38 | 39 | return parent::check( $element, $schema, $path, $i ); 40 | } 41 | 42 | protected function validateHtml( $html, array $allowed_html_tags = array() ) { 43 | 44 | global $allowedposttags, $allowedtags; 45 | 46 | if ( $allowed_html_tags ) { 47 | $kses_format = array(); 48 | 49 | foreach ( $allowed_html_tags as $tag ) { 50 | if ( isset( $allowedposttags[ $tag ] ) ) { 51 | $kses_format[ $tag ] = $allowedposttags[ $tag ]; 52 | } 53 | } 54 | } else { 55 | $kses_format = $allowedtags; 56 | $kses_format['p'] = array(); 57 | } 58 | 59 | return trim( $html ) === trim( wp_kses( $html, $kses_format ) ); 60 | } 61 | } -------------------------------------------------------------------------------- /src/LazyRetriever.php: -------------------------------------------------------------------------------- 1 | <?php 2 | /** 3 | * Class Lazy Retriever. 4 | * 5 | * @author Iron Bound Designs 6 | * @since 1.0 7 | * @copyright 2018 (c) Iron Bound Designs. 8 | * @license GPLv2 9 | */ 10 | 11 | namespace IronBound\WP_REST_API\SchemaValidator; 12 | 13 | use JsonSchema\Uri\Retrievers\AbstractRetriever; 14 | use JsonSchema\Validator; 15 | 16 | /** 17 | * Class LazyRetriever 18 | * 19 | * @package IronBound\WP_REST_API\SchemaValidator 20 | */ 21 | class LazyRetriever extends AbstractRetriever { 22 | 23 | /** 24 | * Contains schema resolvers as URI => Callable. 25 | * 26 | * @var callable[] 27 | */ 28 | private $callables; 29 | 30 | /** 31 | * Contains schemas as URI => JSON 32 | * 33 | * @var array 34 | */ 35 | private $schemas = array(); 36 | 37 | /** 38 | * Constructor 39 | * 40 | * @param callable[] $schemas 41 | * @param string $contentType 42 | */ 43 | public function __construct( array $callables, $contentType = Validator::SCHEMA_MEDIA_TYPE ) { 44 | $this->callables = $callables; 45 | $this->contentType = $contentType; 46 | } 47 | 48 | /** 49 | * Add a callable registration. 50 | * 51 | * @param string $uri 52 | * @param callable $callable 53 | */ 54 | public function add_callable( $uri, $callable ) { 55 | if ( is_callable( $callable ) ) { 56 | $this->callables[ $uri ] = $callable; 57 | } 58 | } 59 | 60 | /** 61 | * Add a Schema entry. 62 | * 63 | * @param string $uri 64 | * @param string $schema 65 | */ 66 | public function add_schema( $uri, $schema ) { 67 | $this->schemas[ $uri ] = $schema; 68 | } 69 | 70 | /** 71 | * {@inheritdoc} 72 | * 73 | * @see \JsonSchema\Uri\Retrievers\UriRetrieverInterface::retrieve() 74 | */ 75 | public function retrieve( $uri ) { 76 | if ( ! array_key_exists( $uri, $this->callables ) && ! array_key_exists( $uri, $this->schemas ) ) { 77 | throw new \JsonSchema\Exception\ResourceNotFoundException( sprintf( 78 | 'The JSON schema "%s" was not found.', 79 | $uri 80 | ) ); 81 | } 82 | 83 | if ( ! isset( $this->schemas[ $uri ] ) ) { 84 | 85 | $schema = call_user_func( $this->callables[ $uri ] ); 86 | 87 | if ( ! empty( $schema['properties'] ) ) { 88 | foreach ( $schema['properties'] as &$property ) { 89 | unset( $property['arg_options'], $property['sanitize_callback'], $property['validate_callback'] ); 90 | } 91 | } 92 | 93 | $this->schemas[ $uri ] = wp_json_encode( $schema ); 94 | } 95 | 96 | return $this->schemas[ $uri ]; 97 | } 98 | } -------------------------------------------------------------------------------- /src/Middleware.php: -------------------------------------------------------------------------------- 1 | <?php 2 | /** 3 | * Schema Validator 4 | * 5 | * @author Iron Bound Designs 6 | * @since 1.0 7 | * @copyright 2017 (c) Iron Bound Designs. 8 | * @license GPLv2 9 | */ 10 | 11 | namespace IronBound\WP_REST_API\SchemaValidator; 12 | 13 | use JsonSchema\Constraints\Constraint; 14 | use JsonSchema\Constraints\Factory; 15 | use JsonSchema\Entity\JsonPointer; 16 | use JsonSchema\Exception\ResourceNotFoundException; 17 | use JsonSchema\Iterator\ObjectIterator; 18 | use JsonSchema\SchemaStorage; 19 | use JsonSchema\Uri\Retrievers\PredefinedArray; 20 | use JsonSchema\Uri\UriRetriever; 21 | use JsonSchema\Validator; 22 | 23 | /** 24 | * Class Middleware 25 | * 26 | * @package IronBound\WP_REST_API\SchemaValidator 27 | */ 28 | class Middleware { 29 | 30 | /** @var string */ 31 | private $namespace; 32 | 33 | /** @var array */ 34 | private $strings; 35 | 36 | /** @var int */ 37 | private $check_mode; 38 | 39 | /** @var array */ 40 | private $options; 41 | 42 | /** @var array[] */ 43 | private $shared_schemas = array(); 44 | 45 | /** @var UriRetriever */ 46 | private $uri_retriever; 47 | 48 | /** @var SchemaStorage */ 49 | private $schema_storage; 50 | 51 | /** @var array[] '/wp/v2/posts' => [ 'GET' => 'posts', 'POST' => 'http://...', 'PUT' => 'http://...' ] */ 52 | private $routes_to_schema_urls = array(); 53 | 54 | /** 55 | * Middleware constructor. 56 | * 57 | * @param string $namespace 58 | * @param array $strings 59 | * @param int $check_mode Check mode. See Constraint class constants. 60 | * @param array $options Additional options to customize how the middleware behaves. 61 | */ 62 | public function __construct( $namespace, array $strings = array(), $check_mode = 0, array $options = [] ) { 63 | $this->namespace = trim( $namespace, '/' ); 64 | $this->strings = wp_parse_args( $strings, array( 65 | 'methodParamDescription' => 'HTTP method to get the schema for. If not provided, will use the base schema.', 66 | 'schemaNotFound' => 'Schema not found.', 67 | 'expandSchema' => 'Expand $ref schemas.', 68 | ) ); 69 | 70 | if ( $check_mode === 0 ) { 71 | $check_mode = Constraint::CHECK_MODE_NORMAL | Constraint::CHECK_MODE_APPLY_DEFAULTS | Constraint::CHECK_MODE_COERCE_TYPES | Constraint::CHECK_MODE_TYPE_CAST; 72 | } 73 | 74 | $this->check_mode = $check_mode; 75 | $this->options = $options; 76 | } 77 | 78 | /** 79 | * Initialize the middleware. 80 | * 81 | * @since 1.0.0 82 | */ 83 | public function initialize() { 84 | add_filter( 'rest_dispatch_request', array( $this, 'validate_and_conform_request' ), 10, 4 ); 85 | add_action( 'rest_api_init', array( $this, 'load_schemas' ), 100 ); 86 | add_filter( 'rest_endpoints', array( $this, 'remove_default_validators_and_set_variable_schemas' ) ); 87 | } 88 | 89 | /** 90 | * Deinitialize the middleware and remove filters. 91 | * 92 | * @since 1.0.0 93 | */ 94 | public function deinitialize() { 95 | remove_filter( 'rest_dispatch_request', array( $this, 'validate_and_conform_request' ), 10 ); 96 | remove_action( 'rest_api_init', array( $this, 'load_schemas' ), 100 ); 97 | remove_filter( 'rest_endpoints', array( $this, 'remove_default_validators_and_set_variable_schemas' ) ); 98 | } 99 | 100 | /** 101 | * Add a schema that is not attached to a particular route, but can still be referenced by URL. 102 | * 103 | * @since 1.0.0 104 | * 105 | * @param array $schema 106 | */ 107 | public function add_shared_schema( array $schema ) { 108 | $this->shared_schemas[] = $schema; 109 | } 110 | 111 | /** 112 | * After the routes have been registered with the REST server, load all of their schemas into schema storage. 113 | * 114 | * @since 1.0.0 115 | * 116 | * @param \WP_REST_Server $server 117 | */ 118 | public function load_schemas( \WP_REST_Server $server ) { 119 | 120 | $endpoints = $this->get_endpoints_for_namespace( $server ); 121 | $schemas = $callables = array(); 122 | $urls_by_method = array(); 123 | 124 | foreach ( $endpoints as $route => $handlers ) { 125 | 126 | $options = $server->get_route_options( $route ); 127 | 128 | if ( empty( $options['schema'] ) ) { 129 | if ( empty( $handlers[0]['schema'] ) ) { 130 | continue; 131 | } 132 | 133 | $callable = $handlers[0]['schema']; 134 | $title = isset( $handlers[0]['schema-title'] ) ? $handlers[0]['schema-title'] : ''; 135 | } else { 136 | $callable = $options['schema']; 137 | $title = isset( $options['schema-title'] ) ? $options['schema-title'] : ''; 138 | } 139 | 140 | if ( $title ) { 141 | $schema = null; 142 | } else { 143 | $schema = call_user_func( $callable ); 144 | 145 | if ( empty( $schema['title'] ) ) { 146 | continue; 147 | } 148 | 149 | $title = $schema['title']; 150 | } 151 | 152 | $uri = $this->get_url_for_schema( $title ); 153 | 154 | $urls_by_method[ $route ] = array(); 155 | 156 | if ( $schema ) { 157 | $schemas[ $uri ] = wp_json_encode( $schema ); 158 | } else { 159 | $callables[ $uri ] = $callable; 160 | } 161 | 162 | if ( isset( $handlers['callback'] ) ) { 163 | $handlers = array( $handlers ); 164 | } 165 | 166 | // Allow for different schemas per HTTP Method. 167 | foreach ( $handlers as $i => $handler ) { 168 | foreach ( $handler['methods'] as $method => $_ ) { 169 | 170 | if ( ! isset( $options["schema-{$method}"] ) ) { 171 | $urls_by_method[ $route ][ $method ] = $uri; 172 | 173 | continue; 174 | } 175 | 176 | $method_schema_cb = $options["schema-{$method}"]; 177 | $method_schema = null; 178 | 179 | if ( isset( $options["schema-title-{$method}"] ) ) { 180 | $method_title = $options["schema-title-{$method}"]; 181 | } else { 182 | $method_schema = call_user_func( $method_schema_cb ); 183 | 184 | if ( empty( $method_schema['title'] ) ) { 185 | continue; 186 | } 187 | 188 | $method_title = $method_schema['title']; 189 | } 190 | 191 | $method_uri = $this->get_url_for_schema( $title, $method ); 192 | 193 | if ( $method_schema ) { 194 | $schemas[ $method_uri ] = wp_json_encode( $method_schema ); 195 | } else { 196 | $callables[ $method_uri ] = $method_schema_cb; 197 | } 198 | 199 | $urls_by_method[ $route ][ $method ] = $method_uri; 200 | 201 | if ( $method_title !== $title ) { 202 | $alt_method_uri = $this->get_url_for_schema( $method_title ); 203 | 204 | if ( $method_schema ) { 205 | $schemas[ $alt_method_uri ] = wp_json_encode( $method_schema ); 206 | } else { 207 | $callables[ $alt_method_uri ] = $method_schema_cb; 208 | } 209 | } 210 | } 211 | } 212 | } 213 | 214 | $strategy = new LazyRetriever( $callables ); 215 | 216 | foreach ( $this->shared_schemas as $shared_schema ) { 217 | $strategy->add_schema( $this->get_url_for_schema( $shared_schema['title'] ), wp_json_encode( $shared_schema ) ); 218 | } 219 | 220 | foreach ( $schemas as $uri => $schema ) { 221 | $strategy->add_schema( $uri, $schema ); 222 | } 223 | 224 | $this->uri_retriever = new UriRetriever(); 225 | $this->uri_retriever->setUriRetriever( $strategy ); 226 | 227 | $this->schema_storage = new SchemaStorage( $this->uri_retriever ); 228 | $this->routes_to_schema_urls = $urls_by_method; 229 | 230 | $this->register_schema_route(); 231 | } 232 | 233 | /** 234 | * Validate a request and conform it to the schema. 235 | * 236 | * @since 1.0.0 237 | * 238 | * @param \WP_REST_Response|null|\WP_Error $response 239 | * @param \WP_REST_Request $request 240 | * @param string $route 241 | * @param array $handler 242 | * 243 | * @return \WP_REST_Response|null|\WP_Error 244 | */ 245 | public function validate_and_conform_request( $response, $request, $route, $handler ) { 246 | 247 | if ( $response !== null ) { 248 | return $response; 249 | } 250 | 251 | if ( strpos( trim( $route, '/' ), $this->namespace ) !== 0 ) { 252 | return $response; 253 | } 254 | 255 | $method = $request->get_method(); 256 | 257 | if ( $method === 'PATCH' ) { 258 | $patch_get_info = $this->get_validate_info_for_method( 'GET', $route, $handler ); 259 | $validated = $this->validate_and_conform_for_method( $request, $patch_get_info['schema'], 'GET' ); 260 | 261 | if ( is_wp_error( $validated ) ) { 262 | return $validated; 263 | } 264 | } 265 | 266 | $info = $this->get_validate_info_for_method( $method, $route, $handler ); 267 | 268 | if ( ! $info ) { 269 | return $response; 270 | } 271 | 272 | if ( ! empty( $info['described'] ) ) { 273 | $this->add_described_by( $request, $info['described'] ); 274 | } 275 | 276 | return $this->validate_and_conform_for_method( $request, $info['schema'], $method ); 277 | } 278 | 279 | /** 280 | * Get the schema to use for a given method. 281 | * 282 | * @param string $method 283 | * @param string $route 284 | * @param array $handler 285 | * 286 | * @return array 287 | */ 288 | protected function get_validate_info_for_method( $method, $route, $handler ) { 289 | $map = $this->routes_to_schema_urls; 290 | 291 | if ( $method === 'GET' || $method === 'DELETE' ) { 292 | $schema_object = json_decode( $this->transform_schema_to_json( array( 293 | 'type' => 'object', 294 | 'properties' => $handler['args'], 295 | ) ) ); 296 | $described_by = isset( $map[ $route ], $map[ $route ][ $method ] ) ? $map[ $route ][ $method ] : null; 297 | } elseif ( isset( $map[ $route ], $map[ $route ][ $method ] ) ) { 298 | $schema_object = clone $this->schema_storage->getSchema( $map[ $route ][ $method ] ); 299 | $described_by = $map[ $route ][ $method ]; 300 | } else { 301 | return array(); 302 | } 303 | 304 | return array( 305 | 'schema' => $schema_object, 306 | 'described' => $described_by, 307 | ); 308 | } 309 | 310 | 311 | /** 312 | * Conform the request or return an error. 313 | * 314 | * @param \WP_REST_Request $request 315 | * @param object $schema 316 | * @param string $method 317 | * 318 | * @return null|\WP_Error 319 | */ 320 | public function validate_and_conform_for_method( $request, $schema, $method ) { 321 | 322 | $to_validate = $this->get_params_to_validate( $request, $method ); 323 | 324 | /*if ( ! $to_validate ) { 325 | return null; 326 | }*/ 327 | 328 | $validated = $this->validate_params( $to_validate, $schema, $method ); 329 | 330 | if ( is_wp_error( $validated ) ) { 331 | return $validated; 332 | } 333 | 334 | $this->update_request_params( $request, $validated ); 335 | 336 | return null; 337 | } 338 | 339 | /** 340 | * Get the parameters that we should be validating. 341 | * 342 | * @param \WP_REST_Request $request 343 | * @param string $method 344 | * 345 | * @return array 346 | */ 347 | protected function get_params_to_validate( $request, $method ) { 348 | 349 | $defaults = $request->get_default_params(); 350 | $request->set_default_params( array() ); 351 | 352 | if ( $request->get_method() === 'PATCH' && $method === 'PATCH' ) { 353 | $to_validate = $request->get_json_params() ?: $request->get_body_params(); 354 | } elseif ( $request->get_method() === 'PATCH' && $method === 'GET' ) { 355 | $to_validate = $request->get_query_params(); 356 | } elseif ( ! empty( $this->options['strict_body'] ) && ( $request->get_method() === 'POST' || $request->get_method() === 'PUT' ) ) { 357 | $to_validate = $request->get_json_params() ?: $request->get_body_params(); 358 | } else { 359 | $to_validate = $request->get_params(); 360 | 361 | foreach ( $request->get_url_params() as $param => $value ) { 362 | unset( $to_validate[ $param ] ); 363 | } 364 | } 365 | 366 | $request->set_default_params( $defaults ); 367 | 368 | return $to_validate; 369 | } 370 | 371 | /** 372 | * Update the params on a request object. 373 | * 374 | * @param \WP_REST_Request $request 375 | * @param array $validated 376 | */ 377 | protected function update_request_params( $request, $validated ) { 378 | 379 | $defaults = $request->get_default_params(); 380 | $request->set_default_params( array() ); 381 | 382 | foreach ( $validated as $property => $value ) { 383 | 384 | if ( $value === null && $request[ $property ] !== null ) { 385 | unset( $request[ $property ] ); 386 | continue; 387 | } 388 | 389 | if ( $value === $request->get_param( $property ) ) { 390 | continue; 391 | } 392 | 393 | $request->set_param( $property, $value ); 394 | } 395 | 396 | $request->set_default_params( $defaults ); 397 | } 398 | 399 | /** 400 | * Validate parameters. 401 | * 402 | * @since 1.0.0 403 | * 404 | * @param array $to_validate 405 | * @param \stdClass $schema_object 406 | * @param string $method 407 | * 408 | * @return array|\WP_Error 409 | */ 410 | protected function validate_params( $to_validate, $schema_object, $method ) { 411 | 412 | $to_validate = json_decode( wp_json_encode( $to_validate ) ); 413 | $validator = $this->make_validator( $method === 'POST' ); 414 | 415 | $validator->validate( $to_validate, $schema_object ); 416 | 417 | if ( $validator->isValid() ) { 418 | $return = array(); 419 | 420 | // Validate may change the request contents based on the check mode. 421 | foreach ( json_decode( json_encode( $to_validate ), true ) as $prop => $value ) { 422 | $return[ $prop ] = $value; 423 | } 424 | 425 | return $return; 426 | } 427 | 428 | $errors = $validator->getErrors(); 429 | $missing_errors = array_filter( $errors, function ( $error ) { return $error['constraint'] === 'required'; } ); 430 | 431 | $required = array(); 432 | 433 | foreach ( $missing_errors as $missing_error ) { 434 | $required[] = $missing_error['property']; 435 | } 436 | 437 | if ( $required ) { 438 | return new \WP_Error( 439 | 'rest_missing_callback_param', 440 | sprintf( __( 'Missing parameter(s): %s' ), implode( ', ', $required ) ), 441 | array( 'status' => 400, 'params' => $required ) 442 | ); 443 | } 444 | 445 | $invalid_params = array(); 446 | 447 | foreach ( $validator->getErrors() as $error ) { 448 | $invalid_params[ $error['property'] ?: '#' ] = $error['message']; 449 | } 450 | 451 | return new \WP_Error( 452 | 'rest_invalid_param', 453 | sprintf( __( 'Invalid parameter(s): %s' ), implode( ', ', array_keys( $invalid_params ) ) ), 454 | array( 'status' => 400, 'params' => $invalid_params ) 455 | ); 456 | } 457 | 458 | /** 459 | * Make a Schema validator. 460 | * 461 | * @since 1.0.0 462 | * 463 | * @param bool $is_create_request 464 | * @param bool $skip_readonly 465 | * 466 | * @return Validator 467 | */ 468 | protected function make_validator( $is_create_request = false, $skip_readonly = true ) { 469 | $factory = new Factory( 470 | $this->schema_storage, 471 | $this->uri_retriever, 472 | $this->check_mode 473 | ); 474 | $factory->setConstraintClass( 475 | 'undefined', 476 | '\IronBound\WP_REST_API\SchemaValidator\UndefinedConstraint' 477 | ); 478 | $factory->setConstraintClass( 479 | 'format', 480 | '\IronBound\WP_REST_API\SchemaValidator\FormatConstraint' 481 | ); 482 | $factory->setConstraintClass( 483 | 'type', 484 | '\IronBound\WP_REST_API\SchemaValidator\TypeConstraint' 485 | ); 486 | 487 | if ( $is_create_request ) { 488 | $factory->addConfig( UndefinedConstraint::CHECK_MODE_CREATE_REQUEST ); 489 | } 490 | 491 | if ( $skip_readonly ) { 492 | $factory->addConfig( UndefinedConstraint::CHECK_MODE_SKIP_READONLY ); 493 | } 494 | 495 | return new Validator( $factory ); 496 | } 497 | 498 | /** 499 | * Add the described by header. 500 | * 501 | * @since 1.0.0 502 | * 503 | * @param \WP_REST_Request $request 504 | * @param string $described_by 505 | */ 506 | protected function add_described_by( \WP_REST_Request $request, $described_by ) { 507 | 508 | if ( ! $described_by ) { 509 | return; 510 | } 511 | 512 | add_filter( 'rest_post_dispatch', $fn = function ( $response, $_, $_request ) use ( $request, $described_by, &$fn ) { 513 | 514 | if ( $request !== $_request ) { 515 | return $response; 516 | } 517 | 518 | if ( $response instanceof \WP_REST_Response ) { 519 | $response->link_header( 'describedby', $described_by ); 520 | } 521 | 522 | remove_filter( 'rest_post_dispatch', $fn ); 523 | 524 | return $response; 525 | }, 10, 3 ); 526 | } 527 | 528 | /** 529 | * Remove the default validator functions from endpoints in this namespace. 530 | * 531 | * @since 1.0.0 532 | * 533 | * @param array[] $endpoints 534 | * 535 | * @return array 536 | */ 537 | public function remove_default_validators_and_set_variable_schemas( array $endpoints ) { 538 | 539 | /** @var array $handlers */ 540 | foreach ( $endpoints as $route => $handlers ) { 541 | if ( isset( $handlers['namespace'] ) && $handlers['namespace'] !== $this->namespace ) { 542 | continue; 543 | } 544 | 545 | if ( isset( $handlers['callback'] ) ) { 546 | $endpoints[ $route ] = $this->set_default_callbacks_for_handler( $handlers ); 547 | 548 | continue; 549 | } 550 | 551 | $handlers = array_filter( $handlers, 'is_int', ARRAY_FILTER_USE_KEY ); 552 | 553 | foreach ( $handlers as $i => $handler ) { 554 | 555 | if ( isset( $handler['namespace'] ) && $handler['namespace'] !== $this->namespace ) { 556 | continue; 557 | } 558 | 559 | $endpoints[ $route ][ $i ] = $this->set_default_callbacks_for_handler( $handler ); 560 | 561 | // Variable schema. Move to specific option for method. 562 | if ( count( $handlers ) > 1 && isset( $handler['schema'] ) ) { 563 | $methods = is_string( $handler['methods'] ) ? explode( ',', $handler['methods'] ) : $handler['methods']; 564 | 565 | foreach ( $methods as $method ) { 566 | $endpoints[ $route ]["schema-{$method}"] = $handler['schema']; 567 | 568 | if ( isset( $handler['schema-title'] ) ) { 569 | $endpoints[ $route ]["schema-title-{$method}"] = $handler['schema-title']; 570 | } 571 | } 572 | } elseif ( isset( $handler['schema'] ) ) { 573 | // Have the per-route schema overwrite the main schema. 574 | $endpoints[ $route ]['schema'] = $handler['schema']; 575 | } 576 | } 577 | } 578 | 579 | return $endpoints; 580 | } 581 | 582 | /** 583 | * Transform an array based schema to JSON. 584 | * 585 | * @since 1.0.0 586 | * 587 | * @param array $schema 588 | * 589 | * @return false|string 590 | */ 591 | protected function transform_schema_to_json( array $schema ) { 592 | 593 | if ( ! empty( $schema['properties'] ) ) { 594 | foreach ( $schema['properties'] as &$property ) { 595 | unset( $property['arg_options'], $property['sanitize_callback'], $property['validate_callback'] ); 596 | } 597 | } 598 | 599 | return wp_json_encode( $schema ); 600 | } 601 | 602 | /** 603 | * Get the URL to a schema. 604 | * 605 | * @since 1.0.0 606 | * 607 | * @param string $title The 'title' property of the schema. 608 | * @param string $method 609 | * 610 | * @return string 611 | */ 612 | public function get_url_for_schema( $title, $method = '' ) { 613 | $url = rest_url( "{$this->namespace}/schemas/{$title}" ); 614 | 615 | if ( $method ) { 616 | $url = urldecode_deep( add_query_arg( 'method', strtoupper( $method ), $url ) ); 617 | } 618 | 619 | return $url; 620 | } 621 | 622 | /** 623 | * Register the REST Route to show schemas. 624 | * 625 | * @since 1.0.0 626 | */ 627 | protected function register_schema_route() { 628 | register_rest_route( $this->namespace, '/schemas/(?P<title>\S+)', array( 629 | 'args' => array( 630 | 'method' => array( 631 | 'description' => $this->strings['methodParamDescription'], 632 | 'type' => 'string', 633 | 'enum' => array( 'GET', 'POST', 'PUT', 'PATCH', 'DELETE' ), 634 | ), 635 | 'expand' => array( 636 | 'description' => $this->strings['expandSchema'], 637 | 'type' => 'boolean', 638 | ), 639 | ), 640 | 'methods' => 'GET', 641 | 'callback' => array( $this, 'get_schema_endpoint' ), 642 | 'permission_callback' => '__return_true', 643 | ) ); 644 | } 645 | 646 | /** 647 | * REST endpoint for retrieving a schema. 648 | * 649 | * @since 1.0.0 650 | * 651 | * @param \WP_REST_Request $request 652 | * 653 | * @return \WP_Error|\WP_REST_Response 654 | */ 655 | public function get_schema_endpoint( \WP_REST_Request $request ) { 656 | 657 | $title = $request['title']; 658 | $schema = null; 659 | $method = $request['method']; 660 | 661 | $try = array( $this->get_url_for_schema( $title ) ); 662 | 663 | if ( $method ) { 664 | $try[] = $this->get_url_for_schema( $title, $method ); 665 | } 666 | 667 | foreach ( array_reverse( $try ) as $url ) { 668 | try { 669 | $schema = $this->schema_storage->getSchema( $url ); 670 | 671 | if ( $request['expand'] ) { 672 | $schema = $this->expand( $schema ); 673 | } 674 | break; 675 | } catch ( ResourceNotFoundException $e ) { 676 | 677 | } 678 | } 679 | 680 | if ( ! $schema ) { 681 | return new \WP_Error( 682 | 'schema_not_found', 683 | $this->strings['schemaNotFound'], 684 | array( 'status' => \WP_Http::NOT_FOUND ) 685 | ); 686 | } 687 | 688 | $response = new \WP_REST_Response( $this->clean_schema( json_decode( wp_json_encode( $schema ), true ) ) ); 689 | 690 | foreach ( $this->routes_to_schema_urls as $path => $urls ) { 691 | foreach ( $urls as $maybe_url ) { 692 | if ( $maybe_url === $url ) { 693 | $template = $this->convert_regex_route_to_uri_template( rest_url( $path ) ); 694 | $response->link_header( 'describes', $template ); 695 | break 2; 696 | } 697 | } 698 | } 699 | 700 | return $response; 701 | } 702 | 703 | /** 704 | * Clean a schema of any arg_options. 705 | * 706 | * @param array $schema 707 | * 708 | * @return array 709 | */ 710 | protected function clean_schema( $schema ) { 711 | 712 | if ( is_array( $schema ) ) { 713 | foreach ( $schema as $key => $value ) { 714 | if ( is_array( $value ) ) { 715 | unset( $value['arg_options'] ); 716 | $schema[ $key ] = $this->clean_schema( $value ); 717 | } 718 | } 719 | } 720 | 721 | return $schema; 722 | } 723 | 724 | /** 725 | * Expand $ref schemas. 726 | * 727 | * @since 1.0.0 728 | * 729 | * @param \stdClass $schema 730 | * 731 | * @return \stdClass 732 | */ 733 | protected function expand( $schema ) { 734 | 735 | foreach ( $schema as $i => $sub_schema ) { 736 | if ( is_object( $sub_schema ) && property_exists( $sub_schema, '$ref' ) && is_string( $sub_schema->{'$ref'} ) ) { 737 | $schema->{$i} = $this->schema_storage->resolveRefSchema( $sub_schema ); 738 | } elseif ( is_object( $sub_schema ) ) { 739 | $schema->{$i} = $this->expand( $sub_schema ); 740 | } 741 | } 742 | 743 | return $schema; 744 | } 745 | 746 | /** 747 | * Set the validate and sanitize callbacks to false if not set to disable WP's default validation. 748 | * 749 | * @since 1.0.0 750 | * 751 | * @param array $handler 752 | * 753 | * @return array 754 | */ 755 | private function set_default_callbacks_for_handler( array $handler ) { 756 | 757 | if ( empty( $handler['args'] ) || ! is_array( $handler['args'] ) ) { 758 | return $handler; 759 | } 760 | 761 | foreach ( $handler['args'] as $i => $arg ) { 762 | 763 | if ( empty( $arg['validate_callback'] ) || $arg['validate_callback'] === 'rest_validate_request_arg' ) { 764 | $arg['validate_callback'] = false; 765 | } 766 | 767 | if ( empty( $arg['sanitize_callback'] ) || $arg['sanitize_callback'] === 'rest_sanitize_request_arg' ) { 768 | $arg['sanitize_callback'] = false; 769 | } 770 | 771 | $handler['args'][ $i ] = $arg; 772 | } 773 | 774 | return $handler; 775 | } 776 | 777 | /** 778 | * Get all endpoint configurations for this namespace. 779 | * 780 | * @since 1.0.0 781 | * 782 | * @param \WP_REST_Server $server 783 | * 784 | * @return array 785 | */ 786 | protected function get_endpoints_for_namespace( \WP_REST_Server $server ) { 787 | 788 | $routes = $server->get_routes(); 789 | $matched = array(); 790 | 791 | foreach ( $routes as $route => $handlers ) { 792 | $options = $server->get_route_options( $route ); 793 | 794 | if ( ! isset( $options['namespace'] ) || $options['namespace'] !== $this->namespace ) { 795 | continue; 796 | } 797 | 798 | $matched[ $route ] = $handlers; 799 | } 800 | 801 | return $matched; 802 | } 803 | 804 | /** 805 | * Convert an array to an object. 806 | * 807 | * @since 1.0.0 808 | * 809 | * @param array $array 810 | * 811 | * @return \stdClass 812 | */ 813 | private static function array_to_object( array $array ) { 814 | $obj = new \stdClass; 815 | 816 | foreach ( $array as $k => $v ) { 817 | if ( is_array( $v ) ) { 818 | $obj->{$k} = self::array_to_object( $v ); 819 | } elseif ( $k !== '' ) { 820 | $obj->{$k} = $v; 821 | } 822 | } 823 | 824 | return $obj; 825 | } 826 | 827 | /** 828 | * Convert a regex based route to one that follows the URI template standard. 829 | * 830 | * @param string $url 831 | * 832 | * @return string 833 | */ 834 | private function convert_regex_route_to_uri_template( $url ) { 835 | return preg_replace( '/\(.[^<*]<(\w+)>[^<.]*\)/', '{$1}', $url ); 836 | } 837 | } 838 | -------------------------------------------------------------------------------- /src/TypeConstraint.php: -------------------------------------------------------------------------------- 1 | <?php 2 | /** 3 | * Extended Types Constraint to add support for coercing "1" to true. 4 | * 5 | * @author Iron Bound Designs 6 | * @since 1.0 7 | * @copyright 2018 (c) Iron Bound Designs. 8 | * @license MIT 9 | */ 10 | 11 | namespace IronBound\WP_REST_API\SchemaValidator; 12 | 13 | /** 14 | * Class TypeConstraint 15 | * 16 | * @package IronBound\WP_REST_API\SchemaValidator 17 | */ 18 | class TypeConstraint extends \JsonSchema\Constraints\TypeConstraint { 19 | 20 | /** 21 | * @inheritDoc 22 | */ 23 | protected function toBoolean( $value ) { 24 | 25 | if ( $value === "1" ) { 26 | return true; 27 | } 28 | 29 | if ( $value === "0" ) { 30 | return false; 31 | } 32 | 33 | return parent::toBoolean( $value ); 34 | } 35 | } -------------------------------------------------------------------------------- /src/UndefinedConstraint.php: -------------------------------------------------------------------------------- 1 | <?php 2 | /** 3 | * Extended Undefined Constraint to add support for readonly and createonly. 4 | * 5 | * @author Iron Bound Designs 6 | * @since 1.0 7 | * @copyright 2017 (c) Iron Bound Designs. 8 | * @license MIT 9 | */ 10 | 11 | namespace IronBound\WP_REST_API\SchemaValidator; 12 | 13 | use JsonSchema\Entity\JsonPointer; 14 | 15 | /** 16 | * Class UndefinedConstraint 17 | * 18 | * @package IronBound\WP_REST_API\SchemaValidator 19 | */ 20 | class UndefinedConstraint extends \JsonSchema\Constraints\UndefinedConstraint { 21 | 22 | const CHECK_MODE_SKIP_READONLY = 0x1000000; 23 | const CHECK_MODE_CREATE_REQUEST = 0x2000000; 24 | 25 | /** 26 | * @inheritdoc 27 | */ 28 | public function check( &$value, $schema = null, JsonPointer $path = null, $i = null, $fromDefault = false ) { 29 | 30 | if ( is_null( $schema ) || ! is_object( $schema ) ) { 31 | return; 32 | } 33 | 34 | if ( $this->factory->getConfig( self::CHECK_MODE_SKIP_READONLY ) && ( ! empty( $schema->readonly ) || ! empty( $schema->readOnly ) ) ) { 35 | $value = null; 36 | 37 | return; 38 | } 39 | 40 | // If this is not a create request and the property is marked as createOnly, then skip validation for it. 41 | if ( ! $this->factory->getConfig( self::CHECK_MODE_CREATE_REQUEST ) && ( ! empty( $schema->createonly ) || ! empty( $schema->createOnly ) ) ) { 42 | $value = null; 43 | 44 | return; 45 | } 46 | 47 | return parent::check( $value, $schema, $path, $i, $fromDefault ); 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | <?php 2 | /** 3 | * Base Test Case class. 4 | * 5 | * @author Iron Bound Designs 6 | * @since 1.0 7 | * @copyright 2017 (c) Iron Bound Designs. 8 | * @license GPLv2 9 | */ 10 | 11 | namespace IronBound\WP_REST_API\SchemaValidator\Tests; 12 | 13 | /** 14 | * Class TestCase 15 | * 16 | * @package IronBound\WP_REST_API\SchemaValidator\Tests 17 | */ 18 | abstract class TestCase extends \WP_UnitTestCase { 19 | 20 | /** @var \WP_REST_Server */ 21 | protected $server; 22 | 23 | public function setUp() { 24 | parent::setUp(); 25 | /** @var \WP_REST_Server $wp_rest_server */ 26 | global $wp_rest_server; 27 | $this->server = $wp_rest_server = new \Spy_REST_Server; 28 | do_action( 'rest_api_init' ); 29 | } 30 | 31 | public function tearDown() { 32 | parent::tearDown(); 33 | /** @var \WP_REST_Server $wp_rest_server */ 34 | global $wp_rest_server; 35 | $wp_rest_server = null; 36 | } 37 | 38 | protected function assertErrorResponse( $code, $response, $status = null ) { 39 | 40 | if ( $response instanceof \WP_REST_Response ) { 41 | $this->assertTrue( $response->is_error(), print_r( $response->get_data(), true ) ); 42 | $response = $response->as_error(); 43 | } 44 | 45 | $this->assertInstanceOf( 'WP_Error', $response ); 46 | $this->assertEquals( $code, $response->get_error_code() ); 47 | 48 | if ( null !== $status ) { 49 | $data = $response->get_error_data(); 50 | $this->assertArrayHasKey( 'status', $data ); 51 | $this->assertEquals( $status, $data['status'] ); 52 | } 53 | } 54 | 55 | /** 56 | * Retrieves an array of endpoint arguments from the item schema for the controller. 57 | * 58 | * @since 4.7.0 59 | * @access public 60 | * 61 | * @param string $method Optional. HTTP method of the request. The arguments for `CREATABLE` requests are 62 | * checked for required values and may fall-back to a given default, this is not done 63 | * on `EDITABLE` requests. Default WP_REST_Server::CREATABLE. 64 | * 65 | * @return array Endpoint arguments. 66 | */ 67 | protected function get_endpoint_args_for_item_schema( $schema, $method = \WP_REST_Server::CREATABLE ) { 68 | 69 | $schema_properties = ! empty( $schema['properties'] ) ? $schema['properties'] : array(); 70 | $endpoint_args = array(); 71 | 72 | foreach ( $schema_properties as $field_id => $params ) { 73 | 74 | // Arguments specified as `readonly` are not allowed to be set. 75 | if ( ! empty( $params['readonly'] ) ) { 76 | continue; 77 | } 78 | 79 | $endpoint_args[ $field_id ] = array( 80 | 'validate_callback' => 'rest_validate_request_arg', 81 | 'sanitize_callback' => 'rest_sanitize_request_arg', 82 | ); 83 | 84 | if ( isset( $params['description'] ) ) { 85 | $endpoint_args[ $field_id ]['description'] = $params['description']; 86 | } 87 | 88 | if ( \WP_REST_Server::CREATABLE === $method && isset( $params['default'] ) ) { 89 | $endpoint_args[ $field_id ]['default'] = $params['default']; 90 | } 91 | 92 | if ( \WP_REST_Server::CREATABLE === $method && ! empty( $params['required'] ) ) { 93 | $endpoint_args[ $field_id ]['required'] = true; 94 | } 95 | 96 | foreach ( array( 'type', 'format', 'enum', 'items' ) as $schema_prop ) { 97 | if ( isset( $params[ $schema_prop ] ) ) { 98 | $endpoint_args[ $field_id ][ $schema_prop ] = $params[ $schema_prop ]; 99 | } 100 | } 101 | 102 | // Merge in any options provided by the schema property. 103 | if ( isset( $params['arg_options'] ) ) { 104 | 105 | // Only use required / default from arg_options on CREATABLE endpoints. 106 | if ( \WP_REST_Server::CREATABLE !== $method ) { 107 | $params['arg_options'] = array_diff_key( $params['arg_options'], array( 108 | 'required' => '', 109 | 'default' => '' 110 | ) ); 111 | } 112 | 113 | $endpoint_args[ $field_id ] = array_merge( $endpoint_args[ $field_id ], $params['arg_options'] ); 114 | } 115 | } 116 | 117 | return $endpoint_args; 118 | } 119 | } -------------------------------------------------------------------------------- /tests/TestMiddleware.php: -------------------------------------------------------------------------------- 1 | <?php 2 | /** 3 | * Test the middleware class. 4 | * 5 | * @author Iron Bound Designs 6 | * @since 1.0 7 | * @copyright 2017 (c) Iron Bound Designs. 8 | * @license GPLv2 9 | */ 10 | 11 | namespace IronBound\WP_REST_API\SchemaValidator\Tests; 12 | 13 | use IronBound\WP_REST_API\SchemaValidator\Middleware; 14 | 15 | /** 16 | * Class TestMiddleware 17 | * 18 | * @package IronBound\WP_REST_API\SchemaValidator\Tests 19 | */ 20 | class TestMiddleware extends TestCase { 21 | 22 | /** @var Middleware */ 23 | protected static $middleware; 24 | 25 | public static function setUpBeforeClass() { 26 | static::$middleware = new Middleware( 'test' ); 27 | 28 | return parent::setUpBeforeClass(); 29 | } 30 | 31 | public function setUp() { 32 | parent::setUp(); 33 | 34 | static::$middleware->add_shared_schema( $this->get_shared_schema() ); 35 | } 36 | 37 | public function tearDown() { 38 | parent::tearDown(); 39 | 40 | static::$middleware->deinitialize(); 41 | } 42 | 43 | public function test_register_route_with_single_method() { 44 | 45 | register_rest_route( 'test', 'simple', array( 46 | 'methods' => 'POST', 47 | 'callback' => function () { return new \WP_REST_Response( array( 'enum' => 'a' ) ); }, 48 | 'args' => $this->get_endpoint_args_for_item_schema( $this->get_schema(), 'POST' ), 49 | 'schema' => array( $this, 'get_schema' ), 50 | ) ); 51 | 52 | static::$middleware->initialize(); 53 | static::$middleware->load_schemas( rest_get_server() ); 54 | 55 | $request = \WP_REST_Request::from_url( rest_url( '/test/simple' ) ); 56 | $request->set_method( 'POST' ); 57 | $request->add_header( 'content-type', 'application/json' ); 58 | $request->set_body( wp_json_encode( array( 'enum' => 'd' ) ) ); 59 | 60 | $response = $this->server->dispatch( $request ); 61 | 62 | $this->assertErrorResponse( 'rest_invalid_param', $response ); 63 | } 64 | 65 | public function test_register_route_with_multiple_methods() { 66 | 67 | register_rest_route( 'test', 'simple', array( 68 | array( 69 | 'methods' => 'GET', 70 | 'callback' => function () { return new \WP_REST_Response( array( 'enum' => 'a' ) ); }, 71 | 'args' => $this->get_endpoint_args_for_item_schema( $this->get_schema(), 'GET' ), 72 | ), 73 | array( 74 | 'methods' => 'POST', 75 | 'callback' => function () { return new \WP_REST_Response( array( 'enum' => 'a' ) ); }, 76 | 'args' => $this->get_endpoint_args_for_item_schema( $this->get_schema(), 'POST' ), 77 | ), 78 | 'schema' => array( $this, 'get_schema' ), 79 | ) ); 80 | 81 | static::$middleware->initialize(); 82 | static::$middleware->load_schemas( rest_get_server() ); 83 | 84 | $request = \WP_REST_Request::from_url( rest_url( '/test/simple' ) ); 85 | $request->set_method( 'POST' ); 86 | $request->add_header( 'content-type', 'application/json' ); 87 | $request->set_body( wp_json_encode( array( 'enum' => 'd' ) ) ); 88 | 89 | $response = $this->server->dispatch( $request ); 90 | 91 | $this->assertErrorResponse( 'rest_invalid_param', $response ); 92 | } 93 | 94 | public function test_shared_schema() { 95 | register_rest_route( 'test', 'simple', array( 96 | 'methods' => 'POST', 97 | 'callback' => function () { return new \WP_REST_Response( array( 'enum' => 'a' ) ); }, 98 | 'args' => $this->get_endpoint_args_for_item_schema( $this->get_schema(), 'POST' ), 99 | 'schema' => array( $this, 'get_schema' ), 100 | ) ); 101 | 102 | static::$middleware->initialize(); 103 | static::$middleware->load_schemas( rest_get_server() ); 104 | 105 | $request = \WP_REST_Request::from_url( rest_url( '/test/simple' ) ); 106 | $request->set_method( 'POST' ); 107 | $request->add_header( 'content-type', 'application/json' ); 108 | $request->set_body( wp_json_encode( array( 'shared' => array( 'enum' => 5 ) ) ) ); 109 | 110 | $response = $this->server->dispatch( $request ); 111 | 112 | $this->assertErrorResponse( 'rest_invalid_param', $response ); 113 | } 114 | 115 | public function test_coercion() { 116 | 117 | register_rest_route( 'test', 'simple', array( 118 | 'methods' => 'POST', 119 | 'callback' => function () { return new \WP_REST_Response( array( 'enum' => 'a' ) ); }, 120 | 'args' => $this->get_endpoint_args_for_item_schema( $this->get_schema(), 'POST' ), 121 | 'schema' => array( $this, 'get_schema' ), 122 | ) ); 123 | 124 | static::$middleware->initialize(); 125 | static::$middleware->load_schemas( rest_get_server() ); 126 | 127 | $request = \WP_REST_Request::from_url( rest_url( '/test/simple' ) ); 128 | $request->set_method( 'POST' ); 129 | $request->add_header( 'content-type', 'application/json' ); 130 | $request->set_body( wp_json_encode( array( 131 | 'int' => '2' 132 | ) ) ); 133 | 134 | $response = $this->server->dispatch( $request ); 135 | 136 | $this->assertInternalType( 'integer', $request['int'] ); 137 | $this->assertEquals( 2, $request['int'] ); 138 | } 139 | 140 | public function test_variable_schema() { 141 | 142 | register_rest_route( 'test', 'simple', array( 143 | array( 144 | 'methods' => 'GET', 145 | 'callback' => function () { return new \WP_REST_Response( array( 'enum' => 'a' ) ); }, 146 | 'args' => $this->get_endpoint_args_for_item_schema( $this->get_schema(), 'GET' ), 147 | ), 148 | array( 149 | 'methods' => 'POST', 150 | 'callback' => function () { return new \WP_REST_Response( array( 'enum' => 'a' ) ); }, 151 | 'args' => $this->get_endpoint_args_for_item_schema( $this->get_post_schema(), 'POST' ), 152 | 'schema' => array( $this, 'get_post_schema' ), 153 | ), 154 | array( 155 | 'methods' => 'PUT', 156 | 'callback' => function () { return new \WP_REST_Response( array( 'enum' => 'a' ) ); }, 157 | 'args' => $this->get_endpoint_args_for_item_schema( $this->get_schema(), 'PUT' ), 158 | ), 159 | 'schema' => array( $this, 'get_schema' ), 160 | ) ); 161 | 162 | static::$middleware->initialize(); 163 | static::$middleware->load_schemas( rest_get_server() ); 164 | 165 | $request = \WP_REST_Request::from_url( rest_url( '/test/simple' ) ); 166 | $request->set_method( 'PUT' ); 167 | $request->add_header( 'content-type', 'application/json' ); 168 | $request->set_body( wp_json_encode( array( 'enum' => 'c' ) ) ); 169 | 170 | $response = $this->server->dispatch( $request ); 171 | $this->assertArrayHasKey( 'enum', $response->get_data() ); 172 | 173 | $request = \WP_REST_Request::from_url( rest_url( '/test/simple' ) ); 174 | $request->set_method( 'POST' ); 175 | $request->add_header( 'content-type', 'application/json' ); 176 | $request->set_body( wp_json_encode( array( 'enum' => 'c' ) ) ); 177 | 178 | $response = $this->server->dispatch( $request ); 179 | 180 | $this->assertErrorResponse( 'rest_invalid_param', $response ); 181 | } 182 | 183 | public function test_get_request() { 184 | 185 | register_rest_route( 'test', 'simple', array( 186 | 'methods' => 'GET', 187 | 'callback' => function () { return new \WP_REST_Response( array( 'enum' => 'a' ) ); }, 188 | 'args' => array( 189 | 'getParam' => array( 190 | 'type' => 'string', 191 | 'enum' => array( 'alice', 'bob', 'mallory' ) 192 | ), 193 | ), 194 | 'schema' => array( $this, 'get_schema' ), 195 | ) ); 196 | 197 | static::$middleware->initialize(); 198 | static::$middleware->load_schemas( rest_get_server() ); 199 | 200 | $request = \WP_REST_Request::from_url( rest_url( '/test/simple' ) ); 201 | $request->set_query_params( array( 'getParam' => 'eve' ) ); 202 | 203 | $response = $this->server->dispatch( $request ); 204 | 205 | $this->assertErrorResponse( 'rest_invalid_param', $response ); 206 | } 207 | 208 | public function test_get_request_without_schema_registered() { 209 | 210 | register_rest_route( 'test', 'simple', array( 211 | 'methods' => 'GET', 212 | 'callback' => function () { return new \WP_REST_Response( array( 'enum' => 'a' ) ); }, 213 | 'args' => array( 214 | 'getParam' => array( 215 | 'type' => 'string', 216 | 'enum' => array( 'alice', 'bob', 'mallory' ) 217 | ), 218 | ), 219 | ) ); 220 | 221 | static::$middleware->initialize(); 222 | static::$middleware->load_schemas( rest_get_server() ); 223 | 224 | $request = \WP_REST_Request::from_url( rest_url( '/test/simple' ) ); 225 | $request->set_query_params( array( 'getParam' => 'eve' ) ); 226 | 227 | $response = $this->server->dispatch( $request ); 228 | 229 | $this->assertErrorResponse( 'rest_invalid_param', $response ); 230 | } 231 | 232 | public function test_delete_request() { 233 | 234 | register_rest_route( 'test', 'simple', array( 235 | 'methods' => 'DELETE', 236 | 'callback' => function () { return new \WP_REST_Response( null, 204 ); }, 237 | 'args' => array( 238 | 'getParam' => array( 239 | 'type' => 'string', 240 | 'enum' => array( 'alice', 'bob', 'mallory' ) 241 | ), 242 | ), 243 | 'schema' => array( $this, 'get_schema' ), 244 | ) ); 245 | 246 | static::$middleware->initialize(); 247 | static::$middleware->load_schemas( rest_get_server() ); 248 | 249 | $request = \WP_REST_Request::from_url( rest_url( '/test/simple' ) ); 250 | $request->set_method( 'DELETE' ); 251 | $request->set_query_params( array( 'getParam' => 'eve' ) ); 252 | 253 | $response = $this->server->dispatch( $request ); 254 | 255 | $this->assertErrorResponse( 'rest_invalid_param', $response ); 256 | } 257 | 258 | public function test_validate_callback_is_called() { 259 | 260 | register_rest_route( 'test', 'simple', array( 261 | 'methods' => 'POST', 262 | 'callback' => function () { return new \WP_REST_Response( array( 'enum' => 'a' ) ); }, 263 | 'args' => $this->get_endpoint_args_for_item_schema( $this->get_schema(), 'POST' ), 264 | 'schema' => array( $this, 'get_schema' ), 265 | ) ); 266 | 267 | static::$middleware->initialize(); 268 | static::$middleware->load_schemas( rest_get_server() ); 269 | 270 | $request = \WP_REST_Request::from_url( rest_url( '/test/simple' ) ); 271 | $request->set_method( 'POST' ); 272 | $request->add_header( 'content-type', 'application/json' ); 273 | $request->set_body( wp_json_encode( array( 'validateCallback' => 'valid' ) ) ); 274 | 275 | $response = $this->server->dispatch( $request ); 276 | $this->assertArrayHasKey( 'enum', $response->get_data() ); 277 | 278 | $request = \WP_REST_Request::from_url( rest_url( '/test/simple' ) ); 279 | $request->set_method( 'POST' ); 280 | $request->add_header( 'content-type', 'application/json' ); 281 | $request->set_body( wp_json_encode( array( 'validateCallback' => 'invalid' ) ) ); 282 | 283 | $response = $this->server->dispatch( $request ); 284 | 285 | $this->assertErrorResponse( 'rest_invalid_param', $response ); 286 | } 287 | 288 | public function test_required() { 289 | 290 | register_rest_route( 'test', 'simple', array( 291 | 'methods' => 'POST', 292 | 'callback' => function () { return new \WP_REST_Response( array( 'enum' => 'a' ) ); }, 293 | 'args' => $this->get_endpoint_args_for_item_schema( $this->get_schema_with_required(), 'POST' ), 294 | 'schema' => array( $this, 'get_schema_with_required' ), 295 | ) ); 296 | 297 | static::$middleware->initialize(); 298 | static::$middleware->load_schemas( rest_get_server() ); 299 | 300 | $request = \WP_REST_Request::from_url( rest_url( '/test/simple' ) ); 301 | $request->set_method( 'POST' ); 302 | $request->add_header( 'content-type', 'application/json' ); 303 | $request->set_body( wp_json_encode( array( 'unneeded' => 'hi' ) ) ); 304 | 305 | $response = $this->server->dispatch( $request ); 306 | 307 | $this->assertErrorResponse( 'rest_missing_callback_param', $response ); 308 | } 309 | 310 | public function test_core_validators_are_removed_by_default() { 311 | 312 | register_rest_route( 'test', 'simple', array( 313 | 'methods' => 'POST', 314 | 'callback' => function () { return new \WP_REST_Response( array( 'enum' => 'a' ) ); }, 315 | 'args' => $this->get_endpoint_args_for_item_schema( $this->get_schema(), 'POST' ), 316 | 'schema' => array( $this, 'get_schema' ), 317 | ) ); 318 | 319 | static::$middleware->initialize(); 320 | static::$middleware->load_schemas( rest_get_server() ); 321 | 322 | $request = \WP_REST_Request::from_url( rest_url( '/test/simple' ) ); 323 | $request->set_method( 'POST' ); 324 | $request->add_header( 'content-type', 'application/json' ); 325 | $request->set_body( wp_json_encode( array( 'enum' => 'b' ) ) ); 326 | 327 | $this->server->dispatch( $request ); 328 | 329 | $attributes = $request->get_attributes(); 330 | 331 | $this->assertArrayHasKey( 'args', $attributes ); 332 | 333 | foreach ( $attributes['args'] as $key => $arg ) { 334 | 335 | if ( $key === 'validateCallback' ) { 336 | continue; 337 | } 338 | 339 | $this->assertArrayHasKey( 'validate_callback', $arg, "Validate callback exists for {$key}." ); 340 | $this->assertFalse( $arg['validate_callback'] ); 341 | 342 | $this->assertArrayHasKey( 'sanitize_callback', $arg, "Sanitize callback exists for {$key}." ); 343 | $this->assertFalse( $arg['sanitize_callback'] ); 344 | } 345 | } 346 | 347 | public function test_get_schema_route() { 348 | 349 | register_rest_route( 'test', 'simple', array( 350 | 'methods' => 'POST', 351 | 'callback' => function () { return new \WP_REST_Response( array( 'enum' => 'a' ) ); }, 352 | 'args' => $this->get_endpoint_args_for_item_schema( $this->get_schema(), 'POST' ), 353 | 'schema' => array( $this, 'get_schema' ), 354 | ) ); 355 | 356 | static::$middleware->initialize(); 357 | static::$middleware->load_schemas( rest_get_server() ); 358 | 359 | $request = \WP_REST_Request::from_url( static::$middleware->get_url_for_schema( 'test' ) ); 360 | 361 | $response = $this->server->dispatch( $request ); 362 | $schema = $response->get_data(); 363 | 364 | $this->assertInternalType( 'array', $schema ); 365 | $this->assertNotEmpty( $schema ); 366 | $this->assertArrayHasKey( 'title', $schema ); 367 | $this->assertEquals( 'test', $schema['title'] ); 368 | $this->assertArrayHasKey( 'properties', $schema ); 369 | $this->assertArrayHasKey( 'enum', $schema['properties'] ); 370 | $this->assertArrayHasKey( 'validateCallback', $schema['properties'] ); 371 | $this->assertArrayNotHasKey( 'arg_options', $schema['properties']['validateCallback'] ); 372 | } 373 | 374 | public function test_default_is_applied() { 375 | 376 | register_rest_route( 'test', 'simple', array( 377 | 'methods' => 'POST', 378 | 'callback' => function () { return new \WP_REST_Response( array( 'enum' => 'a' ) ); }, 379 | 'args' => $this->get_endpoint_args_for_item_schema( $this->get_schema(), 'POST' ), 380 | 'schema' => array( $this, 'get_schema' ), 381 | ) ); 382 | 383 | static::$middleware->initialize(); 384 | static::$middleware->load_schemas( rest_get_server() ); 385 | 386 | $request = \WP_REST_Request::from_url( rest_url( '/test/simple' ) ); 387 | $request->set_method( 'POST' ); 388 | $request->add_header( 'content-type', 'application/json' ); 389 | $request->set_body( wp_json_encode( array( 'enum' => 'a' ) ) ); 390 | 391 | $this->server->dispatch( $request ); 392 | $json = $request->get_json_params(); 393 | $this->assertArrayHasKey( 'withDefault', $json ); 394 | $this->assertEquals( 'hi', $json['withDefault'] ); 395 | } 396 | 397 | public function test_invalid_readonly_properties_do_not_error() { 398 | 399 | register_rest_route( 'test', 'simple', array( 400 | 'methods' => 'POST', 401 | 'callback' => function () { return new \WP_REST_Response( array( 'enum' => 'a' ) ); }, 402 | 'args' => $this->get_endpoint_args_for_item_schema( $this->get_schema(), 'POST' ), 403 | 'schema' => array( $this, 'get_schema' ), 404 | ) ); 405 | 406 | static::$middleware->initialize(); 407 | static::$middleware->load_schemas( rest_get_server() ); 408 | 409 | $request = \WP_REST_Request::from_url( rest_url( '/test/simple' ) ); 410 | $request->set_method( 'POST' ); 411 | $request->add_header( 'content-type', 'application/json' ); 412 | $request->set_body( wp_json_encode( array( 'readOnly' => 'd' ) ) ); 413 | 414 | $response = $this->server->dispatch( $request ); 415 | $this->assertEquals( 200, $response->get_status() ); 416 | 417 | $params = $request->get_params(); 418 | $this->assertArrayNotHasKey( 'readOnly', $params, 'Read only property set to null.' ); 419 | } 420 | 421 | public function test_invalid_createonly_properties_do_not_error_on_non_create_requests() { 422 | 423 | register_rest_route( 'test', 'simple', array( 424 | array( 425 | 'methods' => 'POST', 426 | 'callback' => function () { return new \WP_REST_Response( array( 'enum' => 'a' ) ); }, 427 | 'args' => $this->get_endpoint_args_for_item_schema( $this->get_schema(), 'POST' ), 428 | ), 429 | array( 430 | 'methods' => 'PUT', 431 | 'callback' => function () { return new \WP_REST_Response( array( 'enum' => 'a' ) ); }, 432 | 'args' => $this->get_endpoint_args_for_item_schema( $this->get_schema(), 'PUT' ), 433 | ), 434 | 'schema' => array( $this, 'get_schema' ), 435 | ) ); 436 | 437 | static::$middleware->initialize(); 438 | static::$middleware->load_schemas( rest_get_server() ); 439 | 440 | $request = \WP_REST_Request::from_url( rest_url( '/test/simple' ) ); 441 | $request->set_method( 'PUT' ); 442 | $request->add_header( 'content-type', 'application/json' ); 443 | $request->set_body( wp_json_encode( array( 'createOnly' => 'd' ) ) ); 444 | 445 | $response = $this->server->dispatch( $request ); 446 | $this->assertEquals( 200, $response->get_status() ); 447 | 448 | $params = $request->get_params(); 449 | $this->assertArrayNotHasKey( 'createOnly', $params, 'Create only property set to null.' ); 450 | } 451 | 452 | public function test_invalid_createonly_properties_error_on_create_requests() { 453 | 454 | register_rest_route( 'test', 'simple', array( 455 | 'methods' => 'POST', 456 | 'callback' => function () { return new \WP_REST_Response( array( 'enum' => 'a' ) ); }, 457 | 'args' => $this->get_endpoint_args_for_item_schema( $this->get_schema(), 'POST' ), 458 | 'schema' => array( $this, 'get_schema' ), 459 | ) ); 460 | 461 | static::$middleware->initialize(); 462 | static::$middleware->load_schemas( rest_get_server() ); 463 | 464 | $request = \WP_REST_Request::from_url( rest_url( '/test/simple' ) ); 465 | $request->set_method( 'POST' ); 466 | $request->add_header( 'content-type', 'application/json' ); 467 | $request->set_body( wp_json_encode( array( 'createOnly' => 'd' ) ) ); 468 | 469 | $response = $this->server->dispatch( $request ); 470 | $this->assertErrorResponse( 'rest_invalid_param', $response ); 471 | } 472 | 473 | public function test_html_format_uses_basic_tags_if_none_specified() { 474 | 475 | $schema = array( 476 | '$schema' => 'http://json-schema.org/schema#', 477 | 'title' => 'html', 478 | 'type' => 'object', 479 | 'properties' => array( 480 | 'html' => array( 481 | 'type' => 'string', 482 | 'format' => 'html', 483 | ) 484 | ) 485 | ); 486 | 487 | register_rest_route( 'test', 'simple', array( 488 | 'methods' => 'POST', 489 | 'callback' => function () { return new \WP_REST_Response(); }, 490 | 'args' => $this->get_endpoint_args_for_item_schema( $schema, 'POST' ), 491 | 'schema' => function () use ( $schema ) { return $schema; } 492 | ) ); 493 | 494 | static::$middleware->initialize(); 495 | static::$middleware->load_schemas( rest_get_server() ); 496 | 497 | $request = \WP_REST_Request::from_url( rest_url( '/test/simple' ) ); 498 | $request->set_method( 'POST' ); 499 | $request->add_header( 'content-type', 'application/json' ); 500 | $request->set_body( wp_json_encode( array( 'html' => '<div>My Text</div>' ) ) ); 501 | 502 | $response = $this->server->dispatch( $request ); 503 | $this->assertErrorResponse( 'rest_invalid_param', $response ); 504 | } 505 | 506 | public function test_html_format_uses_basic_tags_if_none_specified_valid_request() { 507 | 508 | $schema = array( 509 | '$schema' => 'http://json-schema.org/schema#', 510 | 'title' => 'html', 511 | 'type' => 'object', 512 | 'properties' => array( 513 | 'html' => array( 514 | 'type' => 'string', 515 | 'format' => 'html', 516 | ) 517 | ) 518 | ); 519 | 520 | register_rest_route( 'test', 'simple', array( 521 | 'methods' => 'POST', 522 | 'callback' => function () { return new \WP_REST_Response(); }, 523 | 'args' => $this->get_endpoint_args_for_item_schema( $schema, 'POST' ), 524 | 'schema' => function () use ( $schema ) { return $schema; } 525 | ) ); 526 | 527 | static::$middleware->initialize(); 528 | static::$middleware->load_schemas( rest_get_server() ); 529 | 530 | $request = \WP_REST_Request::from_url( rest_url( '/test/simple' ) ); 531 | $request->set_method( 'POST' ); 532 | $request->add_header( 'content-type', 'application/json' ); 533 | $request->set_body( wp_json_encode( array( 'html' => '<strong>My Text</strong>' ) ) ); 534 | 535 | $response = $this->server->dispatch( $request ); 536 | $this->assertEquals( 200, $response->get_status() ); 537 | } 538 | 539 | public function test_html_format_specify_tags() { 540 | 541 | $schema = array( 542 | '$schema' => 'http://json-schema.org/schema#', 543 | 'title' => 'html', 544 | 'type' => 'object', 545 | 'properties' => array( 546 | 'html' => array( 547 | 'type' => 'string', 548 | 'format' => 'html', 549 | 'formatAllowedHtml' => array( 'a' ) 550 | ) 551 | ) 552 | ); 553 | 554 | register_rest_route( 'test', 'simple', array( 555 | 'methods' => 'POST', 556 | 'callback' => function () { return new \WP_REST_Response(); }, 557 | 'args' => $this->get_endpoint_args_for_item_schema( $schema, 'POST' ), 558 | 'schema' => function () use ( $schema ) { return $schema; } 559 | ) ); 560 | 561 | static::$middleware->initialize(); 562 | static::$middleware->load_schemas( rest_get_server() ); 563 | 564 | $request = \WP_REST_Request::from_url( rest_url( '/test/simple' ) ); 565 | $request->set_method( 'POST' ); 566 | $request->add_header( 'content-type', 'application/json' ); 567 | $request->set_body( wp_json_encode( array( 'html' => '<strong>My Text</strong>' ) ) ); 568 | 569 | $response = $this->server->dispatch( $request ); 570 | $this->assertErrorResponse( 'rest_invalid_param', $response ); 571 | } 572 | 573 | public function test_html_format_specify_tags_valid_request() { 574 | 575 | $schema = array( 576 | '$schema' => 'http://json-schema.org/schema#', 577 | 'title' => 'html', 578 | 'type' => 'object', 579 | 'properties' => array( 580 | 'html' => array( 581 | 'type' => 'string', 582 | 'format' => 'html', 583 | 'formatAllowedHtml' => array( 'div' ) 584 | ) 585 | ) 586 | ); 587 | 588 | register_rest_route( 'test', 'simple', array( 589 | 'methods' => 'POST', 590 | 'callback' => function () { return new \WP_REST_Response(); }, 591 | 'args' => $this->get_endpoint_args_for_item_schema( $schema, 'POST' ), 592 | 'schema' => function () use ( $schema ) { return $schema; } 593 | ) ); 594 | 595 | static::$middleware->initialize(); 596 | static::$middleware->load_schemas( rest_get_server() ); 597 | 598 | $request = \WP_REST_Request::from_url( rest_url( '/test/simple' ) ); 599 | $request->set_method( 'POST' ); 600 | $request->add_header( 'content-type', 'application/json' ); 601 | $request->set_body( wp_json_encode( array( 'html' => '<div>My Text</div>' ) ) ); 602 | 603 | $response = $this->server->dispatch( $request ); 604 | $this->assertEquals( 200, $response->get_status() ); 605 | } 606 | 607 | public function get_schema() { 608 | return array( 609 | '$schema' => 'http://json-schema.org/schema#', 610 | 'title' => 'test', 611 | 'type' => 'object', 612 | 'properties' => array( 613 | 'enum' => array( 614 | 'type' => 'string', 615 | 'enum' => array( 'a', 'b', 'c' ) 616 | ), 617 | 'int' => array( 618 | 'type' => 'integer' 619 | ), 620 | 'shared' => array( 621 | '$ref' => static::$middleware->get_url_for_schema( 'shared' ), 622 | ), 623 | 'withDefault' => array( 624 | 'type' => 'string', 625 | 'default' => 'hi' 626 | ), 627 | 'readOnly' => array( 628 | 'type' => 'string', 629 | 'enum' => array( 'a', 'b', 'c' ), 630 | 'readonly' => true, 631 | ), 632 | 'createOnly' => array( 633 | 'type' => 'string', 634 | 'enum' => array( 'a', 'b', 'c' ), 635 | 'createonly' => true, 636 | ), 637 | 'validateCallback' => array( 638 | 'type' => 'string', 639 | 'arg_options' => array( 640 | 'validate_callback' => function ( $value ) { 641 | return $value === 'valid'; 642 | } 643 | ), 644 | ) 645 | ), 646 | ); 647 | } 648 | 649 | public function get_post_schema() { 650 | return array( 651 | '$schema' => 'http://json-schema.org/schema#', 652 | 'title' => 'test', 653 | 'type' => 'object', 654 | 'properties' => array( 655 | 'enum' => array( 656 | 'type' => 'string', 657 | 'enum' => array( 'a', 'b' ) 658 | ), 659 | ), 660 | ); 661 | } 662 | 663 | public function get_schema_with_required() { 664 | return array( 665 | '$schema' => 'http://json-schema.org/schema#', 666 | 'title' => 'test', 667 | 'type' => 'object', 668 | 'properties' => array( 669 | 'needed' => array( 670 | 'type' => 'string', 671 | 'required' => true, 672 | ), 673 | 'unneeded' => array( 674 | 'type' => 'string', 675 | 'required' => false, 676 | ), 677 | ), 678 | ); 679 | } 680 | 681 | protected function get_shared_schema() { 682 | return array( 683 | '$schema' => 'http://json-schema.org/schema#', 684 | 'title' => 'shared', 685 | 'type' => 'object', 686 | 'properties' => array( 687 | 'enum' => array( 688 | 'type' => 'integer', 689 | 'enum' => array( 1, 2, 3 ), 690 | ) 691 | ) 692 | ); 693 | } 694 | } -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | <?php 2 | /** 3 | * Test bootstrap file. 4 | * 5 | * @author Iron Bound Designs 6 | * @since 1.0 7 | * @copyright 2017 (c) Iron Bound Designs. 8 | * @license GPLv2 9 | */ 10 | 11 | /** 12 | * Determine where the WP test suite lives. 13 | * 14 | * Support for: 15 | * 1. `WP_DEVELOP_DIR` environment variable, which points to a checkout 16 | * of the develop.svn.wordpress.org repository (this is recommended) 17 | * 2. `WP_TESTS_DIR` environment variable, which points to a checkout 18 | * 3. `WP_ROOT_DIR` environment variable, which points to a checkout 19 | * 4. Plugin installed inside of WordPress.org developer checkout 20 | * 5. Tests checked out to /tmp 21 | */ 22 | if ( false !== getenv( 'WP_DEVELOP_DIR' ) ) { 23 | $test_root = getenv( 'WP_DEVELOP_DIR' ) . '/tests/phpunit'; 24 | } else if ( false !== getenv( 'WP_TESTS_DIR' ) ) { 25 | $test_root = getenv( 'WP_TESTS_DIR' ); 26 | } else if ( false !== getenv( 'WP_ROOT_DIR' ) ) { 27 | $test_root = getenv( 'WP_ROOT_DIR' ) . '/tests/phpunit'; 28 | } else if ( file_exists( '../../../../tests/phpunit/includes/bootstrap.php' ) ) { 29 | $test_root = '../../../../tests/phpunit'; 30 | } else if ( file_exists( '/tmp/wordpress-tests-lib/includes/bootstrap.php' ) ) { 31 | $test_root = '/tmp/wordpress-tests-lib'; 32 | } 33 | 34 | if ( getenv( 'SAVEQUERIES' ) && ! defined( 'SAVEQUERIES' ) ) { 35 | define( 'SAVEQUERIES', true ); 36 | } 37 | 38 | require_once $test_root . '/includes/functions.php'; 39 | 40 | $_SERVER['REMOTE_ADDR'] = '127.0.0.1'; 41 | 42 | require_once __DIR__ . '/../vendor/autoload.php'; 43 | 44 | ini_set( 'xdebug.max_nesting_level', 250 ); 45 | 46 | require $test_root . '/includes/bootstrap.php'; --------------------------------------------------------------------------------