├── .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 |
3 |
4 |
5 |
6 | ./tests/
7 |
8 |
9 |
10 |
11 | ./src
12 |
13 |
--------------------------------------------------------------------------------
/src/FormatConstraint.php:
--------------------------------------------------------------------------------
1 | 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 | 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 | [ '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\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 | 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 | 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 | 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' => 'My Text
' ) ) );
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' => 'My Text' ) ) );
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' => 'My Text' ) ) );
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' => 'My Text
' ) ) );
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 |