├── .gitignore
├── phpunit
├── data
│ └── canola.jpg
├── includes
│ ├── spy-rest-server.php
│ └── testcase-rest-api.php
├── bootstrap.php
└── tests
│ ├── rest-api.php
│ └── rest-api
│ ├── rest-request.php
│ └── rest-server.php
├── wp-includes
├── http.php
├── functions.php
├── filters.php
├── compat.php
├── class-wp-http-response.php
├── rest-api
│ ├── class-wp-rest-response.php
│ ├── class-wp-rest-request.php
│ └── class-wp-rest-server.php
└── rest-api.php
├── package.json
├── README.md
├── composer.json
├── phpcs.ruleset.xml
├── phpunit.xml.dist
├── multisite.xml
├── codecoverage.xml
├── .scrutinizer.yml
├── bin
└── sync-core.php
├── Gruntfile.js
├── .travis.yml
├── rest-api.php
└── license.txt
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | composer.lock
3 | node_modules
4 | vendor
5 |
--------------------------------------------------------------------------------
/phpunit/data/canola.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/WP-API/api-core/HEAD/phpunit/data/canola.jpg
--------------------------------------------------------------------------------
/wp-includes/http.php:
--------------------------------------------------------------------------------
1 | ",
10 | "devDependencies": {
11 | "grunt": "^0.4.5",
12 | "grunt-phpcs": "^0.4.0",
13 | "phplint": "^1.2.0"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # WordPress REST API Core
2 |
3 | This repository holds the infrastructure of the WordPress REST API.
4 |
5 | Please note, neither issues nor pull requests should be filed here. Issues should be filed [on the main API repository](https://github.com/WP-API/WP-API) instead.
6 |
7 | We're currently preparing the API for [WordPress core merge](https://core.trac.wordpress.org/ticket/33982), and this is our repository for creating patches for that.
8 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "wp-api/api-core",
3 | "description": "Compatibility repo for WP-API infrastructure components.",
4 | "homepage": "http://wp-api.org/",
5 | "license": "GPL2+",
6 | "authors": [
7 | {
8 | "name": "WP-API Team",
9 | "homepage": "http://wp-api.org/"
10 | }
11 | ],
12 | "support": {
13 | "issues": "https://github.com/WP-API/WP-API/issues"
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/phpcs.ruleset.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | Sniffs for the coding standards of the WP-API plugin
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/phpunit/includes/spy-rest-server.php:
--------------------------------------------------------------------------------
1 | endpoints;
11 | }
12 |
13 | /**
14 | * Allow calling protected methods from tests
15 | *
16 | * @param string $method Method to call
17 | * @param array $args Arguments to pass to the method
18 | * @return mixed
19 | */
20 | public function __call( $method, $args ) {
21 | return call_user_func_array( array( $this, $method ), $args );
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/phpunit/includes/testcase-rest-api.php:
--------------------------------------------------------------------------------
1 | as_error();
8 | }
9 |
10 | $this->assertInstanceOf( 'WP_Error', $response );
11 | $this->assertEquals( $code, $response->get_error_code() );
12 |
13 | if ( null !== $status ) {
14 | $data = $response->get_error_data();
15 | $this->assertArrayHasKey( 'status', $data );
16 | $this->assertEquals( $status, $data['status'] );
17 | }
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 | phpunit
13 |
14 |
15 |
16 |
17 | .
18 |
19 |
20 | ./lib
21 | ./plugin.php
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/multisite.xml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | tests
16 |
17 |
18 |
19 |
20 |
21 | .
22 |
23 |
24 | ./lib
25 | ./plugin.php
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/codecoverage.xml:
--------------------------------------------------------------------------------
1 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | tests
16 |
17 |
18 |
19 |
20 | .
21 |
22 |
23 | ./lib
24 | ./plugin.php
25 |
26 |
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/.scrutinizer.yml:
--------------------------------------------------------------------------------
1 | # Scrutinizer Configuration File
2 |
3 | tools:
4 | external_code_coverage: false
5 |
6 | php_sim:
7 | enabled: true
8 | min_mass: 50
9 |
10 | php_pdepend:
11 | enabled: true
12 | configuration_file: null
13 | suffixes:
14 | - php
15 | excluded_dirs: { }
16 |
17 | php_analyzer:
18 | enabled: true
19 | extensions:
20 | - php
21 | dependency_paths: { }
22 | path_configs: { }
23 |
24 | php_changetracking:
25 | enabled: true
26 | bug_patterns:
27 | - '\bfix(?:es|ed)?\b'
28 | feature_patterns:
29 | - '\badd(?:s|ed)?\b'
30 | - '\bimplement(?:s|ed)?\b'
31 |
32 | php_hhvm:
33 | enabled: true
34 | command: hhvm
35 | extensions:
36 | - php
37 | path_configs: { }
38 |
39 | before_commands: { }
40 | after_commands: { }
41 | artifacts: { }
42 | build_failure_conditions: { }
43 |
--------------------------------------------------------------------------------
/wp-includes/functions.php:
--------------------------------------------------------------------------------
1 | \n * Copyright (c) <%= grunt.template.today("yyyy") %>\n * This file is generated automatically. Do not edit.\n */\n';
5 | require('phplint').gruntPlugin(grunt);
6 | // Project configuration
7 | grunt.initConfig( {
8 |
9 | pkg: grunt.file.readJSON( 'package.json' ),
10 |
11 | phpcs: {
12 | plugin: {
13 | src: './'
14 | },
15 | options: {
16 | bin: "vendor/bin/phpcs --extensions=php --ignore=\"*/vendor/*,*/node_modules/*\"",
17 | standard: "phpcs.ruleset.xml"
18 | }
19 | },
20 |
21 | phplint: {
22 | options: {
23 | limit: 10,
24 | stdout: true,
25 | stderr: true
26 | },
27 | files: ['lib/**/*.php', 'tests/*.php', '*.php']
28 | },
29 |
30 | phpunit: {
31 | 'default': {
32 | cmd: 'phpunit',
33 | args: ['-c', 'phpunit.xml.dist']
34 | },
35 | 'multisite': {
36 | cmd: 'phpunit',
37 | args: ['-c', 'multisite.xml']
38 | },
39 | 'codecoverage': {
40 | cmd: 'phpunit',
41 | args: ['-c', 'codecoverage.xml']
42 | }
43 | }
44 |
45 | } );
46 | grunt.loadNpmTasks( 'grunt-phpcs' );
47 |
48 | // Testing tasks.
49 | grunt.registerMultiTask('phpunit', 'Runs PHPUnit tests, including the ajax, external-http, and multisite tests.', function() {
50 | grunt.util.spawn({
51 | cmd: this.data.cmd,
52 | args: this.data.args,
53 | opts: {stdio: 'inherit'}
54 | }, this.async());
55 | });
56 |
57 | grunt.registerTask( 'test', [ 'phpcs', 'phplint', 'phpunit:default', 'phpunit:multisite' ] );
58 | grunt.util.linefeed = '\n';
59 |
60 | // Travis CI tasks.
61 | grunt.registerTask('travis:phpvalidate', 'Runs PHPUnit Travis CI PHP code tasks.', [
62 | 'phpcs',
63 | 'phplint'
64 | ] );
65 | grunt.registerTask('travis:phpunit', 'Runs PHPUnit Travis CI tasks.', [
66 | 'phpunit:default',
67 | 'phpunit:multisite'
68 | ] );
69 | grunt.registerTask('travis:codecoverage', 'Runs PHPUnit Travis CI Code Coverage task.', [
70 | 'phpunit:codecoverage',
71 | 'phpunit:multisite'
72 | ] );
73 | };
74 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | # Travis CI Configuration File
2 |
3 | # Tell Travis CI we're using PHP
4 | language: php
5 |
6 | sudo: false
7 |
8 | matrix:
9 | include:
10 | - php: 5.6
11 | env: WP_TRAVISCI=travis:phpvalidate
12 | - php: 5.6
13 | env: WP_TRAVISCI=travis:codecoverage
14 | - php: 5.5
15 | env: WP_TRAVISCI=travis:phpunit
16 | - php: 5.4
17 | env: WP_TRAVISCI=travis:phpunit
18 | - php: 5.3
19 | env: WP_TRAVISCI=travis:phpunit
20 | - php: 5.2
21 | env: WP_TRAVISCI=travis:phpunit
22 | - php: hhvm
23 | env: WP_TRAVISCI=travis:phpunit
24 | - php: 7.0
25 | env: WP_TRAVISCI=travis:phpunit
26 | allow_failures:
27 | - php: hhvm
28 | - php: 7.0
29 | fast_finish: true
30 |
31 | cache:
32 | directories:
33 | - vendor
34 | - $HOME/.composer/cache
35 | - node_modules
36 |
37 | before_install:
38 | # set up WP install
39 | - export WP_DEVELOP_DIR=/tmp/wordpress/
40 | - mkdir -p $WP_DEVELOP_DIR
41 | - git clone --depth=1 git://develop.git.wordpress.org/ $WP_DEVELOP_DIR
42 | # set up tests config
43 | - cd $WP_DEVELOP_DIR
44 | - echo $WP_DEVELOP_DIR
45 | - cp wp-tests-config-sample.php wp-tests-config.php
46 | - sed -i "s/youremptytestdbnamehere/wordpress_test/" wp-tests-config.php
47 | - sed -i "s/yourusernamehere/root/" wp-tests-config.php
48 | - sed -i "s/yourpasswordhere//" wp-tests-config.php
49 | # set up database
50 | - mysql -e 'CREATE DATABASE wordpress_test;' -uroot
51 | # prepare for running the tests
52 | - cd $TRAVIS_BUILD_DIR
53 | - npm install -g npm
54 | - npm install -g grunt-cli
55 | - npm install
56 | - node --version
57 | - npm --version
58 | - grunt --version
59 |
60 | before_script:
61 | # Setup Coveralls
62 | - |
63 | if [[ "$WP_TRAVISCI" == "travis:phpvalidate" ]] ; then
64 | composer self-update
65 | composer install --no-interaction
66 | fi
67 | # Setup Coveralls
68 | - |
69 | if [[ "$WP_TRAVISCI" == "travis:codecoverage" ]] ; then
70 | composer self-update
71 | composer install --no-interaction
72 | fi
73 |
74 | script:
75 | - grunt $WP_TRAVISCI
76 |
77 | after_script:
78 | # Push coverage off to Codecov
79 | - |
80 | if [[ "$WP_TRAVISCI" == "travis:codecoverage" ]] ; then
81 | bash <(curl -s https://codecov.io/bash)
82 | fi
83 |
84 | git:
85 | depth: 1
86 |
--------------------------------------------------------------------------------
/wp-includes/compat.php:
--------------------------------------------------------------------------------
1 | data = $data;
56 | $this->set_status( $status );
57 | $this->set_headers( $headers );
58 | }
59 |
60 | /**
61 | * Retrieves headers associated with the response.
62 | *
63 | * @since 4.4.0
64 | * @access public
65 | *
66 | * @return array Map of header name to header value.
67 | */
68 | public function get_headers() {
69 | return $this->headers;
70 | }
71 |
72 | /**
73 | * Sets all header values.
74 | *
75 | * @since 4.4.0
76 | * @access public
77 | *
78 | * @param array $headers Map of header name to header value.
79 | */
80 | public function set_headers( $headers ) {
81 | $this->headers = $headers;
82 | }
83 |
84 | /**
85 | * Sets a single HTTP header.
86 | *
87 | * @since 4.4.0
88 | * @access public
89 | *
90 | * @param string $key Header name.
91 | * @param string $value Header value.
92 | * @param bool $replace Optional. Whether to replace an existing header of the same name.
93 | * Default true.
94 | */
95 | public function header( $key, $value, $replace = true ) {
96 | if ( $replace || ! isset( $this->headers[ $key ] ) ) {
97 | $this->headers[ $key ] = $value;
98 | } else {
99 | $this->headers[ $key ] .= ', ' . $value;
100 | }
101 | }
102 |
103 | /**
104 | * Retrieves the HTTP return code for the response.
105 | *
106 | * @since 4.4.0
107 | * @access public
108 | *
109 | * @return int The 3-digit HTTP status code.
110 | */
111 | public function get_status() {
112 | return $this->status;
113 | }
114 |
115 | /**
116 | * Sets the 3-digit HTTP status code.
117 | *
118 | * @since 4.4.0
119 | * @access public
120 | *
121 | * @param int $code HTTP status.
122 | */
123 | public function set_status( $code ) {
124 | $this->status = absint( $code );
125 | }
126 |
127 | /**
128 | * Retrieves the response data.
129 | *
130 | * @since 4.4.0
131 | * @access public
132 | *
133 | * @return mixed Response data.
134 | */
135 | public function get_data() {
136 | return $this->data;
137 | }
138 |
139 | /**
140 | * Sets the response data.
141 | *
142 | * @since 4.4.0
143 | * @access public
144 | *
145 | * @param mixed $data Response data.
146 | */
147 | public function set_data( $data ) {
148 | $this->data = $data;
149 | }
150 |
151 | /**
152 | * Retrieves the response data for JSON serialization.
153 | *
154 | * It is expected that in most implementations, this will return the same as get_data(),
155 | * however this may be different if you want to do custom JSON data handling.
156 | *
157 | * @since 4.4.0
158 | * @access public
159 | *
160 | * @return mixed Any JSON-serializable value.
161 | */
162 | public function jsonSerialize() {
163 | return $this->get_data();
164 | }
165 | }
166 |
--------------------------------------------------------------------------------
/wp-includes/rest-api/class-wp-rest-response.php:
--------------------------------------------------------------------------------
1 | links[ $rel ] ) ) {
64 | $this->links[ $rel ] = array();
65 | }
66 |
67 | if ( isset( $attributes['href'] ) ) {
68 | // Remove the href attribute, as it's used for the main URL.
69 | unset( $attributes['href'] );
70 | }
71 |
72 | $this->links[ $rel ][] = array(
73 | 'href' => $href,
74 | 'attributes' => $attributes,
75 | );
76 | }
77 |
78 | /**
79 | * Removes a link from the response.
80 | *
81 | * @since 4.4.0
82 | * @access public
83 | *
84 | * @param string $rel Link relation. Either an IANA registered type, or an absolute URL.
85 | * @param string $href Optional. Only remove links for the relation matching the given href.
86 | * Default null.
87 | */
88 | public function remove_link( $rel, $href = null ) {
89 | if ( ! isset( $this->links[ $rel ] ) ) {
90 | return;
91 | }
92 |
93 | if ( $href ) {
94 | $this->links[ $rel ] = wp_list_filter( $this->links[ $rel ], array( 'href' => $href ), 'NOT' );
95 | } else {
96 | $this->links[ $rel ] = array();
97 | }
98 |
99 | if ( ! $this->links[ $rel ] ) {
100 | unset( $this->links[ $rel ] );
101 | }
102 | }
103 |
104 | /**
105 | * Adds multiple links to the response.
106 | *
107 | * Link data should be an associative array with link relation as the key.
108 | * The value can either be an associative array of link attributes
109 | * (including `href` with the URL for the response), or a list of these
110 | * associative arrays.
111 | *
112 | * @since 4.4.0
113 | * @access public
114 | *
115 | * @param array $links Map of link relation to list of links.
116 | */
117 | public function add_links( $links ) {
118 | foreach ( $links as $rel => $set ) {
119 | // If it's a single link, wrap with an array for consistent handling.
120 | if ( isset( $set['href'] ) ) {
121 | $set = array( $set );
122 | }
123 |
124 | foreach ( $set as $attributes ) {
125 | $this->add_link( $rel, $attributes['href'], $attributes );
126 | }
127 | }
128 | }
129 |
130 | /**
131 | * Retrieves links for the response.
132 | *
133 | * @since 4.4.0
134 | * @access public
135 | *
136 | * @return array List of links.
137 | */
138 | public function get_links() {
139 | return $this->links;
140 | }
141 |
142 | /**
143 | * Sets a single link header.
144 | *
145 | * @internal The $rel parameter is first, as this looks nicer when sending multiple.
146 | *
147 | * @since 4.4.0
148 | * @access public
149 | *
150 | * @link http://tools.ietf.org/html/rfc5988
151 | * @link http://www.iana.org/assignments/link-relations/link-relations.xml
152 | *
153 | * @param string $rel Link relation. Either an IANA registered type, or an absolute URL.
154 | * @param string $link Target IRI for the link.
155 | * @param array $other Optional. Other parameters to send, as an assocative array.
156 | * Default empty array.
157 | */
158 | public function link_header( $rel, $link, $other = array() ) {
159 | $header = '<' . $link . '>; rel="' . $rel . '"';
160 |
161 | foreach ( $other as $key => $value ) {
162 | if ( 'title' === $key ) {
163 | $value = '"' . $value . '"';
164 | }
165 | $header .= '; ' . $key . '=' . $value;
166 | }
167 | $this->header( 'Link', $header, false );
168 | }
169 |
170 | /**
171 | * Retrieves the route that was used.
172 | *
173 | * @since 4.4.0
174 | * @access public
175 | *
176 | * @return string The matched route.
177 | */
178 | public function get_matched_route() {
179 | return $this->matched_route;
180 | }
181 |
182 | /**
183 | * Sets the route (regex for path) that caused the response.
184 | *
185 | * @since 4.4.0
186 | * @access public
187 | *
188 | * @param string $route Route name.
189 | */
190 | public function set_matched_route( $route ) {
191 | $this->matched_route = $route;
192 | }
193 |
194 | /**
195 | * Retrieves the handler that was used to generate the response.
196 | *
197 | * @since 4.4.0
198 | * @access public
199 | *
200 | * @return null|array The handler that was used to create the response.
201 | */
202 | public function get_matched_handler() {
203 | return $this->matched_handler;
204 | }
205 |
206 | /**
207 | * Retrieves the handler that was responsible for generating the response.
208 | *
209 | * @since 4.4.0
210 | * @access public
211 | *
212 | * @param array $handler The matched handler.
213 | */
214 | public function set_matched_handler( $handler ) {
215 | $this->matched_handler = $handler;
216 | }
217 |
218 | /**
219 | * Checks if the response is an error, i.e. >= 400 response code.
220 | *
221 | * @since 4.4.0
222 | * @access public
223 | *
224 | * @return bool Whether the response is an error.
225 | */
226 | public function is_error() {
227 | return $this->get_status() >= 400;
228 | }
229 |
230 | /**
231 | * Retrieves a WP_Error object from the response.
232 | *
233 | * @since 4.4.0
234 | * @access public
235 | *
236 | * @return WP_Error|null WP_Error or null on not an errored response.
237 | */
238 | public function as_error() {
239 | if ( ! $this->is_error() ) {
240 | return null;
241 | }
242 |
243 | $error = new WP_Error;
244 |
245 | if ( is_array( $this->get_data() ) ) {
246 | $data = $this->get_data();
247 | $error->add( $data['code'], $data['message'], $data['data'] );
248 | if ( ! empty( $data['additional_errors'] ) ) {
249 | foreach( $data['additional_errors'] as $err ) {
250 | $error->add( $err['code'], $err['message'], $err['data'] );
251 | }
252 | }
253 | } else {
254 | $error->add( $this->get_status(), '', array( 'status' => $this->get_status() ) );
255 | }
256 |
257 | return $error;
258 | }
259 | }
260 |
--------------------------------------------------------------------------------
/phpunit/tests/rest-api.php:
--------------------------------------------------------------------------------
1 | assertTrue( class_exists( 'WP_REST_Server' ) );
27 | }
28 |
29 | /**
30 | * The rest_api_init hook should have been registered with init, and should
31 | * have a default priority of 10.
32 | */
33 | function test_init_action_added() {
34 | $this->assertEquals( 10, has_action( 'init', 'rest_api_init' ) );
35 | }
36 |
37 | /**
38 | * Check that a single route is canonicalized.
39 | *
40 | * Ensures that single and multiple routes are handled correctly.
41 | */
42 | public function test_route_canonicalized() {
43 | register_rest_route( 'test-ns', '/test', array(
44 | 'methods' => array( 'GET' ),
45 | 'callback' => '__return_null',
46 | ) );
47 |
48 | // Check the route was registered correctly.
49 | $endpoints = $GLOBALS['wp_rest_server']->get_raw_endpoint_data();
50 | $this->assertArrayHasKey( '/test-ns/test', $endpoints );
51 |
52 | // Check the route was wrapped in an array.
53 | $endpoint = $endpoints['/test-ns/test'];
54 | $this->assertArrayNotHasKey( 'callback', $endpoint );
55 | $this->assertArrayHasKey( 'namespace', $endpoint );
56 | $this->assertEquals( 'test-ns', $endpoint['namespace'] );
57 |
58 | // Grab the filtered data.
59 | $filtered_endpoints = $GLOBALS['wp_rest_server']->get_routes();
60 | $this->assertArrayHasKey( '/test-ns/test', $filtered_endpoints );
61 | $endpoint = $filtered_endpoints['/test-ns/test'];
62 | $this->assertCount( 1, $endpoint );
63 | $this->assertArrayHasKey( 'callback', $endpoint[0] );
64 | $this->assertArrayHasKey( 'methods', $endpoint[0] );
65 | $this->assertArrayHasKey( 'args', $endpoint[0] );
66 | }
67 |
68 | /**
69 | * Check that a single route is canonicalized.
70 | *
71 | * Ensures that single and multiple routes are handled correctly.
72 | */
73 | public function test_route_canonicalized_multiple() {
74 | register_rest_route( 'test-ns', '/test', array(
75 | array(
76 | 'methods' => array( 'GET' ),
77 | 'callback' => '__return_null',
78 | ),
79 | array(
80 | 'methods' => array( 'POST' ),
81 | 'callback' => '__return_null',
82 | ),
83 | ) );
84 |
85 | // Check the route was registered correctly.
86 | $endpoints = $GLOBALS['wp_rest_server']->get_raw_endpoint_data();
87 | $this->assertArrayHasKey( '/test-ns/test', $endpoints );
88 |
89 | // Check the route was wrapped in an array.
90 | $endpoint = $endpoints['/test-ns/test'];
91 | $this->assertArrayNotHasKey( 'callback', $endpoint );
92 | $this->assertArrayHasKey( 'namespace', $endpoint );
93 | $this->assertEquals( 'test-ns', $endpoint['namespace'] );
94 |
95 | $filtered_endpoints = $GLOBALS['wp_rest_server']->get_routes();
96 | $endpoint = $filtered_endpoints['/test-ns/test'];
97 | $this->assertCount( 2, $endpoint );
98 |
99 | // Check for both methods.
100 | foreach ( array( 0, 1 ) as $key ) {
101 | $this->assertArrayHasKey( 'callback', $endpoint[ $key ] );
102 | $this->assertArrayHasKey( 'methods', $endpoint[ $key ] );
103 | $this->assertArrayHasKey( 'args', $endpoint[ $key ] );
104 | }
105 | }
106 |
107 | /**
108 | * Check that routes are merged by default.
109 | */
110 | public function test_route_merge() {
111 | register_rest_route( 'test-ns', '/test', array(
112 | 'methods' => array( 'GET' ),
113 | 'callback' => '__return_null',
114 | ) );
115 | register_rest_route( 'test-ns', '/test', array(
116 | 'methods' => array( 'POST' ),
117 | 'callback' => '__return_null',
118 | ) );
119 |
120 | // Check both routes exist.
121 | $endpoints = $GLOBALS['wp_rest_server']->get_routes();
122 | $endpoint = $endpoints['/test-ns/test'];
123 | $this->assertCount( 2, $endpoint );
124 | }
125 |
126 | /**
127 | * Check that we can override routes.
128 | */
129 | public function test_route_override() {
130 | register_rest_route( 'test-ns', '/test', array(
131 | 'methods' => array( 'GET' ),
132 | 'callback' => '__return_null',
133 | 'should_exist' => false,
134 | ) );
135 | register_rest_route( 'test-ns', '/test', array(
136 | 'methods' => array( 'POST' ),
137 | 'callback' => '__return_null',
138 | 'should_exist' => true,
139 | ), true );
140 |
141 | // Check we only have one route.
142 | $endpoints = $GLOBALS['wp_rest_server']->get_routes();
143 | $endpoint = $endpoints['/test-ns/test'];
144 | $this->assertCount( 1, $endpoint );
145 |
146 | // Check it's the right one.
147 | $this->assertArrayHasKey( 'should_exist', $endpoint[0] );
148 | $this->assertTrue( $endpoint[0]['should_exist'] );
149 | }
150 |
151 | /**
152 | * Test that we reject routes without namespaces
153 | *
154 | * @expectedIncorrectUsage register_rest_route
155 | */
156 | public function test_route_reject_empty_namespace() {
157 | register_rest_route( '', '/test-empty-namespace', array(
158 | 'methods' => array( 'POST' ),
159 | 'callback' => '__return_null',
160 | ), true );
161 | $endpoints = $GLOBALS['wp_rest_server']->get_routes();
162 | $this->assertFalse( isset( $endpoints['/test-empty-namespace'] ) );
163 | }
164 |
165 | /**
166 | * Test that we reject empty routes
167 | *
168 | * @expectedIncorrectUsage register_rest_route
169 | */
170 | public function test_route_reject_empty_route() {
171 | register_rest_route( '/test-empty-route', '', array(
172 | 'methods' => array( 'POST' ),
173 | 'callback' => '__return_null',
174 | ), true );
175 | $endpoints = $GLOBALS['wp_rest_server']->get_routes();
176 | $this->assertFalse( isset( $endpoints['/test-empty-route'] ) );
177 | }
178 |
179 | /**
180 | * The rest_route query variable should be registered.
181 | */
182 | function test_rest_route_query_var() {
183 | rest_api_init();
184 | $this->assertTrue( in_array( 'rest_route', $GLOBALS['wp']->public_query_vars ) );
185 | }
186 |
187 | public function test_route_method() {
188 | register_rest_route( 'test-ns', '/test', array(
189 | 'methods' => array( 'GET' ),
190 | 'callback' => '__return_null',
191 | ) );
192 |
193 | $routes = $GLOBALS['wp_rest_server']->get_routes();
194 |
195 | $this->assertEquals( $routes['/test-ns/test'][0]['methods'], array( 'GET' => true ) );
196 | }
197 |
198 | /**
199 | * The 'methods' arg should accept a single value as well as array.
200 | */
201 | public function test_route_method_string() {
202 | register_rest_route( 'test-ns', '/test', array(
203 | 'methods' => 'GET',
204 | 'callback' => '__return_null',
205 | ) );
206 |
207 | $routes = $GLOBALS['wp_rest_server']->get_routes();
208 |
209 | $this->assertEquals( $routes['/test-ns/test'][0]['methods'], array( 'GET' => true ) );
210 | }
211 |
212 | /**
213 | * The 'methods' arg should accept a single value as well as array.
214 | */
215 | public function test_route_method_array() {
216 | register_rest_route( 'test-ns', '/test', array(
217 | 'methods' => array( 'GET', 'POST' ),
218 | 'callback' => '__return_null',
219 | ) );
220 |
221 | $routes = $GLOBALS['wp_rest_server']->get_routes();
222 |
223 | $this->assertEquals( $routes['/test-ns/test'][0]['methods'], array( 'GET' => true, 'POST' => true ) );
224 | }
225 |
226 | /**
227 | * The 'methods' arg should a comma seperated string.
228 | */
229 | public function test_route_method_comma_seperated() {
230 | register_rest_route( 'test-ns', '/test', array(
231 | 'methods' => 'GET,POST',
232 | 'callback' => '__return_null',
233 | ) );
234 |
235 | $routes = $GLOBALS['wp_rest_server']->get_routes();
236 |
237 | $this->assertEquals( $routes['/test-ns/test'][0]['methods'], array( 'GET' => true, 'POST' => true ) );
238 | }
239 |
240 | public function test_options_request() {
241 | register_rest_route( 'test-ns', '/test', array(
242 | 'methods' => 'GET,POST',
243 | 'callback' => '__return_null',
244 | ) );
245 |
246 | $request = new WP_REST_Request( 'OPTIONS', '/test-ns/test' );
247 | $response = rest_handle_options_request( null, $GLOBALS['wp_rest_server'], $request );
248 |
249 | $headers = $response->get_headers();
250 | $this->assertArrayHasKey( 'Accept', $headers );
251 |
252 | $this->assertEquals( 'GET, POST', $headers['Accept'] );
253 | }
254 |
255 | /**
256 | * Ensure that the OPTIONS handler doesn't kick in for non-OPTIONS requests.
257 | */
258 | public function test_options_request_not_options() {
259 | register_rest_route( 'test-ns', '/test', array(
260 | 'methods' => 'GET,POST',
261 | 'callback' => '__return_true',
262 | ) );
263 |
264 | $request = new WP_REST_Request( 'GET', '/test-ns/test' );
265 | $response = rest_handle_options_request( null, $GLOBALS['wp_rest_server'], $request );
266 |
267 | $this->assertNull( $response );
268 | }
269 |
270 | /**
271 | * The get_rest_url function should return a URL consistently terminated with a "/",
272 | * whether the blog is configured with pretty permalink support or not.
273 | */
274 | public function test_rest_url_generation() {
275 | // In pretty permalinks case, we expect a path of wp-json/ with no query.
276 | update_option( 'permalink_structure', '/%year%/%monthnum%/%day%/%postname%/' );
277 | $this->assertEquals( 'http://' . WP_TESTS_DOMAIN . '/wp-json/', get_rest_url() );
278 |
279 | update_option( 'permalink_structure', '' );
280 | // In non-pretty case, we get a query string to invoke the rest router.
281 | $this->assertEquals( 'http://' . WP_TESTS_DOMAIN . '/?rest_route=/', get_rest_url() );
282 |
283 | }
284 | /**
285 | * @ticket 34299
286 | */
287 | public function test_rest_url_scheme() {
288 | if ( isset( $_SERVER['HTTPS'] ) ) {
289 | $_https = $_SERVER['HTTPS'];
290 | }
291 | $_name = $_SERVER['SERVER_NAME'];
292 | $_SERVER['SERVER_NAME'] = parse_url( home_url(), PHP_URL_HOST );
293 | $_siteurl = get_option( 'siteurl' );
294 |
295 | set_current_screen( 'edit.php' );
296 | $this->assertTrue( is_admin() );
297 |
298 | // Test an HTTP URL
299 | unset( $_SERVER['HTTPS'] );
300 | $url = get_rest_url();
301 | $this->assertSame( 'http', parse_url( $url, PHP_URL_SCHEME ) );
302 |
303 | // Test an HTTPS URL
304 | $_SERVER['HTTPS'] = 'on';
305 | $url = get_rest_url();
306 | $this->assertSame( 'https', parse_url( $url, PHP_URL_SCHEME ) );
307 |
308 | // Switch to an admin request on a different domain name
309 | $_SERVER['SERVER_NAME'] = 'admin.example.org';
310 | update_option( 'siteurl', 'http://admin.example.org' );
311 | $this->assertNotEquals( $_SERVER['SERVER_NAME'], parse_url( home_url(), PHP_URL_HOST ) );
312 |
313 | // // Test an HTTP URL
314 | unset( $_SERVER['HTTPS'] );
315 | $url = get_rest_url();
316 | $this->assertSame( 'http', parse_url( $url, PHP_URL_SCHEME ) );
317 |
318 | // // Test an HTTPS URL
319 | $_SERVER['HTTPS'] = 'on';
320 | $url = get_rest_url();
321 | $this->assertSame( 'http', parse_url( $url, PHP_URL_SCHEME ) );
322 |
323 | // Reset
324 | if ( isset( $_https ) ) {
325 | $_SERVER['HTTPS'] = $_https;
326 | } else {
327 | unset( $_SERVER['HTTPS'] );
328 | }
329 | $_SERVER['SERVER_NAME'] = $_name;
330 | update_option( 'siteurl', $_siteurl );
331 | set_current_screen( 'front' );
332 |
333 | }
334 |
335 | }
336 |
--------------------------------------------------------------------------------
/phpunit/tests/rest-api/rest-request.php:
--------------------------------------------------------------------------------
1 | request = new WP_REST_Request();
15 | }
16 |
17 | public function test_header() {
18 | $value = 'application/x-wp-example';
19 |
20 | $this->request->set_header( 'Content-Type', $value );
21 |
22 | $this->assertEquals( $value, $this->request->get_header( 'Content-Type' ) );
23 | }
24 |
25 | public function test_header_missing() {
26 | $this->assertNull( $this->request->get_header( 'missing' ) );
27 | $this->assertNull( $this->request->get_header_as_array( 'missing' ) );
28 | }
29 |
30 | public function test_header_multiple() {
31 | $value1 = 'application/x-wp-example-1';
32 | $value2 = 'application/x-wp-example-2';
33 | $this->request->add_header( 'Accept', $value1 );
34 | $this->request->add_header( 'Accept', $value2 );
35 |
36 | $this->assertEquals( $value1 . ',' . $value2, $this->request->get_header( 'Accept' ) );
37 | $this->assertEquals( array( $value1, $value2 ), $this->request->get_header_as_array( 'Accept' ) );
38 | }
39 |
40 | public static function header_provider() {
41 | return array(
42 | array( 'Test', 'test' ),
43 | array( 'TEST', 'test' ),
44 | array( 'Test-Header', 'test_header' ),
45 | array( 'test-header', 'test_header' ),
46 | array( 'Test_Header', 'test_header' ),
47 | array( 'test_header', 'test_header' ),
48 | );
49 | }
50 |
51 | /**
52 | * @dataProvider header_provider
53 | * @param string $original Original header key.
54 | * @param string $expected Expected canonicalized version.
55 | */
56 | public function test_header_canonicalization( $original, $expected ) {
57 | $this->assertEquals( $expected, $this->request->canonicalize_header_name( $original ) );
58 | }
59 |
60 | public static function content_type_provider() {
61 | return array(
62 | // Check basic parsing.
63 | array( 'application/x-wp-example', 'application/x-wp-example', 'application', 'x-wp-example', '' ),
64 | array( 'application/x-wp-example; charset=utf-8', 'application/x-wp-example', 'application', 'x-wp-example', 'charset=utf-8' ),
65 |
66 | // Check case insensitivity.
67 | array( 'APPLICATION/x-WP-Example', 'application/x-wp-example', 'application', 'x-wp-example', '' ),
68 | );
69 | }
70 |
71 | /**
72 | * @dataProvider content_type_provider
73 | *
74 | * @param string $header Header value.
75 | * @param string $value Full type value.
76 | * @param string $type Main type (application, text, etc).
77 | * @param string $subtype Subtype (json, etc).
78 | * @param string $parameters Parameters (charset=utf-8, etc).
79 | */
80 | public function test_content_type_parsing( $header, $value, $type, $subtype, $parameters ) {
81 | // Check we start with nothing.
82 | $this->assertEmpty( $this->request->get_content_type() );
83 |
84 | $this->request->set_header( 'Content-Type', $header );
85 | $parsed = $this->request->get_content_type();
86 |
87 | $this->assertEquals( $value, $parsed['value'] );
88 | $this->assertEquals( $type, $parsed['type'] );
89 | $this->assertEquals( $subtype, $parsed['subtype'] );
90 | $this->assertEquals( $parameters, $parsed['parameters'] );
91 | }
92 |
93 | protected function request_with_parameters() {
94 | $this->request->set_url_params( array(
95 | 'source' => 'url',
96 | 'has_url_params' => true,
97 | ) );
98 | $this->request->set_query_params( array(
99 | 'source' => 'query',
100 | 'has_query_params' => true,
101 | ) );
102 | $this->request->set_body_params( array(
103 | 'source' => 'body',
104 | 'has_body_params' => true,
105 | ) );
106 |
107 | $json_data = wp_json_encode( array(
108 | 'source' => 'json',
109 | 'has_json_params' => true,
110 | ) );
111 | $this->request->set_body( $json_data );
112 |
113 | $this->request->set_default_params( array(
114 | 'source' => 'defaults',
115 | 'has_default_params' => true,
116 | ) );
117 | }
118 |
119 | public function test_parameter_order() {
120 | $this->request_with_parameters();
121 |
122 | $this->request->set_method( 'GET' );
123 |
124 | // Check that query takes precedence.
125 | $this->assertEquals( 'query', $this->request->get_param( 'source' ) );
126 |
127 | // Check that the correct arguments are parsed (and that falling through
128 | // the stack works).
129 | $this->assertTrue( $this->request->get_param( 'has_url_params' ) );
130 | $this->assertTrue( $this->request->get_param( 'has_query_params' ) );
131 | $this->assertTrue( $this->request->get_param( 'has_default_params' ) );
132 |
133 | // POST and JSON parameters shouldn't be parsed.
134 | $this->assertEmpty( $this->request->get_param( 'has_body_params' ) );
135 | $this->assertEmpty( $this->request->get_param( 'has_json_params' ) );
136 | }
137 |
138 | public function test_parameter_order_post() {
139 | $this->request_with_parameters();
140 |
141 | $this->request->set_method( 'POST' );
142 | $this->request->set_header( 'Content-Type', 'application/x-www-form-urlencoded' );
143 | $this->request->set_attributes( array( 'accept_json' => true ) );
144 |
145 | // Check that POST takes precedence.
146 | $this->assertEquals( 'body', $this->request->get_param( 'source' ) );
147 |
148 | // Check that the correct arguments are parsed (and that falling through
149 | // the stack works).
150 | $this->assertTrue( $this->request->get_param( 'has_url_params' ) );
151 | $this->assertTrue( $this->request->get_param( 'has_query_params' ) );
152 | $this->assertTrue( $this->request->get_param( 'has_body_params' ) );
153 | $this->assertTrue( $this->request->get_param( 'has_default_params' ) );
154 |
155 | // JSON shouldn't be parsed.
156 | $this->assertEmpty( $this->request->get_param( 'has_json_params' ) );
157 | }
158 |
159 | public function test_parameter_order_json() {
160 | $this->request_with_parameters();
161 |
162 | $this->request->set_method( 'POST' );
163 | $this->request->set_header( 'Content-Type', 'application/json' );
164 | $this->request->set_attributes( array( 'accept_json' => true ) );
165 |
166 | // Check that JSON takes precedence.
167 | $this->assertEquals( 'json', $this->request->get_param( 'source' ) );
168 |
169 | // Check that the correct arguments are parsed (and that falling through
170 | // the stack works).
171 | $this->assertTrue( $this->request->get_param( 'has_url_params' ) );
172 | $this->assertTrue( $this->request->get_param( 'has_query_params' ) );
173 | $this->assertTrue( $this->request->get_param( 'has_body_params' ) );
174 | $this->assertTrue( $this->request->get_param( 'has_json_params' ) );
175 | $this->assertTrue( $this->request->get_param( 'has_default_params' ) );
176 | }
177 |
178 | public function test_parameter_order_json_invalid() {
179 | $this->request_with_parameters();
180 |
181 | $this->request->set_method( 'POST' );
182 | $this->request->set_header( 'Content-Type', 'application/json' );
183 | $this->request->set_attributes( array( 'accept_json' => true ) );
184 |
185 | // Use invalid JSON data.
186 | $this->request->set_body( '{ this is not json }' );
187 |
188 | // Check that JSON is ignored.
189 | $this->assertEquals( 'body', $this->request->get_param( 'source' ) );
190 |
191 | // Check that the correct arguments are parsed (and that falling through
192 | // the stack works).
193 | $this->assertTrue( $this->request->get_param( 'has_url_params' ) );
194 | $this->assertTrue( $this->request->get_param( 'has_query_params' ) );
195 | $this->assertTrue( $this->request->get_param( 'has_body_params' ) );
196 | $this->assertTrue( $this->request->get_param( 'has_default_params' ) );
197 |
198 | // JSON should be ignored.
199 | $this->assertEmpty( $this->request->get_param( 'has_json_params' ) );
200 | }
201 |
202 | /**
203 | * PUT requests don't get $_POST automatically parsed, so ensure that
204 | * WP_REST_Request does it for us.
205 | */
206 | public function test_parameters_for_put() {
207 | $data = array(
208 | 'foo' => 'bar',
209 | 'alot' => array(
210 | 'of' => 'parameters',
211 | ),
212 | 'list' => array(
213 | 'of',
214 | 'cool',
215 | 'stuff',
216 | ),
217 | );
218 |
219 | $this->request->set_method( 'PUT' );
220 | $this->request->set_body_params( array() );
221 | $this->request->set_body( http_build_query( $data ) );
222 |
223 | foreach ( $data as $key => $expected_value ) {
224 | $this->assertEquals( $expected_value, $this->request->get_param( $key ) );
225 | }
226 | }
227 |
228 | public function test_parameters_for_json_put() {
229 | $data = array(
230 | 'foo' => 'bar',
231 | 'alot' => array(
232 | 'of' => 'parameters',
233 | ),
234 | 'list' => array(
235 | 'of',
236 | 'cool',
237 | 'stuff',
238 | ),
239 | );
240 |
241 | $this->request->set_method( 'PUT' );
242 | $this->request->add_header( 'content-type', 'application/json' );
243 | $this->request->set_body( wp_json_encode( $data ) );
244 |
245 | foreach ( $data as $key => $expected_value ) {
246 | $this->assertEquals( $expected_value, $this->request->get_param( $key ) );
247 | }
248 | }
249 |
250 | public function test_parameters_for_json_post() {
251 | $data = array(
252 | 'foo' => 'bar',
253 | 'alot' => array(
254 | 'of' => 'parameters',
255 | ),
256 | 'list' => array(
257 | 'of',
258 | 'cool',
259 | 'stuff',
260 | ),
261 | );
262 |
263 | $this->request->set_method( 'POST' );
264 | $this->request->add_header( 'content-type', 'application/json' );
265 | $this->request->set_body( wp_json_encode( $data ) );
266 |
267 | foreach ( $data as $key => $expected_value ) {
268 | $this->assertEquals( $expected_value, $this->request->get_param( $key ) );
269 | }
270 | }
271 |
272 | public function test_parameter_merging() {
273 | $this->request_with_parameters();
274 |
275 | $this->request->set_method( 'POST' );
276 |
277 | $expected = array(
278 | 'source' => 'body',
279 | 'has_url_params' => true,
280 | 'has_query_params' => true,
281 | 'has_body_params' => true,
282 | 'has_default_params' => true,
283 | );
284 | $this->assertEquals( $expected, $this->request->get_params() );
285 | }
286 |
287 | public function test_sanitize_params() {
288 | $this->request->set_url_params( array(
289 | 'someinteger' => '123',
290 | 'somestring' => 'hello',
291 | ));
292 |
293 | $this->request->set_attributes( array(
294 | 'args' => array(
295 | 'someinteger' => array(
296 | 'sanitize_callback' => 'absint',
297 | ),
298 | 'somestring' => array(
299 | 'sanitize_callback' => 'absint',
300 | ),
301 | ),
302 | ) );
303 |
304 | $this->request->sanitize_params();
305 |
306 | $this->assertEquals( 123, $this->request->get_param( 'someinteger' ) );
307 | $this->assertEquals( 0, $this->request->get_param( 'somestring' ) );
308 | }
309 |
310 | public function test_has_valid_params_required_flag() {
311 | $this->request->set_attributes( array(
312 | 'args' => array(
313 | 'someinteger' => array(
314 | 'required' => true,
315 | ),
316 | ),
317 | ) );
318 |
319 | $valid = $this->request->has_valid_params();
320 |
321 | $this->assertWPError( $valid );
322 | $this->assertEquals( 'rest_missing_callback_param', $valid->get_error_code() );
323 | }
324 |
325 | public function test_has_valid_params_required_flag_multiple() {
326 | $this->request->set_attributes( array(
327 | 'args' => array(
328 | 'someinteger' => array(
329 | 'required' => true,
330 | ),
331 | 'someotherinteger' => array(
332 | 'required' => true,
333 | ),
334 | ),
335 | ));
336 |
337 | $valid = $this->request->has_valid_params();
338 |
339 | $this->assertWPError( $valid );
340 | $this->assertEquals( 'rest_missing_callback_param', $valid->get_error_code() );
341 |
342 | $data = $valid->get_error_data( 'rest_missing_callback_param' );
343 |
344 | $this->assertTrue( in_array( 'someinteger', $data['params'] ) );
345 | $this->assertTrue( in_array( 'someotherinteger', $data['params'] ) );
346 | }
347 |
348 | public function test_has_valid_params_validate_callback() {
349 | $this->request->set_url_params( array(
350 | 'someinteger' => '123',
351 | ));
352 |
353 | $this->request->set_attributes( array(
354 | 'args' => array(
355 | 'someinteger' => array(
356 | 'validate_callback' => '__return_false',
357 | ),
358 | ),
359 | ));
360 |
361 | $valid = $this->request->has_valid_params();
362 |
363 | $this->assertWPError( $valid );
364 | $this->assertEquals( 'rest_invalid_param', $valid->get_error_code() );
365 | }
366 |
367 | public function test_has_multiple_invalid_params_validate_callback() {
368 | $this->request->set_url_params( array(
369 | 'someinteger' => '123',
370 | 'someotherinteger' => '123',
371 | ));
372 |
373 | $this->request->set_attributes( array(
374 | 'args' => array(
375 | 'someinteger' => array(
376 | 'validate_callback' => '__return_false',
377 | ),
378 | 'someotherinteger' => array(
379 | 'validate_callback' => '__return_false',
380 | ),
381 | ),
382 | ));
383 |
384 | $valid = $this->request->has_valid_params();
385 |
386 | $this->assertWPError( $valid );
387 | $this->assertEquals( 'rest_invalid_param', $valid->get_error_code() );
388 |
389 | $data = $valid->get_error_data( 'rest_invalid_param' );
390 |
391 | $this->assertArrayHasKey( 'someinteger', $data['params'] );
392 | $this->assertArrayHasKey( 'someotherinteger', $data['params'] );
393 | }
394 | }
395 |
--------------------------------------------------------------------------------
/license.txt:
--------------------------------------------------------------------------------
1 | GNU GENERAL PUBLIC LICENSE
2 | Version 2, June 1991
3 |
4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc.
5 | 51 Franklin St, Fifth Floor, Boston, MA 02110, USA
6 |
7 | Everyone is permitted to copy and distribute verbatim copies
8 | of this license document, but changing it is not allowed.
9 |
10 | Preamble
11 |
12 | The licenses for most software are designed to take away your
13 | freedom to share and change it. By contrast, the GNU General Public
14 | License is intended to guarantee your freedom to share and change free
15 | software--to make sure the software is free for all its users. This
16 | General Public License applies to most of the Free Software
17 | Foundation's software and to any other program whose authors commit to
18 | using it. (Some other Free Software Foundation software is covered by
19 | the GNU Library General Public License instead.) You can apply it to
20 | your programs, too.
21 |
22 | When we speak of free software, we are referring to freedom, not
23 | price. Our General Public Licenses are designed to make sure that you
24 | have the freedom to distribute copies of free software (and charge for
25 | this service if you wish), that you receive source code or can get it
26 | if you want it, that you can change the software or use pieces of it
27 | in new free programs; and that you know you can do these things.
28 |
29 | To protect your rights, we need to make restrictions that forbid
30 | anyone to deny you these rights or to ask you to surrender the rights.
31 | These restrictions translate to certain responsibilities for you if you
32 | distribute copies of the software, or if you modify it.
33 |
34 | For example, if you distribute copies of such a program, whether
35 | gratis or for a fee, you must give the recipients all the rights that
36 | you have. You must make sure that they, too, receive or can get the
37 | source code. And you must show them these terms so they know their
38 | rights.
39 |
40 | We protect your rights with two steps: (1) copyright the software, and
41 | (2) offer you this license which gives you legal permission to copy,
42 | distribute and/or modify the software.
43 |
44 | Also, for each author's protection and ours, we want to make certain
45 | that everyone understands that there is no warranty for this free
46 | software. If the software is modified by someone else and passed on, we
47 | want its recipients to know that what they have is not the original, so
48 | that any problems introduced by others will not reflect on the original
49 | authors' reputations.
50 |
51 | Finally, any free program is threatened constantly by software
52 | patents. We wish to avoid the danger that redistributors of a free
53 | program will individually obtain patent licenses, in effect making the
54 | program proprietary. To prevent this, we have made it clear that any
55 | patent must be licensed for everyone's free use or not licensed at all.
56 |
57 | The precise terms and conditions for copying, distribution and
58 | modification follow.
59 |
60 | GNU GENERAL PUBLIC LICENSE
61 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION
62 |
63 | 0. This License applies to any program or other work which contains
64 | a notice placed by the copyright holder saying it may be distributed
65 | under the terms of this General Public License. The "Program", below,
66 | refers to any such program or work, and a "work based on the Program"
67 | means either the Program or any derivative work under copyright law:
68 | that is to say, a work containing the Program or a portion of it,
69 | either verbatim or with modifications and/or translated into another
70 | language. (Hereinafter, translation is included without limitation in
71 | the term "modification".) Each licensee is addressed as "you".
72 |
73 | Activities other than copying, distribution and modification are not
74 | covered by this License; they are outside its scope. The act of
75 | running the Program is not restricted, and the output from the Program
76 | is covered only if its contents constitute a work based on the
77 | Program (independent of having been made by running the Program).
78 | Whether that is true depends on what the Program does.
79 |
80 | 1. You may copy and distribute verbatim copies of the Program's
81 | source code as you receive it, in any medium, provided that you
82 | conspicuously and appropriately publish on each copy an appropriate
83 | copyright notice and disclaimer of warranty; keep intact all the
84 | notices that refer to this License and to the absence of any warranty;
85 | and give any other recipients of the Program a copy of this License
86 | along with the Program.
87 |
88 | You may charge a fee for the physical act of transferring a copy, and
89 | you may at your option offer warranty protection in exchange for a fee.
90 |
91 | 2. You may modify your copy or copies of the Program or any portion
92 | of it, thus forming a work based on the Program, and copy and
93 | distribute such modifications or work under the terms of Section 1
94 | above, provided that you also meet all of these conditions:
95 |
96 | a) You must cause the modified files to carry prominent notices
97 | stating that you changed the files and the date of any change.
98 |
99 | b) You must cause any work that you distribute or publish, that in
100 | whole or in part contains or is derived from the Program or any
101 | part thereof, to be licensed as a whole at no charge to all third
102 | parties under the terms of this License.
103 |
104 | c) If the modified program normally reads commands interactively
105 | when run, you must cause it, when started running for such
106 | interactive use in the most ordinary way, to print or display an
107 | announcement including an appropriate copyright notice and a
108 | notice that there is no warranty (or else, saying that you provide
109 | a warranty) and that users may redistribute the program under
110 | these conditions, and telling the user how to view a copy of this
111 | License. (Exception: if the Program itself is interactive but
112 | does not normally print such an announcement, your work based on
113 | the Program is not required to print an announcement.)
114 |
115 | These requirements apply to the modified work as a whole. If
116 | identifiable sections of that work are not derived from the Program,
117 | and can be reasonably considered independent and separate works in
118 | themselves, then this License, and its terms, do not apply to those
119 | sections when you distribute them as separate works. But when you
120 | distribute the same sections as part of a whole which is a work based
121 | on the Program, the distribution of the whole must be on the terms of
122 | this License, whose permissions for other licensees extend to the
123 | entire whole, and thus to each and every part regardless of who wrote it.
124 | Thus, it is not the intent of this section to claim rights or contest
125 | your rights to work written entirely by you; rather, the intent is to
126 | exercise the right to control the distribution of derivative or
127 | collective works based on the Program.
128 |
129 | In addition, mere aggregation of another work not based on the Program
130 | with the Program (or with a work based on the Program) on a volume of
131 | a storage or distribution medium does not bring the other work under
132 | the scope of this License.
133 |
134 | 3. You may copy and distribute the Program (or a work based on it,
135 | under Section 2) in object code or executable form under the terms of
136 | Sections 1 and 2 above provided that you also do one of the following:
137 |
138 | a) Accompany it with the complete corresponding machine-readable
139 | source code, which must be distributed under the terms of Sections
140 | 1 and 2 above on a medium customarily used for software interchange; or,
141 |
142 | b) Accompany it with a written offer, valid for at least three
143 | years, to give any third party, for a charge no more than your
144 | cost of physically performing source distribution, a complete
145 | machine-readable copy of the corresponding source code, to be
146 | distributed under the terms of Sections 1 and 2 above on a medium
147 | customarily used for software interchange; or,
148 |
149 | c) Accompany it with the information you received as to the offer
150 | to distribute corresponding source code. (This alternative is
151 | allowed only for noncommercial distribution and only if you
152 | received the program in object code or executable form with such
153 | an offer, in accord with Subsection b above.)
154 |
155 | The source code for a work means the preferred form of the work for
156 | making modifications to it. For an executable work, complete source
157 | code means all the source code for all modules it contains, plus any
158 | associated interface definition files, plus the scripts used to
159 | control compilation and installation of the executable. However, as a
160 | special exception, the source code distributed need not include
161 | anything that is normally distributed (in either source or binary
162 | form) with the major components (compiler, kernel, and so on) of the
163 | operating system on which the executable runs, unless that component
164 | itself accompanies the executable.
165 |
166 | If distribution of executable or object code is made by offering
167 | access to copy from a designated place, then offering equivalent
168 | access to copy the source code from the same place counts as
169 | distribution of the source code, even though third parties are not
170 | compelled to copy the source along with the object code.
171 |
172 | 4. You may not copy, modify, sublicense, or distribute the Program
173 | except as expressly provided under this License. Any attempt
174 | otherwise to copy, modify, sublicense or distribute the Program is
175 | void, and will automatically terminate your rights under this License.
176 | However, parties who have received copies, or rights, from you under
177 | this License will not have their licenses terminated so long as such
178 | parties remain in full compliance.
179 |
180 | 5. You are not required to accept this License, since you have not
181 | signed it. However, nothing else grants you permission to modify or
182 | distribute the Program or its derivative works. These actions are
183 | prohibited by law if you do not accept this License. Therefore, by
184 | modifying or distributing the Program (or any work based on the
185 | Program), you indicate your acceptance of this License to do so, and
186 | all its terms and conditions for copying, distributing or modifying
187 | the Program or works based on it.
188 |
189 | 6. Each time you redistribute the Program (or any work based on the
190 | Program), the recipient automatically receives a license from the
191 | original licensor to copy, distribute or modify the Program subject to
192 | these terms and conditions. You may not impose any further
193 | restrictions on the recipients' exercise of the rights granted herein.
194 | You are not responsible for enforcing compliance by third parties to
195 | this License.
196 |
197 | 7. If, as a consequence of a court judgment or allegation of patent
198 | infringement or for any other reason (not limited to patent issues),
199 | conditions are imposed on you (whether by court order, agreement or
200 | otherwise) that contradict the conditions of this License, they do not
201 | excuse you from the conditions of this License. If you cannot
202 | distribute so as to satisfy simultaneously your obligations under this
203 | License and any other pertinent obligations, then as a consequence you
204 | may not distribute the Program at all. For example, if a patent
205 | license would not permit royalty-free redistribution of the Program by
206 | all those who receive copies directly or indirectly through you, then
207 | the only way you could satisfy both it and this License would be to
208 | refrain entirely from distribution of the Program.
209 |
210 | If any portion of this section is held invalid or unenforceable under
211 | any particular circumstance, the balance of the section is intended to
212 | apply and the section as a whole is intended to apply in other
213 | circumstances.
214 |
215 | It is not the purpose of this section to induce you to infringe any
216 | patents or other property right claims or to contest validity of any
217 | such claims; this section has the sole purpose of protecting the
218 | integrity of the free software distribution system, which is
219 | implemented by public license practices. Many people have made
220 | generous contributions to the wide range of software distributed
221 | through that system in reliance on consistent application of that
222 | system; it is up to the author/donor to decide if he or she is willing
223 | to distribute software through any other system and a licensee cannot
224 | impose that choice.
225 |
226 | This section is intended to make thoroughly clear what is believed to
227 | be a consequence of the rest of this License.
228 |
229 | 8. If the distribution and/or use of the Program is restricted in
230 | certain countries either by patents or by copyrighted interfaces, the
231 | original copyright holder who places the Program under this License
232 | may add an explicit geographical distribution limitation excluding
233 | those countries, so that distribution is permitted only in or among
234 | countries not thus excluded. In such case, this License incorporates
235 | the limitation as if written in the body of this License.
236 |
237 | 9. The Free Software Foundation may publish revised and/or new versions
238 | of the General Public License from time to time. Such new versions will
239 | be similar in spirit to the present version, but may differ in detail to
240 | address new problems or concerns.
241 |
242 | Each version is given a distinguishing version number. If the Program
243 | specifies a version number of this License which applies to it and "any
244 | later version", you have the option of following the terms and conditions
245 | either of that version or of any later version published by the Free
246 | Software Foundation. If the Program does not specify a version number of
247 | this License, you may choose any version ever published by the Free Software
248 | Foundation.
249 |
250 | 10. If you wish to incorporate parts of the Program into other free
251 | programs whose distribution conditions are different, write to the author
252 | to ask for permission. For software which is copyrighted by the Free
253 | Software Foundation, write to the Free Software Foundation; we sometimes
254 | make exceptions for this. Our decision will be guided by the two goals
255 | of preserving the free status of all derivatives of our free software and
256 | of promoting the sharing and reuse of software generally.
257 |
258 | NO WARRANTY
259 |
260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY
261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN
262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES
263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED
264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS
266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE
267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING,
268 | REPAIR OR CORRECTION.
269 |
270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR
272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES,
273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING
274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED
275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY
276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER
277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE
278 | POSSIBILITY OF SUCH DAMAGES.
279 |
280 | END OF TERMS AND CONDITIONS
281 |
282 |
--------------------------------------------------------------------------------
/wp-includes/rest-api.php:
--------------------------------------------------------------------------------
1 | 'GET',
56 | 'callback' => null,
57 | 'args' => array(),
58 | );
59 | foreach ( $args as $key => &$arg_group ) {
60 | if ( ! is_numeric( $arg_group ) ) {
61 | // Route option, skip here.
62 | continue;
63 | }
64 |
65 | $arg_group = array_merge( $defaults, $arg_group );
66 | }
67 |
68 | $full_route = '/' . trim( $namespace, '/' ) . '/' . trim( $route, '/' );
69 | $wp_rest_server->register_route( $namespace, $full_route, $args, $override );
70 | return true;
71 | }
72 |
73 | /**
74 | * Registers rewrite rules for the API.
75 | *
76 | * @since 4.4.0
77 | *
78 | * @see rest_api_register_rewrites()
79 | * @global WP $wp Current WordPress environment instance.
80 | */
81 | function rest_api_init() {
82 | rest_api_register_rewrites();
83 |
84 | global $wp;
85 | $wp->add_query_var( 'rest_route' );
86 | }
87 |
88 | /**
89 | * Adds REST rewrite rules.
90 | *
91 | * @since 4.4.0
92 | *
93 | * @see add_rewrite_rule()
94 | */
95 | function rest_api_register_rewrites() {
96 | add_rewrite_rule( '^' . rest_get_url_prefix() . '/?$','index.php?rest_route=/','top' );
97 | add_rewrite_rule( '^' . rest_get_url_prefix() . '/(.*)?','index.php?rest_route=/$matches[1]','top' );
98 | }
99 |
100 | /**
101 | * Registers the default REST API filters.
102 | *
103 | * Attached to the {@see 'rest_api_init'} action
104 | * to make testing and disabling these filters easier.
105 | *
106 | * @since 4.4.0
107 | */
108 | function rest_api_default_filters() {
109 | // Deprecated reporting.
110 | add_action( 'deprecated_function_run', 'rest_handle_deprecated_function', 10, 3 );
111 | add_filter( 'deprecated_function_trigger_error', '__return_false' );
112 | add_action( 'deprecated_argument_run', 'rest_handle_deprecated_argument', 10, 3 );
113 | add_filter( 'deprecated_argument_trigger_error', '__return_false' );
114 |
115 | // Default serving.
116 | add_filter( 'rest_pre_serve_request', 'rest_send_cors_headers' );
117 | add_filter( 'rest_post_dispatch', 'rest_send_allow_header', 10, 3 );
118 |
119 | add_filter( 'rest_pre_dispatch', 'rest_handle_options_request', 10, 3 );
120 | }
121 |
122 | /**
123 | * Loads the REST API.
124 | *
125 | * @since 4.4.0
126 | *
127 | * @global WP $wp Current WordPress environment instance.
128 | * @global WP_REST_Server $wp_rest_server ResponseHandler instance (usually WP_REST_Server).
129 | */
130 | function rest_api_loaded() {
131 | if ( empty( $GLOBALS['wp']->query_vars['rest_route'] ) ) {
132 | return;
133 | }
134 |
135 | /**
136 | * Whether this is a REST Request.
137 | *
138 | * @since 4.4.0
139 | * @var bool
140 | */
141 | define( 'REST_REQUEST', true );
142 |
143 | /** @var WP_REST_Server $wp_rest_server */
144 | global $wp_rest_server;
145 |
146 | /**
147 | * Filter the REST Server Class.
148 | *
149 | * This filter allows you to adjust the server class used by the API, using a
150 | * different class to handle requests.
151 | *
152 | * @since 4.4.0
153 | *
154 | * @param string $class_name The name of the server class. Default 'WP_REST_Server'.
155 | */
156 | $wp_rest_server_class = apply_filters( 'wp_rest_server_class', 'WP_REST_Server' );
157 | $wp_rest_server = new $wp_rest_server_class;
158 |
159 | /**
160 | * Fires when preparing to serve an API request.
161 | *
162 | * Endpoint objects should be created and register their hooks on this action rather
163 | * than another action to ensure they're only loaded when needed.
164 | *
165 | * @since 4.4.0
166 | *
167 | * @param WP_REST_Server $wp_rest_server Server object.
168 | */
169 | do_action( 'rest_api_init', $wp_rest_server );
170 |
171 | // Fire off the request.
172 | $wp_rest_server->serve_request( $GLOBALS['wp']->query_vars['rest_route'] );
173 |
174 | // We're done.
175 | die();
176 | }
177 |
178 | /**
179 | * Retrieves the URL prefix for any API resource.
180 | *
181 | * @since 4.4.0
182 | *
183 | * @return string Prefix.
184 | */
185 | function rest_get_url_prefix() {
186 | /**
187 | * Filter the REST URL prefix.
188 | *
189 | * @since 4.4.0
190 | *
191 | * @param string $prefix URL prefix. Default 'wp-json'.
192 | */
193 | return apply_filters( 'rest_url_prefix', 'wp-json' );
194 | }
195 |
196 | /**
197 | * Retrieves the URL to a REST endpoint on a site.
198 | *
199 | * Note: The returned URL is NOT escaped.
200 | *
201 | * @since 4.4.0
202 | *
203 | * @todo Check if this is even necessary
204 | *
205 | * @param int $blog_id Optional. Blog ID. Default of null returns URL for current blog.
206 | * @param string $path Optional. REST route. Default '/'.
207 | * @param string $scheme Optional. Sanitization scheme. Default 'rest'.
208 | * @return string Full URL to the endpoint.
209 | */
210 | function get_rest_url( $blog_id = null, $path = '/', $scheme = 'rest' ) {
211 | if ( empty( $path ) ) {
212 | $path = '/';
213 | }
214 |
215 | if ( is_multisite() && get_blog_option( $blog_id, 'permalink_structure' ) || get_option( 'permalink_structure' ) ) {
216 | $url = get_home_url( $blog_id, rest_get_url_prefix(), $scheme );
217 | $url .= '/' . ltrim( $path, '/' );
218 | } else {
219 | $url = trailingslashit( get_home_url( $blog_id, '', $scheme ) );
220 |
221 | $path = '/' . ltrim( $path, '/' );
222 |
223 | $url = add_query_arg( 'rest_route', $path, $url );
224 | }
225 |
226 | if ( is_ssl() ) {
227 | // If the current host is the same as the REST URL host, force the REST URL scheme to HTTPS.
228 | if ( $_SERVER['SERVER_NAME'] === parse_url( get_home_url( $blog_id ), PHP_URL_HOST ) ) {
229 | $url = set_url_scheme( $url, 'https' );
230 | }
231 | }
232 |
233 | /**
234 | * Filter the REST URL.
235 | *
236 | * Use this filter to adjust the url returned by the `get_rest_url` function.
237 | *
238 | * @since 4.4.0
239 | *
240 | * @param string $url REST URL.
241 | * @param string $path REST route.
242 | * @param int $blog_id Blog ID.
243 | * @param string $scheme Sanitization scheme.
244 | */
245 | return apply_filters( 'rest_url', $url, $path, $blog_id, $scheme );
246 | }
247 |
248 | /**
249 | * Retrieves the URL to a REST endpoint.
250 | *
251 | * Note: The returned URL is NOT escaped.
252 | *
253 | * @since 4.4.0
254 | *
255 | * @param string $path Optional. REST route. Default empty.
256 | * @param string $scheme Optional. Sanitization scheme. Default 'json'.
257 | * @return string Full URL to the endpoint.
258 | */
259 | function rest_url( $path = '', $scheme = 'json' ) {
260 | return get_rest_url( null, $path, $scheme );
261 | }
262 |
263 | /**
264 | * Do a REST request.
265 | *
266 | * Used primarily to route internal requests through WP_REST_Server.
267 | *
268 | * @since 4.4.0
269 | *
270 | * @global WP_REST_Server $wp_rest_server ResponseHandler instance (usually WP_REST_Server).
271 | *
272 | * @param WP_REST_Request|string $request Request.
273 | * @return WP_REST_Response REST response.
274 | */
275 | function rest_do_request( $request ) {
276 | global $wp_rest_server;
277 | $request = rest_ensure_request( $request );
278 | return $wp_rest_server->dispatch( $request );
279 | }
280 |
281 | /**
282 | * Ensures request arguments are a request object (for consistency).
283 | *
284 | * @since 4.4.0
285 | *
286 | * @param array|WP_REST_Request $request Request to check.
287 | * @return WP_REST_Request REST request instance.
288 | */
289 | function rest_ensure_request( $request ) {
290 | if ( $request instanceof WP_REST_Request ) {
291 | return $request;
292 | }
293 |
294 | return new WP_REST_Request( 'GET', '', $request );
295 | }
296 |
297 | /**
298 | * Ensures a REST response is a response object (for consistency).
299 | *
300 | * This implements WP_HTTP_Response, allowing usage of `set_status`/`header`/etc
301 | * without needing to double-check the object. Will also allow WP_Error to indicate error
302 | * responses, so users should immediately check for this value.
303 | *
304 | * @since 4.4.0
305 | *
306 | * @param WP_Error|WP_HTTP_Response|mixed $response Response to check.
307 | * @return mixed WP_Error if response generated an error, WP_HTTP_Response if response
308 | * is a already an instance, otherwise returns a new WP_REST_Response instance.
309 | */
310 | function rest_ensure_response( $response ) {
311 | if ( is_wp_error( $response ) ) {
312 | return $response;
313 | }
314 |
315 | if ( $response instanceof WP_HTTP_Response ) {
316 | return $response;
317 | }
318 |
319 | return new WP_REST_Response( $response );
320 | }
321 |
322 | /**
323 | * Handles _deprecated_function() errors.
324 | *
325 | * @since 4.4.0
326 | *
327 | * @param string $function Function name.
328 | * @param string $replacement Replacement function name.
329 | * @param string $version Version.
330 | */
331 | function rest_handle_deprecated_function( $function, $replacement, $version ) {
332 | if ( ! empty( $replacement ) ) {
333 | /* translators: 1: function name, 2: WordPress version number, 3: new function name */
334 | $string = sprintf( __( '%1$s (since %2$s; use %3$s instead)' ), $function, $version, $replacement );
335 | } else {
336 | /* translators: 1: function name, 2: WordPress version number */
337 | $string = sprintf( __( '%1$s (since %2$s; no alternative available)' ), $function, $version );
338 | }
339 |
340 | header( sprintf( 'X-WP-DeprecatedFunction: %s', $string ) );
341 | }
342 |
343 | /**
344 | * Handles _deprecated_argument() errors.
345 | *
346 | * @since 4.4.0
347 | *
348 | * @param string $function Function name.
349 | * @param string $replacement Replacement function name.
350 | * @param string $version Version.
351 | */
352 | function rest_handle_deprecated_argument( $function, $replacement, $version ) {
353 | if ( ! empty( $replacement ) ) {
354 | /* translators: 1: function name, 2: WordPress version number, 3: new argument name */
355 | $string = sprintf( __( '%1$s (since %2$s; %3$s)' ), $function, $version, $replacement );
356 | } else {
357 | /* translators: 1: function name, 2: WordPress version number */
358 | $string = sprintf( __( '%1$s (since %2$s; no alternative available)' ), $function, $version );
359 | }
360 |
361 | header( sprintf( 'X-WP-DeprecatedParam: %s', $string ) );
362 | }
363 |
364 | /**
365 | * Sends Cross-Origin Resource Sharing headers with API requests.
366 | *
367 | * @since 4.4.0
368 | *
369 | * @param mixed $value Response data.
370 | * @return mixed Response data.
371 | */
372 | function rest_send_cors_headers( $value ) {
373 | $origin = get_http_origin();
374 |
375 | if ( $origin ) {
376 | header( 'Access-Control-Allow-Origin: ' . esc_url_raw( $origin ) );
377 | header( 'Access-Control-Allow-Methods: POST, GET, OPTIONS, PUT, DELETE' );
378 | header( 'Access-Control-Allow-Credentials: true' );
379 | }
380 |
381 | return $value;
382 | }
383 |
384 | /**
385 | * Handles OPTIONS requests for the server.
386 | *
387 | * This is handled outside of the server code, as it doesn't obey normal route
388 | * mapping.
389 | *
390 | * @since 4.4.0
391 | *
392 | * @param mixed $response Current response, either response or `null` to indicate pass-through.
393 | * @param WP_REST_Server $handler ResponseHandler instance (usually WP_REST_Server).
394 | * @param WP_REST_Request $request The request that was used to make current response.
395 | * @return WP_REST_Response Modified response, either response or `null` to indicate pass-through.
396 | */
397 | function rest_handle_options_request( $response, $handler, $request ) {
398 | if ( ! empty( $response ) || $request->get_method() !== 'OPTIONS' ) {
399 | return $response;
400 | }
401 |
402 | $response = new WP_REST_Response();
403 | $data = array();
404 |
405 | $accept = array();
406 |
407 | foreach ( $handler->get_routes() as $route => $endpoints ) {
408 | $match = preg_match( '@^' . $route . '$@i', $request->get_route(), $args );
409 |
410 | if ( ! $match ) {
411 | continue;
412 | }
413 |
414 | $data = $handler->get_data_for_route( $route, $endpoints, 'help' );
415 | $accept = array_merge( $accept, $data['methods'] );
416 | break;
417 | }
418 | $response->header( 'Accept', implode( ', ', $accept ) );
419 |
420 | $response->set_data( $data );
421 | return $response;
422 | }
423 |
424 | /**
425 | * Sends the "Allow" header to state all methods that can be sent to the current route.
426 | *
427 | * @since 4.4.0
428 | *
429 | * @param WP_REST_Response $response Current response being served.
430 | * @param WP_REST_Server $server ResponseHandler instance (usually WP_REST_Server).
431 | * @param WP_REST_Request $request The request that was used to make current response.
432 | * @return WP_REST_Response Response to be served, with "Allow" header if route has allowed methods.
433 | */
434 | function rest_send_allow_header( $response, $server, $request ) {
435 | $matched_route = $response->get_matched_route();
436 |
437 | if ( ! $matched_route ) {
438 | return $response;
439 | }
440 |
441 | $routes = $server->get_routes();
442 |
443 | $allowed_methods = array();
444 |
445 | // Get the allowed methods across the routes.
446 | foreach ( $routes[ $matched_route ] as $_handler ) {
447 | foreach ( $_handler['methods'] as $handler_method => $value ) {
448 |
449 | if ( ! empty( $_handler['permission_callback'] ) ) {
450 |
451 | $permission = call_user_func( $_handler['permission_callback'], $request );
452 |
453 | $allowed_methods[ $handler_method ] = true === $permission;
454 | } else {
455 | $allowed_methods[ $handler_method ] = true;
456 | }
457 | }
458 | }
459 |
460 | // Strip out all the methods that are not allowed (false values).
461 | $allowed_methods = array_filter( $allowed_methods );
462 |
463 | if ( $allowed_methods ) {
464 | $response->header( 'Allow', implode( ', ', array_map( 'strtoupper', array_keys( $allowed_methods ) ) ) );
465 | }
466 |
467 | return $response;
468 | }
469 |
470 | /**
471 | * Adds the REST API URL to the WP RSD endpoint.
472 | *
473 | * @since 4.4.0
474 | *
475 | * @see get_rest_url()
476 | */
477 | function rest_output_rsd() {
478 | $api_root = get_rest_url();
479 |
480 | if ( empty( $api_root ) ) {
481 | return;
482 | }
483 | ?>
484 |
485 | \n";
503 | }
504 |
505 | /**
506 | * Sends a Link header for the REST API.
507 | *
508 | * @since 4.4.0
509 | */
510 | function rest_output_link_header() {
511 | if ( headers_sent() ) {
512 | return;
513 | }
514 |
515 | $api_root = get_rest_url();
516 |
517 | if ( empty( $api_root ) ) {
518 | return;
519 | }
520 |
521 | header( 'Link: <' . esc_url_raw( $api_root ) . '>; rel="https://api.w.org/"', false );
522 | }
523 |
524 | /**
525 | * Checks for errors when using cookie-based authentication.
526 | *
527 | * WordPress' built-in cookie authentication is always active
528 | * for logged in users. However, the API has to check nonces
529 | * for each request to ensure users are not vulnerable to CSRF.
530 | *
531 | * @since 4.4.0
532 | *
533 | * @global mixed $wp_rest_auth_cookie
534 | *
535 | * @param WP_Error|mixed $result Error from another authentication handler, null if we should handle it,
536 | * or another value if not.
537 | * @return WP_Error|mixed|bool WP_Error if the cookie is invalid, the $result, otherwise true.
538 | */
539 | function rest_cookie_check_errors( $result ) {
540 | if ( ! empty( $result ) ) {
541 | return $result;
542 | }
543 |
544 | global $wp_rest_auth_cookie;
545 |
546 | /*
547 | * Is cookie authentication being used? (If we get an auth
548 | * error, but we're still logged in, another authentication
549 | * must have been used).
550 | */
551 | if ( true !== $wp_rest_auth_cookie && is_user_logged_in() ) {
552 | return $result;
553 | }
554 |
555 | // Determine if there is a nonce.
556 | $nonce = null;
557 |
558 | if ( isset( $_REQUEST['_wpnonce'] ) ) {
559 | $nonce = $_REQUEST['_wpnonce'];
560 | } elseif ( isset( $_SERVER['HTTP_X_WP_NONCE'] ) ) {
561 | $nonce = $_SERVER['HTTP_X_WP_NONCE'];
562 | }
563 |
564 | if ( null === $nonce ) {
565 | // No nonce at all, so act as if it's an unauthenticated request.
566 | wp_set_current_user( 0 );
567 | return true;
568 | }
569 |
570 | // Check the nonce.
571 | $result = wp_verify_nonce( $nonce, 'wp_rest' );
572 |
573 | if ( ! $result ) {
574 | return new WP_Error( 'rest_cookie_invalid_nonce', __( 'Cookie nonce is invalid' ), array( 'status' => 403 ) );
575 | }
576 |
577 | return true;
578 | }
579 |
580 | /**
581 | * Collects cookie authentication status.
582 | *
583 | * Collects errors from wp_validate_auth_cookie for use by rest_cookie_check_errors.
584 | *
585 | * @since 4.4.0
586 | *
587 | * @see current_action()
588 | * @global mixed $wp_rest_auth_cookie
589 | */
590 | function rest_cookie_collect_status() {
591 | global $wp_rest_auth_cookie;
592 |
593 | $status_type = current_action();
594 |
595 | if ( 'auth_cookie_valid' !== $status_type ) {
596 | $wp_rest_auth_cookie = substr( $status_type, 12 );
597 | return;
598 | }
599 |
600 | $wp_rest_auth_cookie = true;
601 | }
602 |
603 | /**
604 | * Parses an RFC3339 timestamp into a DateTime.
605 | *
606 | * @since 4.4.0
607 | *
608 | * @param string $date RFC3339 timestamp.
609 | * @param bool $force_utc Optional. Whether to force UTC timezone instead of using
610 | * the timestamp's timezone. Default false.
611 | * @return DateTime DateTime instance.
612 | */
613 | function rest_parse_date( $date, $force_utc = false ) {
614 | if ( $force_utc ) {
615 | $date = preg_replace( '/[+-]\d+:?\d+$/', '+00:00', $date );
616 | }
617 |
618 | $regex = '#^\d{4}-\d{2}-\d{2}[Tt ]\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}(?::\d{2})?)?$#';
619 |
620 | if ( ! preg_match( $regex, $date, $matches ) ) {
621 | return false;
622 | }
623 |
624 | return strtotime( $date );
625 | }
626 |
627 | /**
628 | * Retrieves a local date with its GMT equivalent, in MySQL datetime format.
629 | *
630 | * @since 4.4.0
631 | *
632 | * @see rest_parse_date()
633 | *
634 | * @param string $date RFC3339 timestamp.
635 | * @param bool $force_utc Whether a UTC timestamp should be forced. Default false.
636 | * @return array|null Local and UTC datetime strings, in MySQL datetime format (Y-m-d H:i:s),
637 | * null on failure.
638 | */
639 | function rest_get_date_with_gmt( $date, $force_utc = false ) {
640 | $date = rest_parse_date( $date, $force_utc );
641 |
642 | if ( empty( $date ) ) {
643 | return null;
644 | }
645 |
646 | $utc = date( 'Y-m-d H:i:s', $date );
647 | $local = get_date_from_gmt( $utc );
648 |
649 | return array( $local, $utc );
650 | }
651 |
--------------------------------------------------------------------------------
/phpunit/tests/rest-api/rest-server.php:
--------------------------------------------------------------------------------
1 | server = $wp_rest_server = new Spy_REST_Server();
19 |
20 | do_action( 'rest_api_init', $this->server );
21 | }
22 |
23 | public function test_envelope() {
24 | $data = array(
25 | 'amount of arbitrary data' => 'alot',
26 | );
27 | $status = 987;
28 | $headers = array(
29 | 'Arbitrary-Header' => 'value',
30 | 'Multiple' => 'maybe, yes',
31 | );
32 |
33 | $response = new WP_REST_Response( $data, $status );
34 | $response->header( 'Arbitrary-Header', 'value' );
35 |
36 | // Check header concatenation as well.
37 | $response->header( 'Multiple', 'maybe' );
38 | $response->header( 'Multiple', 'yes', false );
39 |
40 | $envelope_response = $this->server->envelope_response( $response, false );
41 |
42 | // The envelope should still be a response, but with defaults.
43 | $this->assertInstanceOf( 'WP_REST_Response', $envelope_response );
44 | $this->assertEquals( 200, $envelope_response->get_status() );
45 | $this->assertEmpty( $envelope_response->get_headers() );
46 | $this->assertEmpty( $envelope_response->get_links() );
47 |
48 | $enveloped = $envelope_response->get_data();
49 |
50 | $this->assertEquals( $data, $enveloped['body'] );
51 | $this->assertEquals( $status, $enveloped['status'] );
52 | $this->assertEquals( $headers, $enveloped['headers'] );
53 | }
54 |
55 | public function test_default_param() {
56 |
57 | register_rest_route( 'test-ns', '/test', array(
58 | 'methods' => array( 'GET' ),
59 | 'callback' => '__return_null',
60 | 'args' => array(
61 | 'foo' => array(
62 | 'default' => 'bar',
63 | ),
64 | ),
65 | ) );
66 |
67 | $request = new WP_REST_Request( 'GET', '/test-ns/test' );
68 | $response = $this->server->dispatch( $request );
69 |
70 | $this->assertEquals( 'bar', $request['foo'] );
71 | }
72 |
73 | public function test_default_param_is_overridden() {
74 |
75 | register_rest_route( 'test-ns', '/test', array(
76 | 'methods' => array( 'GET' ),
77 | 'callback' => '__return_null',
78 | 'args' => array(
79 | 'foo' => array(
80 | 'default' => 'bar',
81 | ),
82 | ),
83 | ) );
84 |
85 | $request = new WP_REST_Request( 'GET', '/test-ns/test' );
86 | $request->set_query_params( array( 'foo' => 123 ) );
87 | $response = $this->server->dispatch( $request );
88 |
89 | $this->assertEquals( '123', $request['foo'] );
90 | }
91 |
92 | public function test_optional_param() {
93 | register_rest_route( 'optional', '/test', array(
94 | 'methods' => array( 'GET' ),
95 | 'callback' => '__return_null',
96 | 'args' => array(
97 | 'foo' => array(),
98 | ),
99 | ) );
100 |
101 | $request = new WP_REST_Request( 'GET', '/optional/test' );
102 | $request->set_query_params( array() );
103 | $response = $this->server->dispatch( $request );
104 | $this->assertInstanceOf( 'WP_REST_Response', $response );
105 | $this->assertEquals( 200, $response->get_status() );
106 | $this->assertArrayNotHasKey( 'foo', (array) $request );
107 | }
108 |
109 | public function test_no_zero_param() {
110 | register_rest_route( 'no-zero', '/test', array(
111 | 'methods' => array( 'GET' ),
112 | 'callback' => '__return_null',
113 | 'args' => array(
114 | 'foo' => array(
115 | 'default' => 'bar',
116 | ),
117 | ),
118 | ) );
119 | $request = new WP_REST_Request( 'GET', '/no-zero/test' );
120 | $this->server->dispatch( $request );
121 | $this->assertEquals( array( 'foo' => 'bar' ), $request->get_params() );
122 | }
123 |
124 | public function test_head_request_handled_by_get() {
125 | register_rest_route( 'head-request', '/test', array(
126 | 'methods' => array( 'GET' ),
127 | 'callback' => '__return_true',
128 | ) );
129 | $request = new WP_REST_Request( 'HEAD', '/head-request/test' );
130 | $response = $this->server->dispatch( $request );
131 | $this->assertEquals( 200, $response->get_status() );
132 | }
133 |
134 | /**
135 | * Pass a capability which the user does not have, this should
136 | * result in a 403 error.
137 | */
138 | function test_rest_route_capability_authorization_fails() {
139 | register_rest_route( 'test-ns', '/test', array(
140 | 'methods' => 'GET',
141 | 'callback' => '__return_null',
142 | 'should_exist' => false,
143 | 'permission_callback' => array( $this, 'permission_denied' ),
144 | ) );
145 |
146 | $request = new WP_REST_Request( 'GET', '/test-ns/test', array() );
147 | $result = $this->server->dispatch( $request );
148 |
149 | $this->assertEquals( 403, $result->get_status() );
150 | }
151 |
152 | /**
153 | * An editor should be able to get access to an route with the
154 | * edit_posts capability.
155 | */
156 | function test_rest_route_capability_authorization() {
157 | register_rest_route( 'test-ns', '/test', array(
158 | 'methods' => 'GET',
159 | 'callback' => '__return_null',
160 | 'should_exist' => false,
161 | 'permission_callback' => '__return_true',
162 | ) );
163 |
164 | $editor = self::factory()->user->create( array( 'role' => 'editor' ) );
165 |
166 | $request = new WP_REST_Request( 'GET', '/test-ns/test', array() );
167 |
168 | wp_set_current_user( $editor );
169 |
170 | $result = $this->server->dispatch( $request );
171 |
172 | $this->assertEquals( 200, $result->get_status() );
173 | }
174 |
175 | /**
176 | * An "Allow" HTTP header should be sent with a request
177 | * for all available methods on that route.
178 | */
179 | function test_allow_header_sent() {
180 |
181 | register_rest_route( 'test-ns', '/test', array(
182 | 'methods' => 'GET',
183 | 'callback' => '__return_null',
184 | 'should_exist' => false,
185 | ) );
186 |
187 | $request = new WP_REST_Request( 'GET', '/test-ns/test', array() );
188 |
189 | $result = $this->server->dispatch( $request );
190 | $result = apply_filters( 'rest_post_dispatch', $result, $this->server, $request );
191 |
192 | $this->assertFalse( $result->get_status() !== 200 );
193 |
194 | $sent_headers = $result->get_headers();
195 | $this->assertEquals( $sent_headers['Allow'], 'GET' );
196 | }
197 |
198 | /**
199 | * The "Allow" HTTP header should include all available
200 | * methods that can be sent to a route.
201 | */
202 | function test_allow_header_sent_with_multiple_methods() {
203 |
204 | register_rest_route( 'test-ns', '/test', array(
205 | 'methods' => 'GET',
206 | 'callback' => '__return_null',
207 | 'should_exist' => false,
208 | ) );
209 |
210 | register_rest_route( 'test-ns', '/test', array(
211 | 'methods' => 'POST',
212 | 'callback' => '__return_null',
213 | 'should_exist' => false,
214 | ) );
215 |
216 | $request = new WP_REST_Request( 'GET', '/test-ns/test', array() );
217 |
218 | $result = $this->server->dispatch( $request );
219 |
220 | $this->assertFalse( $result->get_status() !== 200 );
221 |
222 | $result = apply_filters( 'rest_post_dispatch', $result, $this->server, $request );
223 |
224 | $sent_headers = $result->get_headers();
225 | $this->assertEquals( $sent_headers['Allow'], 'GET, POST' );
226 | }
227 |
228 | /**
229 | * The "Allow" HTTP header should NOT include other methods
230 | * which the user does not have access to.
231 | */
232 | function test_allow_header_send_only_permitted_methods() {
233 |
234 | register_rest_route( 'test-ns', '/test', array(
235 | 'methods' => 'GET',
236 | 'callback' => '__return_null',
237 | 'should_exist' => false,
238 | 'permission_callback' => array( $this, 'permission_denied' ),
239 | ) );
240 |
241 | register_rest_route( 'test-ns', '/test', array(
242 | 'methods' => 'POST',
243 | 'callback' => '__return_null',
244 | 'should_exist' => false,
245 | ) );
246 |
247 | $request = new WP_REST_Request( 'GET', '/test-ns/test', array() );
248 |
249 | $result = $this->server->dispatch( $request );
250 | $result = apply_filters( 'rest_post_dispatch', $result, $this->server, $request );
251 |
252 | $this->assertEquals( $result->get_status(), 403 );
253 |
254 | $sent_headers = $result->get_headers();
255 | $this->assertEquals( $sent_headers['Allow'], 'POST' );
256 | }
257 |
258 | public function permission_denied() {
259 | return new WP_Error( 'forbidden', 'You are not allowed to do this', array( 'status' => 403 ) );
260 | }
261 |
262 | public function test_error_to_response() {
263 | $code = 'wp-api-test-error';
264 | $message = 'Test error message for the API';
265 | $error = new WP_Error( $code, $message );
266 |
267 | $response = $this->server->error_to_response( $error );
268 | $this->assertInstanceOf( 'WP_REST_Response', $response );
269 |
270 | // Make sure we default to a 500 error.
271 | $this->assertEquals( 500, $response->get_status() );
272 |
273 | $data = $response->get_data();
274 |
275 | $this->assertEquals( $code, $data['code'] );
276 | $this->assertEquals( $message, $data['message'] );
277 | }
278 |
279 | public function test_error_to_response_with_status() {
280 | $code = 'wp-api-test-error';
281 | $message = 'Test error message for the API';
282 | $error = new WP_Error( $code, $message, array( 'status' => 400 ) );
283 |
284 | $response = $this->server->error_to_response( $error );
285 | $this->assertInstanceOf( 'WP_REST_Response', $response );
286 |
287 | $this->assertEquals( 400, $response->get_status() );
288 |
289 | $data = $response->get_data();
290 |
291 | $this->assertEquals( $code, $data['code'] );
292 | $this->assertEquals( $message, $data['message'] );
293 | }
294 |
295 | public function test_error_to_response_to_error() {
296 | $code = 'wp-api-test-error';
297 | $message = 'Test error message for the API';
298 | $code2 = 'wp-api-test-error-2';
299 | $message2 = 'Another test message';
300 | $error = new WP_Error( $code, $message, array( 'status' => 400 ) );
301 | $error->add( $code2, $message2, array( 'status' => 403 ) );
302 |
303 | $response = $this->server->error_to_response( $error );
304 | $this->assertInstanceOf( 'WP_REST_Response', $response );
305 |
306 | $this->assertEquals( 400, $response->get_status() );
307 |
308 | $error = $response->as_error();
309 | $this->assertInstanceOf( 'WP_Error', $error );
310 | $this->assertEquals( $code, $error->get_error_code() );
311 | $this->assertEquals( $message, $error->get_error_message() );
312 | $this->assertEquals( $message2, $error->errors[ $code2 ][0] );
313 | $this->assertEquals( array( 'status' => 403 ), $error->error_data[ $code2 ] );
314 | }
315 |
316 | public function test_rest_error() {
317 | $data = array(
318 | 'code' => 'wp-api-test-error',
319 | 'message' => 'Message text',
320 | );
321 | $expected = wp_json_encode( $data );
322 | $response = $this->server->json_error( 'wp-api-test-error', 'Message text' );
323 |
324 | $this->assertEquals( $expected, $response );
325 | }
326 |
327 | public function test_json_error_with_status() {
328 | $stub = $this->getMockBuilder( 'Spy_REST_Server' )
329 | ->setMethods( array( 'set_status' ) )
330 | ->getMock();
331 |
332 | $stub->expects( $this->once() )
333 | ->method( 'set_status' )
334 | ->with( $this->equalTo( 400 ) );
335 |
336 | $data = array(
337 | 'code' => 'wp-api-test-error',
338 | 'message' => 'Message text',
339 | );
340 | $expected = wp_json_encode( $data );
341 |
342 | $response = $stub->json_error( 'wp-api-test-error', 'Message text', 400 );
343 |
344 | $this->assertEquals( $expected, $response );
345 | }
346 |
347 | public function test_response_to_data_links() {
348 | $response = new WP_REST_Response();
349 | $response->add_link( 'self', 'http://example.com/' );
350 | $response->add_link( 'alternate', 'http://example.org/', array( 'type' => 'application/xml' ) );
351 |
352 | $data = $this->server->response_to_data( $response, false );
353 | $this->assertArrayHasKey( '_links', $data );
354 |
355 | $self = array(
356 | 'href' => 'http://example.com/',
357 | );
358 | $this->assertEquals( $self, $data['_links']['self'][0] );
359 |
360 | $alternate = array(
361 | 'href' => 'http://example.org/',
362 | 'type' => 'application/xml',
363 | );
364 | $this->assertEquals( $alternate, $data['_links']['alternate'][0] );
365 | }
366 |
367 | public function test_link_embedding() {
368 | // Register our testing route.
369 | $this->server->register_route( 'test', '/test/embeddable', array(
370 | 'methods' => 'GET',
371 | 'callback' => array( $this, 'embedded_response_callback' ),
372 | ) );
373 | $response = new WP_REST_Response();
374 |
375 | // External links should be ignored.
376 | $response->add_link( 'alternate', 'http://not-api.example.com/', array( 'embeddable' => true ) );
377 |
378 | // All others should be embedded.
379 | $response->add_link( 'alternate', rest_url( '/test/embeddable' ), array( 'embeddable' => true ) );
380 |
381 | $data = $this->server->response_to_data( $response, true );
382 | $this->assertArrayHasKey( '_embedded', $data );
383 |
384 | $alternate = $data['_embedded']['alternate'];
385 | $this->assertCount( 2, $alternate );
386 | $this->assertEmpty( $alternate[0] );
387 |
388 | $this->assertInternalType( 'array', $alternate[1] );
389 | $this->assertArrayNotHasKey( 'code', $alternate[1] );
390 | $this->assertTrue( $alternate[1]['hello'] );
391 |
392 | // Ensure the context is set to embed when requesting.
393 | $this->assertEquals( 'embed', $alternate[1]['parameters']['context'] );
394 | }
395 |
396 | /**
397 | * @depends test_link_embedding
398 | */
399 | public function test_link_embedding_self() {
400 | // Register our testing route.
401 | $this->server->register_route( 'test', '/test/embeddable', array(
402 | 'methods' => 'GET',
403 | 'callback' => array( $this, 'embedded_response_callback' ),
404 | ) );
405 | $response = new WP_REST_Response();
406 |
407 | // 'self' should be ignored.
408 | $response->add_link( 'self', rest_url( '/test/notembeddable' ), array( 'embeddable' => true ) );
409 |
410 | $data = $this->server->response_to_data( $response, true );
411 |
412 | $this->assertArrayNotHasKey( '_embedded', $data );
413 | }
414 |
415 | /**
416 | * @depends test_link_embedding
417 | */
418 | public function test_link_embedding_params() {
419 | // Register our testing route.
420 | $this->server->register_route( 'test', '/test/embeddable', array(
421 | 'methods' => 'GET',
422 | 'callback' => array( $this, 'embedded_response_callback' ),
423 | ) );
424 |
425 | $response = new WP_REST_Response();
426 | $response->add_link( 'alternate', rest_url( '/test/embeddable?parsed_params=yes' ), array( 'embeddable' => true ) );
427 |
428 | $data = $this->server->response_to_data( $response, true );
429 |
430 | $this->assertArrayHasKey( '_embedded', $data );
431 | $this->assertArrayHasKey( 'alternate', $data['_embedded'] );
432 | $data = $data['_embedded']['alternate'][0];
433 |
434 | $this->assertEquals( 'yes', $data['parameters']['parsed_params'] );
435 | }
436 |
437 | /**
438 | * @depends test_link_embedding_params
439 | */
440 | public function test_link_embedding_error() {
441 | // Register our testing route.
442 | $this->server->register_route( 'test', '/test/embeddable', array(
443 | 'methods' => 'GET',
444 | 'callback' => array( $this, 'embedded_response_callback' ),
445 | ) );
446 |
447 | $response = new WP_REST_Response();
448 | $response->add_link( 'up', rest_url( '/test/embeddable?error=1' ), array( 'embeddable' => true ) );
449 |
450 | $data = $this->server->response_to_data( $response, true );
451 |
452 | $this->assertArrayHasKey( '_embedded', $data );
453 | $this->assertArrayHasKey( 'up', $data['_embedded'] );
454 |
455 | // Check that errors are embedded correctly.
456 | $up = $data['_embedded']['up'];
457 | $this->assertCount( 1, $up );
458 |
459 | $up_data = $up[0];
460 | $this->assertEquals( 'wp-api-test-error', $up_data['code'] );
461 | $this->assertEquals( 'Test message', $up_data['message'] );
462 | $this->assertEquals( 403, $up_data['data']['status'] );
463 | }
464 |
465 | /**
466 | * Ensure embedding is a no-op without links in the data.
467 | */
468 | public function test_link_embedding_without_links() {
469 | $data = array(
470 | 'untouched' => 'data',
471 | );
472 | $result = $this->server->embed_links( $data );
473 |
474 | $this->assertArrayNotHasKey( '_links', $data );
475 | $this->assertArrayNotHasKey( '_embedded', $data );
476 | $this->assertEquals( 'data', $data['untouched'] );
477 | }
478 |
479 | public function embedded_response_callback( $request ) {
480 | $params = $request->get_params();
481 |
482 | if ( isset( $params['error'] ) ) {
483 | return new WP_Error( 'wp-api-test-error', 'Test message', array( 'status' => 403 ) );
484 | }
485 |
486 | $data = array(
487 | 'hello' => true,
488 | 'parameters' => $params,
489 | );
490 |
491 | return $data;
492 | }
493 |
494 | public function test_removing_links() {
495 | $response = new WP_REST_Response();
496 | $response->add_link( 'self', 'http://example.com/' );
497 | $response->add_link( 'alternate', 'http://example.org/', array( 'type' => 'application/xml' ) );
498 |
499 | $response->remove_link( 'self' );
500 |
501 | $data = $this->server->response_to_data( $response, false );
502 | $this->assertArrayHasKey( '_links', $data );
503 |
504 | $this->assertArrayNotHasKey( 'self', $data['_links'] );
505 |
506 | $alternate = array(
507 | 'href' => 'http://example.org/',
508 | 'type' => 'application/xml',
509 | );
510 | $this->assertEquals( $alternate, $data['_links']['alternate'][0] );
511 | }
512 |
513 | public function test_removing_links_for_href() {
514 | $response = new WP_REST_Response();
515 | $response->add_link( 'self', 'http://example.com/' );
516 | $response->add_link( 'self', 'https://example.com/' );
517 |
518 | $response->remove_link( 'self', 'https://example.com/' );
519 |
520 | $data = $this->server->response_to_data( $response, false );
521 | $this->assertArrayHasKey( '_links', $data );
522 |
523 | $this->assertArrayHasKey( 'self', $data['_links'] );
524 |
525 | $self_not_filtered = array(
526 | 'href' => 'http://example.com/',
527 | );
528 | $this->assertEquals( $self_not_filtered, $data['_links']['self'][0] );
529 | }
530 |
531 | public function test_get_index() {
532 | $server = new WP_REST_Server();
533 | $server->register_route( 'test/example', '/test/example/some-route', array(
534 | array(
535 | 'methods' => WP_REST_Server::READABLE,
536 | 'callback' => '__return_true',
537 | ),
538 | array(
539 | 'methods' => WP_REST_Server::DELETABLE,
540 | 'callback' => '__return_true',
541 | ),
542 | ) );
543 |
544 | $request = new WP_REST_Request( 'GET', '/' );
545 | $index = $server->dispatch( $request );
546 | $data = $index->get_data();
547 |
548 | $this->assertArrayHasKey( 'name', $data );
549 | $this->assertArrayHasKey( 'description', $data );
550 | $this->assertArrayHasKey( 'url', $data );
551 | $this->assertArrayHasKey( 'namespaces', $data );
552 | $this->assertArrayHasKey( 'authentication', $data );
553 | $this->assertArrayHasKey( 'routes', $data );
554 |
555 | // Check namespace data.
556 | $this->assertContains( 'test/example', $data['namespaces'] );
557 |
558 | // Check the route.
559 | $this->assertArrayHasKey( '/test/example/some-route', $data['routes'] );
560 | $route = $data['routes']['/test/example/some-route'];
561 | $this->assertEquals( 'test/example', $route['namespace'] );
562 | $this->assertArrayHasKey( 'methods', $route );
563 | $this->assertContains( 'GET', $route['methods'] );
564 | $this->assertContains( 'DELETE', $route['methods'] );
565 | $this->assertArrayHasKey( '_links', $route );
566 | }
567 |
568 | public function test_get_namespace_index() {
569 | $server = new WP_REST_Server();
570 | $server->register_route( 'test/example', '/test/example/some-route', array(
571 | array(
572 | 'methods' => WP_REST_Server::READABLE,
573 | 'callback' => '__return_true',
574 | ),
575 | array(
576 | 'methods' => WP_REST_Server::DELETABLE,
577 | 'callback' => '__return_true',
578 | ),
579 | ) );
580 | $server->register_route( 'test/another', '/test/another/route', array(
581 | array(
582 | 'methods' => WP_REST_Server::READABLE,
583 | 'callback' => '__return_false',
584 | ),
585 | ) );
586 |
587 | $request = new WP_REST_Request();
588 | $request->set_param( 'namespace', 'test/example' );
589 | $index = rest_ensure_response( $server->get_namespace_index( $request ) );
590 | $data = $index->get_data();
591 |
592 | // Check top-level.
593 | $this->assertEquals( 'test/example', $data['namespace'] );
594 | $this->assertArrayHasKey( 'routes', $data );
595 |
596 | // Check we have the route we expect...
597 | $this->assertArrayHasKey( '/test/example/some-route', $data['routes'] );
598 |
599 | // ...and none we don't.
600 | $this->assertArrayNotHasKey( '/test/another/route', $data['routes'] );
601 | }
602 |
603 | public function test_get_namespaces() {
604 | $server = new WP_REST_Server();
605 | $server->register_route( 'test/example', '/test/example/some-route', array(
606 | array(
607 | 'methods' => WP_REST_Server::READABLE,
608 | 'callback' => '__return_true',
609 | ),
610 | ) );
611 | $server->register_route( 'test/another', '/test/another/route', array(
612 | array(
613 | 'methods' => WP_REST_Server::READABLE,
614 | 'callback' => '__return_false',
615 | ),
616 | ) );
617 |
618 | $namespaces = $server->get_namespaces();
619 | $this->assertContains( 'test/example', $namespaces );
620 | $this->assertContains( 'test/another', $namespaces );
621 | }
622 |
623 | public function test_nocache_headers_on_authenticated_requests() {
624 | $editor = self::factory()->user->create( array( 'role' => 'editor' ) );
625 | $request = new WP_REST_Request( 'GET', '/', array() );
626 | wp_set_current_user( $editor );
627 |
628 | $result = $this->server->serve_request('/');
629 | $headers = $this->server->sent_headers;
630 |
631 | foreach ( wp_get_nocache_headers() as $header => $value ) {
632 | $this->assertTrue( isset( $headers[ $header ] ), sprintf( 'Header %s is not present in the response.', $header ) );
633 | $this->assertEquals( $value, $headers[ $header ] );
634 | }
635 | }
636 |
637 | public function test_no_nocache_headers_on_unauthenticated_requests() {
638 | $editor = self::factory()->user->create( array( 'role' => 'editor' ) );
639 | $request = new WP_REST_Request( 'GET', '/', array() );
640 |
641 | $result = $this->server->serve_request('/');
642 | $headers = $this->server->sent_headers;
643 |
644 | foreach ( wp_get_nocache_headers() as $header => $value ) {
645 | $this->assertFalse( isset( $headers[ $header ] ) && $headers[ $header ] === $value, sprintf( 'Header %s is set to nocache.', $header ) );
646 | }
647 | }
648 | }
649 |
--------------------------------------------------------------------------------
/wp-includes/rest-api/class-wp-rest-request.php:
--------------------------------------------------------------------------------
1 | params = array(
117 | 'URL' => array(),
118 | 'GET' => array(),
119 | 'POST' => array(),
120 | 'FILES' => array(),
121 |
122 | // See parse_json_params.
123 | 'JSON' => null,
124 |
125 | 'defaults' => array(),
126 | );
127 |
128 | $this->set_method( $method );
129 | $this->set_route( $route );
130 | $this->set_attributes( $attributes );
131 | }
132 |
133 | /**
134 | * Retrieves the HTTP method for the request.
135 | *
136 | * @since 4.4.0
137 | * @access public
138 | *
139 | * @return string HTTP method.
140 | */
141 | public function get_method() {
142 | return $this->method;
143 | }
144 |
145 | /**
146 | * Sets HTTP method for the request.
147 | *
148 | * @since 4.4.0
149 | * @access public
150 | *
151 | * @param string $method HTTP method.
152 | */
153 | public function set_method( $method ) {
154 | $this->method = strtoupper( $method );
155 | }
156 |
157 | /**
158 | * Retrieves all headers from the request.
159 | *
160 | * @since 4.4.0
161 | * @access public
162 | *
163 | * @return array Map of key to value. Key is always lowercase, as per HTTP specification.
164 | */
165 | public function get_headers() {
166 | return $this->headers;
167 | }
168 |
169 | /**
170 | * Canonicalizes the header name.
171 | *
172 | * Ensures that header names are always treated the same regardless of
173 | * source. Header names are always case insensitive.
174 | *
175 | * Note that we treat `-` (dashes) and `_` (underscores) as the same
176 | * character, as per header parsing rules in both Apache and nginx.
177 | *
178 | * @link http://stackoverflow.com/q/18185366
179 | * @link http://wiki.nginx.org/Pitfalls#Missing_.28disappearing.29_HTTP_headers
180 | * @link http://nginx.org/en/docs/http/ngx_http_core_module.html#underscores_in_headers
181 | *
182 | * @since 4.4.0
183 | * @access public
184 | * @static
185 | *
186 | * @param string $key Header name.
187 | * @return string Canonicalized name.
188 | */
189 | public static function canonicalize_header_name( $key ) {
190 | $key = strtolower( $key );
191 | $key = str_replace( '-', '_', $key );
192 |
193 | return $key;
194 | }
195 |
196 | /**
197 | * Retrieves the given header from the request.
198 | *
199 | * If the header has multiple values, they will be concatenated with a comma
200 | * as per the HTTP specification. Be aware that some non-compliant headers
201 | * (notably cookie headers) cannot be joined this way.
202 | *
203 | * @since 4.4.0
204 | * @access public
205 | *
206 | * @param string $key Header name, will be canonicalized to lowercase.
207 | * @return string|null String value if set, null otherwise.
208 | */
209 | public function get_header( $key ) {
210 | $key = $this->canonicalize_header_name( $key );
211 |
212 | if ( ! isset( $this->headers[ $key ] ) ) {
213 | return null;
214 | }
215 |
216 | return implode( ',', $this->headers[ $key ] );
217 | }
218 |
219 | /**
220 | * Retrieves header values from the request.
221 | *
222 | * @since 4.4.0
223 | * @access public
224 | *
225 | * @param string $key Header name, will be canonicalized to lowercase.
226 | * @return array|null List of string values if set, null otherwise.
227 | */
228 | public function get_header_as_array( $key ) {
229 | $key = $this->canonicalize_header_name( $key );
230 |
231 | if ( ! isset( $this->headers[ $key ] ) ) {
232 | return null;
233 | }
234 |
235 | return $this->headers[ $key ];
236 | }
237 |
238 | /**
239 | * Sets the header on request.
240 | *
241 | * @since 4.4.0
242 | * @access public
243 | *
244 | * @param string $key Header name.
245 | * @param string $value Header value, or list of values.
246 | */
247 | public function set_header( $key, $value ) {
248 | $key = $this->canonicalize_header_name( $key );
249 | $value = (array) $value;
250 |
251 | $this->headers[ $key ] = $value;
252 | }
253 |
254 | /**
255 | * Appends a header value for the given header.
256 | *
257 | * @since 4.4.0
258 | * @access public
259 | *
260 | * @param string $key Header name.
261 | * @param string $value Header value, or list of values.
262 | */
263 | public function add_header( $key, $value ) {
264 | $key = $this->canonicalize_header_name( $key );
265 | $value = (array) $value;
266 |
267 | if ( ! isset( $this->headers[ $key ] ) ) {
268 | $this->headers[ $key ] = array();
269 | }
270 |
271 | $this->headers[ $key ] = array_merge( $this->headers[ $key ], $value );
272 | }
273 |
274 | /**
275 | * Removes all values for a header.
276 | *
277 | * @since 4.4.0
278 | * @access public
279 | *
280 | * @param string $key Header name.
281 | */
282 | public function remove_header( $key ) {
283 | unset( $this->headers[ $key ] );
284 | }
285 |
286 | /**
287 | * Sets headers on the request.
288 | *
289 | * @since 4.4.0
290 | * @access public
291 | *
292 | * @param array $headers Map of header name to value.
293 | * @param bool $override If true, replace the request's headers. Otherwise, merge with existing.
294 | */
295 | public function set_headers( $headers, $override = true ) {
296 | if ( true === $override ) {
297 | $this->headers = array();
298 | }
299 |
300 | foreach ( $headers as $key => $value ) {
301 | $this->set_header( $key, $value );
302 | }
303 | }
304 |
305 | /**
306 | * Retrieves the content-type of the request.
307 | *
308 | * @since 4.4.0
309 | * @access public
310 | *
311 | * @return array Map containing 'value' and 'parameters' keys.
312 | */
313 | public function get_content_type() {
314 | $value = $this->get_header( 'content-type' );
315 | if ( empty( $value ) ) {
316 | return null;
317 | }
318 |
319 | $parameters = '';
320 | if ( strpos( $value, ';' ) ) {
321 | list( $value, $parameters ) = explode( ';', $value, 2 );
322 | }
323 |
324 | $value = strtolower( $value );
325 | if ( strpos( $value, '/' ) === false ) {
326 | return null;
327 | }
328 |
329 | // Parse type and subtype out.
330 | list( $type, $subtype ) = explode( '/', $value, 2 );
331 |
332 | $data = compact( 'value', 'type', 'subtype', 'parameters' );
333 | $data = array_map( 'trim', $data );
334 |
335 | return $data;
336 | }
337 |
338 | /**
339 | * Retrieves the parameter priority order.
340 | *
341 | * Used when checking parameters in get_param().
342 | *
343 | * @since 4.4.0
344 | * @access protected
345 | *
346 | * @return array List of types to check, in order of priority.
347 | */
348 | protected function get_parameter_order() {
349 | $order = array();
350 | $order[] = 'JSON';
351 |
352 | $this->parse_json_params();
353 |
354 | // Ensure we parse the body data.
355 | $body = $this->get_body();
356 | if ( $this->method !== 'POST' && ! empty( $body ) ) {
357 | $this->parse_body_params();
358 | }
359 |
360 | $accepts_body_data = array( 'POST', 'PUT', 'PATCH' );
361 | if ( in_array( $this->method, $accepts_body_data ) ) {
362 | $order[] = 'POST';
363 | }
364 |
365 | $order[] = 'GET';
366 | $order[] = 'URL';
367 | $order[] = 'defaults';
368 |
369 | /**
370 | * Filter the parameter order.
371 | *
372 | * The order affects which parameters are checked when using get_param() and family.
373 | * This acts similarly to PHP's `request_order` setting.
374 | *
375 | * @since 4.4.0
376 | *
377 | * @param array $order {
378 | * An array of types to check, in order of priority.
379 | *
380 | * @param string $type The type to check.
381 | * }
382 | * @param WP_REST_Request $this The request object.
383 | */
384 | return apply_filters( 'rest_request_parameter_order', $order, $this );
385 | }
386 |
387 | /**
388 | * Retrieves a parameter from the request.
389 | *
390 | * @since 4.4.0
391 | * @access public
392 | *
393 | * @param string $key Parameter name.
394 | * @return mixed|null Value if set, null otherwise.
395 | */
396 | public function get_param( $key ) {
397 | $order = $this->get_parameter_order();
398 |
399 | foreach ( $order as $type ) {
400 | // Determine if we have the parameter for this type.
401 | if ( isset( $this->params[ $type ][ $key ] ) ) {
402 | return $this->params[ $type ][ $key ];
403 | }
404 | }
405 |
406 | return null;
407 | }
408 |
409 | /**
410 | * Sets a parameter on the request.
411 | *
412 | * @since 4.4.0
413 | * @access public
414 | *
415 | * @param string $key Parameter name.
416 | * @param mixed $value Parameter value.
417 | */
418 | public function set_param( $key, $value ) {
419 | switch ( $this->method ) {
420 | case 'POST':
421 | $this->params['POST'][ $key ] = $value;
422 | break;
423 |
424 | default:
425 | $this->params['GET'][ $key ] = $value;
426 | break;
427 | }
428 | }
429 |
430 | /**
431 | * Retrieves merged parameters from the request.
432 | *
433 | * The equivalent of get_param(), but returns all parameters for the request.
434 | * Handles merging all the available values into a single array.
435 | *
436 | * @since 4.4.0
437 | * @access public
438 | *
439 | * @return array Map of key to value.
440 | */
441 | public function get_params() {
442 | $order = $this->get_parameter_order();
443 | $order = array_reverse( $order, true );
444 |
445 | $params = array();
446 | foreach ( $order as $type ) {
447 | $params = array_merge( $params, (array) $this->params[ $type ] );
448 | }
449 |
450 | return $params;
451 | }
452 |
453 | /**
454 | * Retrieves parameters from the route itself.
455 | *
456 | * These are parsed from the URL using the regex.
457 | *
458 | * @since 4.4.0
459 | * @access public
460 | *
461 | * @return array Parameter map of key to value.
462 | */
463 | public function get_url_params() {
464 | return $this->params['URL'];
465 | }
466 |
467 | /**
468 | * Sets parameters from the route.
469 | *
470 | * Typically, this is set after parsing the URL.
471 | *
472 | * @since 4.4.0
473 | * @access public
474 | *
475 | * @param array $params Parameter map of key to value.
476 | */
477 | public function set_url_params( $params ) {
478 | $this->params['URL'] = $params;
479 | }
480 |
481 | /**
482 | * Retrieves parameters from the query string.
483 | *
484 | * These are the parameters you'd typically find in `$_GET`.
485 | *
486 | * @since 4.4.0
487 | * @access public
488 | *
489 | * @return array Parameter map of key to value
490 | */
491 | public function get_query_params() {
492 | return $this->params['GET'];
493 | }
494 |
495 | /**
496 | * Sets parameters from the query string.
497 | *
498 | * Typically, this is set from `$_GET`.
499 | *
500 | * @since 4.4.0
501 | * @access public
502 | *
503 | * @param array $params Parameter map of key to value.
504 | */
505 | public function set_query_params( $params ) {
506 | $this->params['GET'] = $params;
507 | }
508 |
509 | /**
510 | * Retrieves parameters from the body.
511 | *
512 | * These are the parameters you'd typically find in `$_POST`.
513 | *
514 | * @since 4.4.0
515 | * @access public
516 | *
517 | * @return array Parameter map of key to value.
518 | */
519 | public function get_body_params() {
520 | return $this->params['POST'];
521 | }
522 |
523 | /**
524 | * Sets parameters from the body.
525 | *
526 | * Typically, this is set from `$_POST`.
527 | *
528 | * @since 4.4.0
529 | * @access public
530 | *
531 | * @param array $params Parameter map of key to value.
532 | */
533 | public function set_body_params( $params ) {
534 | $this->params['POST'] = $params;
535 | }
536 |
537 | /**
538 | * Retrieves multipart file parameters from the body.
539 | *
540 | * These are the parameters you'd typically find in `$_FILES`.
541 | *
542 | * @since 4.4.0
543 | * @access public
544 | *
545 | * @return array Parameter map of key to value
546 | */
547 | public function get_file_params() {
548 | return $this->params['FILES'];
549 | }
550 |
551 | /**
552 | * Sets multipart file parameters from the body.
553 | *
554 | * Typically, this is set from `$_FILES`.
555 | *
556 | * @since 4.4.0
557 | * @access public
558 | *
559 | * @param array $params Parameter map of key to value.
560 | */
561 | public function set_file_params( $params ) {
562 | $this->params['FILES'] = $params;
563 | }
564 |
565 | /**
566 | * Retrieves the default parameters.
567 | *
568 | * These are the parameters set in the route registration.
569 | *
570 | * @since 4.4.0
571 | * @access public
572 | *
573 | * @return array Parameter map of key to value
574 | */
575 | public function get_default_params() {
576 | return $this->params['defaults'];
577 | }
578 |
579 | /**
580 | * Sets default parameters.
581 | *
582 | * These are the parameters set in the route registration.
583 | *
584 | * @since 4.4.0
585 | * @access public
586 | *
587 | * @param array $params Parameter map of key to value.
588 | */
589 | public function set_default_params( $params ) {
590 | $this->params['defaults'] = $params;
591 | }
592 |
593 | /**
594 | * Retrieves the request body content.
595 | *
596 | * @since 4.4.0
597 | * @access public
598 | *
599 | * @return string Binary data from the request body.
600 | */
601 | public function get_body() {
602 | return $this->body;
603 | }
604 |
605 | /**
606 | * Sets body content.
607 | *
608 | * @since 4.4.0
609 | * @access public
610 | *
611 | * @param string $data Binary data from the request body.
612 | */
613 | public function set_body( $data ) {
614 | $this->body = $data;
615 |
616 | // Enable lazy parsing.
617 | $this->parsed_json = false;
618 | $this->parsed_body = false;
619 | $this->params['JSON'] = null;
620 | }
621 |
622 | /**
623 | * Retrieves the parameters from a JSON-formatted body.
624 | *
625 | * @since 4.4.0
626 | * @access public
627 | *
628 | * @return array Parameter map of key to value.
629 | */
630 | public function get_json_params() {
631 | // Ensure the parameters have been parsed out.
632 | $this->parse_json_params();
633 |
634 | return $this->params['JSON'];
635 | }
636 |
637 | /**
638 | * Parses the JSON parameters.
639 | *
640 | * Avoids parsing the JSON data until we need to access it.
641 | *
642 | * @since 4.4.0
643 | * @access protected
644 | */
645 | protected function parse_json_params() {
646 | if ( $this->parsed_json ) {
647 | return;
648 | }
649 |
650 | $this->parsed_json = true;
651 |
652 | // Check that we actually got JSON.
653 | $content_type = $this->get_content_type();
654 |
655 | if ( empty( $content_type ) || 'application/json' !== $content_type['value'] ) {
656 | return;
657 | }
658 |
659 | $params = json_decode( $this->get_body(), true );
660 |
661 | /*
662 | * Check for a parsing error.
663 | *
664 | * Note that due to WP's JSON compatibility functions, json_last_error
665 | * might not be defined: https://core.trac.wordpress.org/ticket/27799
666 | */
667 | if ( null === $params && ( ! function_exists( 'json_last_error' ) || JSON_ERROR_NONE !== json_last_error() ) ) {
668 | return;
669 | }
670 |
671 | $this->params['JSON'] = $params;
672 | }
673 |
674 | /**
675 | * Parses the request body parameters.
676 | *
677 | * Parses out URL-encoded bodies for request methods that aren't supported
678 | * natively by PHP. In PHP 5.x, only POST has these parsed automatically.
679 | *
680 | * @since 4.4.0
681 | * @access protected
682 | */
683 | protected function parse_body_params() {
684 | if ( $this->parsed_body ) {
685 | return;
686 | }
687 |
688 | $this->parsed_body = true;
689 |
690 | /*
691 | * Check that we got URL-encoded. Treat a missing content-type as
692 | * URL-encoded for maximum compatibility.
693 | */
694 | $content_type = $this->get_content_type();
695 |
696 | if ( ! empty( $content_type ) && 'application/x-www-form-urlencoded' !== $content_type['value'] ) {
697 | return;
698 | }
699 |
700 | parse_str( $this->get_body(), $params );
701 |
702 | /*
703 | * Amazingly, parse_str follows magic quote rules. Sigh.
704 | *
705 | * NOTE: Do not refactor to use `wp_unslash`.
706 | */
707 | if ( get_magic_quotes_gpc() ) {
708 | $params = stripslashes_deep( $params );
709 | }
710 |
711 | /*
712 | * Add to the POST parameters stored internally. If a user has already
713 | * set these manually (via `set_body_params`), don't override them.
714 | */
715 | $this->params['POST'] = array_merge( $params, $this->params['POST'] );
716 | }
717 |
718 | /**
719 | * Retrieves the route that matched the request.
720 | *
721 | * @since 4.4.0
722 | * @access public
723 | *
724 | * @return string Route matching regex.
725 | */
726 | public function get_route() {
727 | return $this->route;
728 | }
729 |
730 | /**
731 | * Sets the route that matched the request.
732 | *
733 | * @since 4.4.0
734 | * @access public
735 | *
736 | * @param string $route Route matching regex.
737 | */
738 | public function set_route( $route ) {
739 | $this->route = $route;
740 | }
741 |
742 | /**
743 | * Retrieves the attributes for the request.
744 | *
745 | * These are the options for the route that was matched.
746 | *
747 | * @since 4.4.0
748 | * @access public
749 | *
750 | * @return array Attributes for the request.
751 | */
752 | public function get_attributes() {
753 | return $this->attributes;
754 | }
755 |
756 | /**
757 | * Sets the attributes for the request.
758 | *
759 | * @since 4.4.0
760 | * @access public
761 | *
762 | * @param array $attributes Attributes for the request.
763 | */
764 | public function set_attributes( $attributes ) {
765 | $this->attributes = $attributes;
766 | }
767 |
768 | /**
769 | * Sanitizes (where possible) the params on the request.
770 | *
771 | * This is primarily based off the sanitize_callback param on each registered
772 | * argument.
773 | *
774 | * @since 4.4.0
775 | * @access public
776 | *
777 | * @return true|null True if there are no parameters to sanitize, null otherwise.
778 | */
779 | public function sanitize_params() {
780 |
781 | $attributes = $this->get_attributes();
782 |
783 | // No arguments set, skip sanitizing.
784 | if ( empty( $attributes['args'] ) ) {
785 | return true;
786 | }
787 |
788 | $order = $this->get_parameter_order();
789 |
790 | foreach ( $order as $type ) {
791 | if ( empty( $this->params[ $type ] ) ) {
792 | continue;
793 | }
794 | foreach ( $this->params[ $type ] as $key => $value ) {
795 | // Check if this param has a sanitize_callback added.
796 | if ( isset( $attributes['args'][ $key ] ) && ! empty( $attributes['args'][ $key ]['sanitize_callback'] ) ) {
797 | $this->params[ $type ][ $key ] = call_user_func( $attributes['args'][ $key ]['sanitize_callback'], $value, $this, $key );
798 | }
799 | }
800 | }
801 | return null;
802 | }
803 |
804 | /**
805 | * Checks whether this request is valid according to its attributes.
806 | *
807 | * @since 4.4.0
808 | * @access public
809 | *
810 | * @return bool|WP_Error True if there are no parameters to validate or if all pass validation,
811 | * WP_Error if required parameters are missing.
812 | */
813 | public function has_valid_params() {
814 |
815 | $attributes = $this->get_attributes();
816 | $required = array();
817 |
818 | // No arguments set, skip validation.
819 | if ( empty( $attributes['args'] ) ) {
820 | return true;
821 | }
822 |
823 | foreach ( $attributes['args'] as $key => $arg ) {
824 |
825 | $param = $this->get_param( $key );
826 | if ( isset( $arg['required'] ) && true === $arg['required'] && null === $param ) {
827 | $required[] = $key;
828 | }
829 | }
830 |
831 | if ( ! empty( $required ) ) {
832 | return new WP_Error( 'rest_missing_callback_param', sprintf( __( 'Missing parameter(s): %s' ), implode( ', ', $required ) ), array( 'status' => 400, 'params' => $required ) );
833 | }
834 |
835 | /*
836 | * Check the validation callbacks for each registered arg.
837 | *
838 | * This is done after required checking as required checking is cheaper.
839 | */
840 | $invalid_params = array();
841 |
842 | foreach ( $attributes['args'] as $key => $arg ) {
843 |
844 | $param = $this->get_param( $key );
845 |
846 | if ( null !== $param && ! empty( $arg['validate_callback'] ) ) {
847 | $valid_check = call_user_func( $arg['validate_callback'], $param, $this, $key );
848 |
849 | if ( false === $valid_check ) {
850 | $invalid_params[ $key ] = __( 'Invalid parameter.' );
851 | }
852 |
853 | if ( is_wp_error( $valid_check ) ) {
854 | $invalid_params[] = sprintf( '%s (%s)', $key, $valid_check->get_error_message() );
855 | }
856 | }
857 | }
858 |
859 | if ( $invalid_params ) {
860 | return new WP_Error( 'rest_invalid_param', sprintf( __( 'Invalid parameter(s): %s' ), implode( ', ', $invalid_params ) ), array( 'status' => 400, 'params' => $invalid_params ) );
861 | }
862 |
863 | return true;
864 |
865 | }
866 |
867 | /**
868 | * Checks if a parameter is set.
869 | *
870 | * @since 4.4.0
871 | * @access public
872 | *
873 | * @param string $offset Parameter name.
874 | * @return bool Whether the parameter is set.
875 | */
876 | public function offsetExists( $offset ) {
877 | $order = $this->get_parameter_order();
878 |
879 | foreach ( $order as $type ) {
880 | if ( isset( $this->params[ $type ][ $offset ] ) ) {
881 | return true;
882 | }
883 | }
884 |
885 | return false;
886 | }
887 |
888 | /**
889 | * Retrieves a parameter from the request.
890 | *
891 | * @since 4.4.0
892 | * @access public
893 | *
894 | * @param string $offset Parameter name.
895 | * @return mixed|null Value if set, null otherwise.
896 | */
897 | public function offsetGet( $offset ) {
898 | return $this->get_param( $offset );
899 | }
900 |
901 | /**
902 | * Sets a parameter on the request.
903 | *
904 | * @since 4.4.0
905 | * @access public
906 | *
907 | * @param string $offset Parameter name.
908 | * @param mixed $value Parameter value.
909 | */
910 | public function offsetSet( $offset, $value ) {
911 | $this->set_param( $offset, $value );
912 | }
913 |
914 | /**
915 | * Removes a parameter from the request.
916 | *
917 | * @since 4.4.0
918 | * @access public
919 | *
920 | * @param string $offset Parameter name.
921 | */
922 | public function offsetUnset( $offset ) {
923 | $order = $this->get_parameter_order();
924 |
925 | // Remove the offset from every group.
926 | foreach ( $order as $type ) {
927 | unset( $this->params[ $type ][ $offset ] );
928 | }
929 | }
930 | }
931 |
--------------------------------------------------------------------------------
/wp-includes/rest-api/class-wp-rest-server.php:
--------------------------------------------------------------------------------
1 | endpoints = array(
92 | // Meta endpoints.
93 | '/' => array(
94 | 'callback' => array( $this, 'get_index' ),
95 | 'methods' => 'GET',
96 | 'args' => array(
97 | 'context' => array(
98 | 'default' => 'view',
99 | ),
100 | ),
101 | ),
102 | );
103 | }
104 |
105 |
106 | /**
107 | * Checks the authentication headers if supplied.
108 | *
109 | * @since 4.4.0
110 | * @access public
111 | *
112 | * @return WP_Error|null WP_Error indicates unsuccessful login, null indicates successful
113 | * or no authentication provided
114 | */
115 | public function check_authentication() {
116 | /**
117 | * Pass an authentication error to the API
118 | *
119 | * This is used to pass a WP_Error from an authentication method back to
120 | * the API.
121 | *
122 | * Authentication methods should check first if they're being used, as
123 | * multiple authentication methods can be enabled on a site (cookies,
124 | * HTTP basic auth, OAuth). If the authentication method hooked in is
125 | * not actually being attempted, null should be returned to indicate
126 | * another authentication method should check instead. Similarly,
127 | * callbacks should ensure the value is `null` before checking for
128 | * errors.
129 | *
130 | * A WP_Error instance can be returned if an error occurs, and this should
131 | * match the format used by API methods internally (that is, the `status`
132 | * data should be used). A callback can return `true` to indicate that
133 | * the authentication method was used, and it succeeded.
134 | *
135 | * @since 4.4.0
136 | *
137 | * @param WP_Error|null|bool WP_Error if authentication error, null if authentication
138 | * method wasn't used, true if authentication succeeded.
139 | */
140 | return apply_filters( 'rest_authentication_errors', null );
141 | }
142 |
143 | /**
144 | * Converts an error to a response object.
145 | *
146 | * This iterates over all error codes and messages to change it into a flat
147 | * array. This enables simpler client behaviour, as it is represented as a
148 | * list in JSON rather than an object/map.
149 | *
150 | * @since 4.4.0
151 | * @access protected
152 | *
153 | * @param WP_Error $error WP_Error instance.
154 | * @return WP_REST_Response List of associative arrays with code and message keys.
155 | */
156 | protected function error_to_response( $error ) {
157 | $error_data = $error->get_error_data();
158 |
159 | if ( is_array( $error_data ) && isset( $error_data['status'] ) ) {
160 | $status = $error_data['status'];
161 | } else {
162 | $status = 500;
163 | }
164 |
165 | $errors = array();
166 |
167 | foreach ( (array) $error->errors as $code => $messages ) {
168 | foreach ( (array) $messages as $message ) {
169 | $errors[] = array( 'code' => $code, 'message' => $message, 'data' => $error->get_error_data( $code ) );
170 | }
171 | }
172 |
173 | $data = $errors[0];
174 | if ( count( $errors ) > 1 ) {
175 | // Remove the primary error.
176 | array_shift( $errors );
177 | $data['additional_errors'] = $errors;
178 | }
179 |
180 | $response = new WP_REST_Response( $data, $status );
181 |
182 | return $response;
183 | }
184 |
185 | /**
186 | * Retrieves an appropriate error representation in JSON.
187 | *
188 | * Note: This should only be used in WP_REST_Server::serve_request(), as it
189 | * cannot handle WP_Error internally. All callbacks and other internal methods
190 | * should instead return a WP_Error with the data set to an array that includes
191 | * a 'status' key, with the value being the HTTP status to send.
192 | *
193 | * @since 4.4.0
194 | * @access protected
195 | *
196 | * @param string $code WP_Error-style code.
197 | * @param string $message Human-readable message.
198 | * @param int $status Optional. HTTP status code to send. Default null.
199 | * @return string JSON representation of the error
200 | */
201 | protected function json_error( $code, $message, $status = null ) {
202 | if ( $status ) {
203 | $this->set_status( $status );
204 | }
205 |
206 | $error = compact( 'code', 'message' );
207 |
208 | return wp_json_encode( $error );
209 | }
210 |
211 | /**
212 | * Handles serving an API request.
213 | *
214 | * Matches the current server URI to a route and runs the first matching
215 | * callback then outputs a JSON representation of the returned value.
216 | *
217 | * @since 4.4.0
218 | * @access public
219 | *
220 | * @see WP_REST_Server::dispatch()
221 | *
222 | * @param string $path Optional. The request route. If not set, `$_SERVER['PATH_INFO']` will be used.
223 | * Default null.
224 | * @return false|null Null if not served and a HEAD request, false otherwise.
225 | */
226 | public function serve_request( $path = null ) {
227 | $content_type = isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json';
228 | $this->send_header( 'Content-Type', $content_type . '; charset=' . get_option( 'blog_charset' ) );
229 |
230 | /*
231 | * Mitigate possible JSONP Flash attacks.
232 | *
233 | * http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
234 | */
235 | $this->send_header( 'X-Content-Type-Options', 'nosniff' );
236 | $this->send_header( 'Access-Control-Expose-Headers', 'X-WP-Total, X-WP-TotalPages' );
237 | $this->send_header( 'Access-Control-Allow-Headers', 'Authorization' );
238 |
239 | /**
240 | * Send nocache headers on authenticated requests.
241 | *
242 | * @since 4.4.0
243 | *
244 | * @param bool $rest_send_nocache_headers Whether to send no-cache headers.
245 | */
246 | $send_no_cache_headers = apply_filters( 'rest_send_nocache_headers', is_user_logged_in() );
247 | if ( $send_no_cache_headers ) {
248 | foreach ( wp_get_nocache_headers() as $header => $header_value ) {
249 | $this->send_header( $header, $header_value );
250 | }
251 | }
252 |
253 | /**
254 | * Filter whether the REST API is enabled.
255 | *
256 | * @since 4.4.0
257 | *
258 | * @param bool $rest_enabled Whether the REST API is enabled. Default true.
259 | */
260 | $enabled = apply_filters( 'rest_enabled', true );
261 |
262 | /**
263 | * Filter whether jsonp is enabled.
264 | *
265 | * @since 4.4.0
266 | *
267 | * @param bool $jsonp_enabled Whether jsonp is enabled. Default true.
268 | */
269 | $jsonp_enabled = apply_filters( 'rest_jsonp_enabled', true );
270 |
271 | $jsonp_callback = null;
272 |
273 | if ( ! $enabled ) {
274 | echo $this->json_error( 'rest_disabled', __( 'The REST API is disabled on this site.' ), 404 );
275 | return false;
276 | }
277 | if ( isset( $_GET['_jsonp'] ) ) {
278 | if ( ! $jsonp_enabled ) {
279 | echo $this->json_error( 'rest_callback_disabled', __( 'JSONP support is disabled on this site.' ), 400 );
280 | return false;
281 | }
282 |
283 | // Check for invalid characters (only alphanumeric allowed).
284 | if ( is_string( $_GET['_jsonp'] ) ) {
285 | $jsonp_callback = preg_replace( '/[^\w\.]/', '', wp_unslash( $_GET['_jsonp'] ), -1, $illegal_char_count );
286 | if ( 0 !== $illegal_char_count ) {
287 | $jsonp_callback = null;
288 | }
289 | }
290 | if ( null === $jsonp_callback ) {
291 | echo $this->json_error( 'rest_callback_invalid', __( 'The JSONP callback function is invalid.' ), 400 );
292 | return false;
293 | }
294 | }
295 |
296 | if ( empty( $path ) ) {
297 | if ( isset( $_SERVER['PATH_INFO'] ) ) {
298 | $path = $_SERVER['PATH_INFO'];
299 | } else {
300 | $path = '/';
301 | }
302 | }
303 |
304 | $request = new WP_REST_Request( $_SERVER['REQUEST_METHOD'], $path );
305 |
306 | $request->set_query_params( $_GET );
307 | $request->set_body_params( $_POST );
308 | $request->set_file_params( $_FILES );
309 | $request->set_headers( $this->get_headers( $_SERVER ) );
310 | $request->set_body( $this->get_raw_data() );
311 |
312 | /*
313 | * HTTP method override for clients that can't use PUT/PATCH/DELETE. First, we check
314 | * $_GET['_method']. If that is not set, we check for the HTTP_X_HTTP_METHOD_OVERRIDE
315 | * header.
316 | */
317 | if ( isset( $_GET['_method'] ) ) {
318 | $request->set_method( $_GET['_method'] );
319 | } elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
320 | $request->set_method( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] );
321 | }
322 |
323 | $result = $this->check_authentication();
324 |
325 | if ( ! is_wp_error( $result ) ) {
326 | $result = $this->dispatch( $request );
327 | }
328 |
329 | // Normalize to either WP_Error or WP_REST_Response...
330 | $result = rest_ensure_response( $result );
331 |
332 | // ...then convert WP_Error across.
333 | if ( is_wp_error( $result ) ) {
334 | $result = $this->error_to_response( $result );
335 | }
336 |
337 | /**
338 | * Filter the API response.
339 | *
340 | * Allows modification of the response before returning.
341 | *
342 | * @since 4.4.0
343 | *
344 | * @param WP_HTTP_Response $result Result to send to the client. Usually a WP_REST_Response.
345 | * @param WP_REST_Server $this Server instance.
346 | * @param WP_REST_Request $request Request used to generate the response.
347 | */
348 | $result = apply_filters( 'rest_post_dispatch', rest_ensure_response( $result ), $this, $request );
349 |
350 | // Wrap the response in an envelope if asked for.
351 | if ( isset( $_GET['_envelope'] ) ) {
352 | $result = $this->envelope_response( $result, isset( $_GET['_embed'] ) );
353 | }
354 |
355 | // Send extra data from response objects.
356 | $headers = $result->get_headers();
357 | $this->send_headers( $headers );
358 |
359 | $code = $result->get_status();
360 | $this->set_status( $code );
361 |
362 | /**
363 | * Filter whether the request has already been served.
364 | *
365 | * Allow sending the request manually - by returning true, the API result
366 | * will not be sent to the client.
367 | *
368 | * @since 4.4.0
369 | *
370 | * @param bool $served Whether the request has already been served.
371 | * Default false.
372 | * @param WP_HTTP_Response $result Result to send to the client. Usually a WP_REST_Response.
373 | * @param WP_REST_Request $request Request used to generate the response.
374 | * @param WP_REST_Server $this Server instance.
375 | */
376 | $served = apply_filters( 'rest_pre_serve_request', false, $result, $request, $this );
377 |
378 | if ( ! $served ) {
379 | if ( 'HEAD' === $request->get_method() ) {
380 | return null;
381 | }
382 |
383 | // Embed links inside the request.
384 | $result = $this->response_to_data( $result, isset( $_GET['_embed'] ) );
385 |
386 | $result = wp_json_encode( $result );
387 |
388 | $json_error_message = $this->get_json_last_error();
389 | if ( $json_error_message ) {
390 | $json_error_obj = new WP_Error( 'rest_encode_error', $json_error_message, array( 'status' => 500 ) );
391 | $result = $this->error_to_response( $json_error_obj );
392 | $result = wp_json_encode( $result->data[0] );
393 | }
394 |
395 | if ( $jsonp_callback ) {
396 | // Prepend '/**/' to mitigate possible JSONP Flash attacks
397 | // http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
398 | echo '/**/' . $jsonp_callback . '(' . $result . ')';
399 | } else {
400 | echo $result;
401 | }
402 | }
403 | return null;
404 | }
405 |
406 | /**
407 | * Converts a response to data to send.
408 | *
409 | * @since 4.4.0
410 | * @access public
411 | *
412 | * @param WP_REST_Response $response Response object.
413 | * @param bool $embed Whether links should be embedded.
414 | * @return array {
415 | * Data with sub-requests embedded.
416 | *
417 | * @type array [$_links] Links.
418 | * @type array [$_embedded] Embeddeds.
419 | * }
420 | */
421 | public function response_to_data( $response, $embed ) {
422 | $data = $response->get_data();
423 | $links = $this->get_response_links( $response );
424 |
425 | if ( ! empty( $links ) ) {
426 | // Convert links to part of the data.
427 | $data['_links'] = $links;
428 | }
429 | if ( $embed ) {
430 | // Determine if this is a numeric array.
431 | if ( wp_is_numeric_array( $data ) ) {
432 | $data = array_map( array( $this, 'embed_links' ), $data );
433 | } else {
434 | $data = $this->embed_links( $data );
435 | }
436 | }
437 |
438 | return $data;
439 | }
440 |
441 | /**
442 | * Retrieves links from a response.
443 | *
444 | * Extracts the links from a response into a structured hash, suitable for
445 | * direct output.
446 | *
447 | * @since 4.4.0
448 | * @access public
449 | * @static
450 | *
451 | * @param WP_REST_Response $response Response to extract links from.
452 | * @return array Map of link relation to list of link hashes.
453 | */
454 | public static function get_response_links( $response ) {
455 | $links = $response->get_links();
456 |
457 | if ( empty( $links ) ) {
458 | return array();
459 | }
460 |
461 | // Convert links to part of the data.
462 | $data = array();
463 | foreach ( $links as $rel => $items ) {
464 | $data[ $rel ] = array();
465 |
466 | foreach ( $items as $item ) {
467 | $attributes = $item['attributes'];
468 | $attributes['href'] = $item['href'];
469 | $data[ $rel ][] = $attributes;
470 | }
471 | }
472 |
473 | return $data;
474 | }
475 |
476 | /**
477 | * Embeds the links from the data into the request.
478 | *
479 | * @since 4.4.0
480 | * @access protected
481 | *
482 | * @param array $data Data from the request.
483 | * @return array {
484 | * Data with sub-requests embedded.
485 | *
486 | * @type array [$_links] Links.
487 | * @type array [$_embedded] Embeddeds.
488 | * }
489 | */
490 | protected function embed_links( $data ) {
491 | if ( empty( $data['_links'] ) ) {
492 | return $data;
493 | }
494 |
495 | $embedded = array();
496 | $api_root = rest_url();
497 |
498 | foreach ( $data['_links'] as $rel => $links ) {
499 | // Ignore links to self, for obvious reasons.
500 | if ( 'self' === $rel ) {
501 | continue;
502 | }
503 |
504 | $embeds = array();
505 |
506 | foreach ( $links as $item ) {
507 | // Determine if the link is embeddable.
508 | if ( empty( $item['embeddable'] ) || strpos( $item['href'], $api_root ) !== 0 ) {
509 | // Ensure we keep the same order.
510 | $embeds[] = array();
511 | continue;
512 | }
513 |
514 | // Run through our internal routing and serve.
515 | $route = substr( $item['href'], strlen( untrailingslashit( $api_root ) ) );
516 | $query_params = array();
517 |
518 | // Parse out URL query parameters.
519 | $parsed = parse_url( $route );
520 | if ( empty( $parsed['path'] ) ) {
521 | $embeds[] = array();
522 | continue;
523 | }
524 |
525 | if ( ! empty( $parsed['query'] ) ) {
526 | parse_str( $parsed['query'], $query_params );
527 |
528 | // Ensure magic quotes are stripped.
529 | if ( get_magic_quotes_gpc() ) {
530 | $query_params = stripslashes_deep( $query_params );
531 | }
532 | }
533 |
534 | // Embedded resources get passed context=embed.
535 | if ( empty( $query_params['context'] ) ) {
536 | $query_params['context'] = 'embed';
537 | }
538 |
539 | $request = new WP_REST_Request( 'GET', $parsed['path'] );
540 |
541 | $request->set_query_params( $query_params );
542 | $response = $this->dispatch( $request );
543 |
544 | $embeds[] = $this->response_to_data( $response, false );
545 | }
546 |
547 | // Determine if any real links were found.
548 | $has_links = count( array_filter( $embeds ) );
549 | if ( $has_links ) {
550 | $embedded[ $rel ] = $embeds;
551 | }
552 | }
553 |
554 | if ( ! empty( $embedded ) ) {
555 | $data['_embedded'] = $embedded;
556 | }
557 |
558 | return $data;
559 | }
560 |
561 | /**
562 | * Wraps the response in an envelope.
563 | *
564 | * The enveloping technique is used to work around browser/client
565 | * compatibility issues. Essentially, it converts the full HTTP response to
566 | * data instead.
567 | *
568 | * @since 4.4.0
569 | * @access public
570 | *
571 | * @param WP_REST_Response $response Response object.
572 | * @param bool $embed Whether links should be embedded.
573 | * @return WP_REST_Response New response with wrapped data
574 | */
575 | public function envelope_response( $response, $embed ) {
576 | $envelope = array(
577 | 'body' => $this->response_to_data( $response, $embed ),
578 | 'status' => $response->get_status(),
579 | 'headers' => $response->get_headers(),
580 | );
581 |
582 | /**
583 | * Filter the enveloped form of a response.
584 | *
585 | * @since 4.4.0
586 | *
587 | * @param array $envelope Envelope data.
588 | * @param WP_REST_Response $response Original response data.
589 | */
590 | $envelope = apply_filters( 'rest_envelope_response', $envelope, $response );
591 |
592 | // Ensure it's still a response and return.
593 | return rest_ensure_response( $envelope );
594 | }
595 |
596 | /**
597 | * Registers a route to the server.
598 | *
599 | * @since 4.4.0
600 | * @access public
601 | *
602 | * @param string $namespace Namespace.
603 | * @param string $route The REST route.
604 | * @param array $route_args Route arguments.
605 | * @param bool $override Optional. Whether the route should be overriden if it already exists.
606 | * Default false.
607 | */
608 | public function register_route( $namespace, $route, $route_args, $override = false ) {
609 | if ( ! isset( $this->namespaces[ $namespace ] ) ) {
610 | $this->namespaces[ $namespace ] = array();
611 |
612 | $this->register_route( $namespace, '/' . $namespace, array(
613 | array(
614 | 'methods' => self::READABLE,
615 | 'callback' => array( $this, 'get_namespace_index' ),
616 | 'args' => array(
617 | 'namespace' => array(
618 | 'default' => $namespace,
619 | ),
620 | 'context' => array(
621 | 'default' => 'view',
622 | ),
623 | ),
624 | ),
625 | ) );
626 | }
627 |
628 | // Associative to avoid double-registration.
629 | $this->namespaces[ $namespace ][ $route ] = true;
630 | $route_args['namespace'] = $namespace;
631 |
632 | if ( $override || empty( $this->endpoints[ $route ] ) ) {
633 | $this->endpoints[ $route ] = $route_args;
634 | } else {
635 | $this->endpoints[ $route ] = array_merge( $this->endpoints[ $route ], $route_args );
636 | }
637 | }
638 |
639 | /**
640 | * Retrieves the route map.
641 | *
642 | * The route map is an associative array with path regexes as the keys. The
643 | * value is an indexed array with the callback function/method as the first
644 | * item, and a bitmask of HTTP methods as the second item (see the class
645 | * constants).
646 | *
647 | * Each route can be mapped to more than one callback by using an array of
648 | * the indexed arrays. This allows mapping e.g. GET requests to one callback
649 | * and POST requests to another.
650 | *
651 | * Note that the path regexes (array keys) must have @ escaped, as this is
652 | * used as the delimiter with preg_match()
653 | *
654 | * @since 4.4.0
655 | * @access public
656 | *
657 | * @return array `'/path/regex' => array( $callback, $bitmask )` or
658 | * `'/path/regex' => array( array( $callback, $bitmask ), ...)`.
659 | */
660 | public function get_routes() {
661 |
662 | /**
663 | * Filter the array of available endpoints.
664 | *
665 | * @since 4.4.0
666 | *
667 | * @param array $endpoints The available endpoints. An array of matching regex patterns, each mapped
668 | * to an array of callbacks for the endpoint. These take the format
669 | * `'/path/regex' => array( $callback, $bitmask )` or
670 | * `'/path/regex' => array( array( $callback, $bitmask ).
671 | */
672 | $endpoints = apply_filters( 'rest_endpoints', $this->endpoints );
673 |
674 | // Normalise the endpoints.
675 | $defaults = array(
676 | 'methods' => '',
677 | 'accept_json' => false,
678 | 'accept_raw' => false,
679 | 'show_in_index' => true,
680 | 'args' => array(),
681 | );
682 |
683 | foreach ( $endpoints as $route => &$handlers ) {
684 |
685 | if ( isset( $handlers['callback'] ) ) {
686 | // Single endpoint, add one deeper.
687 | $handlers = array( $handlers );
688 | }
689 |
690 | if ( ! isset( $this->route_options[ $route ] ) ) {
691 | $this->route_options[ $route ] = array();
692 | }
693 |
694 | foreach ( $handlers as $key => &$handler ) {
695 |
696 | if ( ! is_numeric( $key ) ) {
697 | // Route option, move it to the options.
698 | $this->route_options[ $route ][ $key ] = $handler;
699 | unset( $handlers[ $key ] );
700 | continue;
701 | }
702 |
703 | $handler = wp_parse_args( $handler, $defaults );
704 |
705 | // Allow comma-separated HTTP methods.
706 | if ( is_string( $handler['methods'] ) ) {
707 | $methods = explode( ',', $handler['methods'] );
708 | } else if ( is_array( $handler['methods'] ) ) {
709 | $methods = $handler['methods'];
710 | } else {
711 | $methods = array();
712 | }
713 |
714 | $handler['methods'] = array();
715 |
716 | foreach ( $methods as $method ) {
717 | $method = strtoupper( trim( $method ) );
718 | $handler['methods'][ $method ] = true;
719 | }
720 | }
721 | }
722 | return $endpoints;
723 | }
724 |
725 | /**
726 | * Retrieves namespaces registered on the server.
727 | *
728 | * @since 4.4.0
729 | * @access public
730 | *
731 | * @return array List of registered namespaces.
732 | */
733 | public function get_namespaces() {
734 | return array_keys( $this->namespaces );
735 | }
736 |
737 | /**
738 | * Retrieves specified options for a route.
739 | *
740 | * @since 4.4.0
741 | * @access public
742 | *
743 | * @param string $route Route pattern to fetch options for.
744 | * @return array|null Data as an associative array if found, or null if not found.
745 | */
746 | public function get_route_options( $route ) {
747 | if ( ! isset( $this->route_options[ $route ] ) ) {
748 | return null;
749 | }
750 |
751 | return $this->route_options[ $route ];
752 | }
753 |
754 | /**
755 | * Matches the request to a callback and call it.
756 | *
757 | * @since 4.4.0
758 | * @access public
759 | *
760 | * @param WP_REST_Request $request Request to attempt dispatching.
761 | * @return WP_REST_Response Response returned by the callback.
762 | */
763 | public function dispatch( $request ) {
764 | /**
765 | * Filter the pre-calculated result of a REST dispatch request.
766 | *
767 | * Allow hijacking the request before dispatching by returning a non-empty. The returned value
768 | * will be used to serve the request instead.
769 | *
770 | * @since 4.4.0
771 | *
772 | * @param mixed $result Response to replace the requested version with. Can be anything
773 | * a normal endpoint can return, or null to not hijack the request.
774 | * @param WP_REST_Server $this Server instance.
775 | * @param WP_REST_Request $request Request used to generate the response.
776 | */
777 | $result = apply_filters( 'rest_pre_dispatch', null, $this, $request );
778 |
779 | if ( ! empty( $result ) ) {
780 | return $result;
781 | }
782 |
783 | $method = $request->get_method();
784 | $path = $request->get_route();
785 |
786 | foreach ( $this->get_routes() as $route => $handlers ) {
787 | $match = preg_match( '@^' . $route . '$@i', $path, $args );
788 |
789 | if ( ! $match ) {
790 | continue;
791 | }
792 |
793 | foreach ( $handlers as $handler ) {
794 | $callback = $handler['callback'];
795 | $response = null;
796 |
797 | $checked_method = 'HEAD' === $method ? 'GET' : $method;
798 | if ( empty( $handler['methods'][ $checked_method ] ) ) {
799 | continue;
800 | }
801 |
802 | if ( ! is_callable( $callback ) ) {
803 | $response = new WP_Error( 'rest_invalid_handler', __( 'The handler for the route is invalid' ), array( 'status' => 500 ) );
804 | }
805 |
806 | if ( ! is_wp_error( $response ) ) {
807 | // Remove the redundant preg_match argument.
808 | unset( $args[0] );
809 |
810 | $request->set_url_params( $args );
811 | $request->set_attributes( $handler );
812 |
813 | $request->sanitize_params();
814 |
815 | $defaults = array();
816 |
817 | foreach ( $handler['args'] as $arg => $options ) {
818 | if ( isset( $options['default'] ) ) {
819 | $defaults[ $arg ] = $options['default'];
820 | }
821 | }
822 |
823 | $request->set_default_params( $defaults );
824 |
825 | $check_required = $request->has_valid_params();
826 | if ( is_wp_error( $check_required ) ) {
827 | $response = $check_required;
828 | }
829 | }
830 |
831 | if ( ! is_wp_error( $response ) ) {
832 | // Check permission specified on the route.
833 | if ( ! empty( $handler['permission_callback'] ) ) {
834 | $permission = call_user_func( $handler['permission_callback'], $request );
835 |
836 | if ( is_wp_error( $permission ) ) {
837 | $response = $permission;
838 | } else if ( false === $permission || null === $permission ) {
839 | $response = new WP_Error( 'rest_forbidden', __( "You don't have permission to do this." ), array( 'status' => 403 ) );
840 | }
841 | }
842 | }
843 |
844 | if ( ! is_wp_error( $response ) ) {
845 | /**
846 | * Filter the REST dispatch request result.
847 | *
848 | * Allow plugins to override dispatching the request.
849 | *
850 | * @since 4.4.0
851 | *
852 | * @param bool $dispatch_result Dispatch result, will be used if not empty.
853 | * @param WP_REST_Request $request Request used to generate the response.
854 | */
855 | $dispatch_result = apply_filters( 'rest_dispatch_request', null, $request );
856 |
857 | // Allow plugins to halt the request via this filter.
858 | if ( null !== $dispatch_result ) {
859 | $response = $dispatch_result;
860 | } else {
861 | $response = call_user_func( $callback, $request );
862 | }
863 | }
864 |
865 | if ( is_wp_error( $response ) ) {
866 | $response = $this->error_to_response( $response );
867 | } else {
868 | $response = rest_ensure_response( $response );
869 | }
870 |
871 | $response->set_matched_route( $route );
872 | $response->set_matched_handler( $handler );
873 |
874 | return $response;
875 | }
876 | }
877 |
878 | return $this->error_to_response( new WP_Error( 'rest_no_route', __( 'No route was found matching the URL and request method' ), array( 'status' => 404 ) ) );
879 | }
880 |
881 | /**
882 | * Returns if an error occurred during most recent JSON encode/decode.
883 | *
884 | * Strings to be translated will be in format like
885 | * "Encoding error: Maximum stack depth exceeded".
886 | *
887 | * @since 4.4.0
888 | * @access protected
889 | *
890 | * @return bool|string Boolean false or string error message.
891 | */
892 | protected function get_json_last_error() {
893 | // See https://core.trac.wordpress.org/ticket/27799.
894 | if ( ! function_exists( 'json_last_error' ) ) {
895 | return false;
896 | }
897 |
898 | $last_error_code = json_last_error();
899 |
900 | if ( ( defined( 'JSON_ERROR_NONE' ) && JSON_ERROR_NONE === $last_error_code ) || empty( $last_error_code ) ) {
901 | return false;
902 | }
903 |
904 | return json_last_error_msg();
905 | }
906 |
907 | /**
908 | * Retrieves the site index.
909 | *
910 | * This endpoint describes the capabilities of the site.
911 | *
912 | * @since 4.4.0
913 | * @access public
914 | *
915 | * @param array $request {
916 | * Request.
917 | *
918 | * @type string $context Context.
919 | * }
920 | * @return array Index entity
921 | */
922 | public function get_index( $request ) {
923 | // General site data.
924 | $available = array(
925 | 'name' => get_option( 'blogname' ),
926 | 'description' => get_option( 'blogdescription' ),
927 | 'url' => get_option( 'siteurl' ),
928 | 'namespaces' => array_keys( $this->namespaces ),
929 | 'authentication' => array(),
930 | 'routes' => $this->get_data_for_routes( $this->get_routes(), $request['context'] ),
931 | );
932 |
933 | $response = new WP_REST_Response( $available );
934 |
935 | $response->add_link( 'help', 'http://v2.wp-api.org/' );
936 |
937 | /**
938 | * Filter the API root index data.
939 | *
940 | * This contains the data describing the API. This includes information
941 | * about supported authentication schemes, supported namespaces, routes
942 | * available on the API, and a small amount of data about the site.
943 | *
944 | * @since 4.4.0
945 | *
946 | * @param WP_REST_Response $response Response data.
947 | */
948 | return apply_filters( 'rest_index', $response );
949 | }
950 |
951 | /**
952 | * Retrieves the index for a namespace.
953 | *
954 | * @since 4.4.0
955 | * @access public
956 | *
957 | * @param WP_REST_Request $request REST request instance.
958 | * @return WP_REST_Response|WP_Error WP_REST_Response instance if the index was found,
959 | * WP_Error if the namespace isn't set.
960 | */
961 | public function get_namespace_index( $request ) {
962 | $namespace = $request['namespace'];
963 |
964 | if ( ! isset( $this->namespaces[ $namespace ] ) ) {
965 | return new WP_Error( 'rest_invalid_namespace', __( 'The specified namespace could not be found.' ), array( 'status' => 404 ) );
966 | }
967 |
968 | $routes = $this->namespaces[ $namespace ];
969 | $endpoints = array_intersect_key( $this->get_routes(), $routes );
970 |
971 | $data = array(
972 | 'namespace' => $namespace,
973 | 'routes' => $this->get_data_for_routes( $endpoints, $request['context'] ),
974 | );
975 | $response = rest_ensure_response( $data );
976 |
977 | // Link to the root index.
978 | $response->add_link( 'up', rest_url( '/' ) );
979 |
980 | /**
981 | * Filter the namespace index data.
982 | *
983 | * This typically is just the route data for the namespace, but you can
984 | * add any data you'd like here.
985 | *
986 | * @since 4.4.0
987 | *
988 | * @param WP_REST_Response $response Response data.
989 | * @param WP_REST_Request $request Request data. The namespace is passed as the 'namespace' parameter.
990 | */
991 | return apply_filters( 'rest_namespace_index', $response, $request );
992 | }
993 |
994 | /**
995 | * Retrieves the publicly-visible data for routes.
996 | *
997 | * @since 4.4.0
998 | * @access public
999 | *
1000 | * @param array $routes Routes to get data for.
1001 | * @param string $context Optional. Context for data. Accepts 'view' or 'help'. Default 'view'.
1002 | * @return array Route data to expose in indexes.
1003 | */
1004 | public function get_data_for_routes( $routes, $context = 'view' ) {
1005 | $available = array();
1006 |
1007 | // Find the available routes.
1008 | foreach ( $routes as $route => $callbacks ) {
1009 | $data = $this->get_data_for_route( $route, $callbacks, $context );
1010 | if ( empty( $data ) ) {
1011 | continue;
1012 | }
1013 |
1014 | /**
1015 | * Filter the REST endpoint data.
1016 | *
1017 | * @since 4.4.0
1018 | *
1019 | * @param WP_REST_Request $request Request data. The namespace is passed as the 'namespace' parameter.
1020 | */
1021 | $available[ $route ] = apply_filters( 'rest_endpoints_description', $data );
1022 | }
1023 |
1024 | /**
1025 | * Filter the publicly-visible data for routes.
1026 | *
1027 | * This data is exposed on indexes and can be used by clients or
1028 | * developers to investigate the site and find out how to use it. It
1029 | * acts as a form of self-documentation.
1030 | *
1031 | * @since 4.4.0
1032 | *
1033 | * @param array $available Map of route to route data.
1034 | * @param array $routes Internal route data as an associative array.
1035 | */
1036 | return apply_filters( 'rest_route_data', $available, $routes );
1037 | }
1038 |
1039 | /**
1040 | * Retrieves publicly-visible data for the route.
1041 | *
1042 | * @since 4.4.0
1043 | * @access public
1044 | *
1045 | * @param string $route Route to get data for.
1046 | * @param array $callbacks Callbacks to convert to data.
1047 | * @param string $context Optional. Context for the data. Accepts 'view' or 'help'. Default 'view'.
1048 | * @return array|null Data for the route, or null if no publicly-visible data.
1049 | */
1050 | public function get_data_for_route( $route, $callbacks, $context = 'view' ) {
1051 | $data = array(
1052 | 'namespace' => '',
1053 | 'methods' => array(),
1054 | 'endpoints' => array(),
1055 | );
1056 |
1057 | if ( isset( $this->route_options[ $route ] ) ) {
1058 | $options = $this->route_options[ $route ];
1059 |
1060 | if ( isset( $options['namespace'] ) ) {
1061 | $data['namespace'] = $options['namespace'];
1062 | }
1063 |
1064 | if ( isset( $options['schema'] ) && 'help' === $context ) {
1065 | $data['schema'] = call_user_func( $options['schema'] );
1066 | }
1067 | }
1068 |
1069 | $route = preg_replace( '#\(\?P<(\w+?)>.*?\)#', '{$1}', $route );
1070 |
1071 | foreach ( $callbacks as $callback ) {
1072 | // Skip to the next route if any callback is hidden.
1073 | if ( empty( $callback['show_in_index'] ) ) {
1074 | continue;
1075 | }
1076 |
1077 | $data['methods'] = array_merge( $data['methods'], array_keys( $callback['methods'] ) );
1078 | $endpoint_data = array(
1079 | 'methods' => array_keys( $callback['methods'] ),
1080 | );
1081 |
1082 | if ( isset( $callback['args'] ) ) {
1083 | $endpoint_data['args'] = array();
1084 | foreach ( $callback['args'] as $key => $opts ) {
1085 | $arg_data = array(
1086 | 'required' => ! empty( $opts['required'] ),
1087 | );
1088 | if ( isset( $opts['default'] ) ) {
1089 | $arg_data['default'] = $opts['default'];
1090 | }
1091 | if ( isset( $opts['enum'] ) ) {
1092 | $arg_data['enum'] = $opts['enum'];
1093 | }
1094 | if ( isset( $opts['description'] ) ) {
1095 | $arg_data['description'] = $opts['description'];
1096 | }
1097 | $endpoint_data['args'][ $key ] = $arg_data;
1098 | }
1099 | }
1100 |
1101 | $data['endpoints'][] = $endpoint_data;
1102 |
1103 | // For non-variable routes, generate links.
1104 | if ( strpos( $route, '{' ) === false ) {
1105 | $data['_links'] = array(
1106 | 'self' => rest_url( $route ),
1107 | );
1108 | }
1109 | }
1110 |
1111 | if ( empty( $data['methods'] ) ) {
1112 | // No methods supported, hide the route.
1113 | return null;
1114 | }
1115 |
1116 | return $data;
1117 | }
1118 |
1119 | /**
1120 | * Sends an HTTP status code.
1121 | *
1122 | * @since 4.4.0
1123 | * @access protected
1124 | *
1125 | * @param int $code HTTP status.
1126 | */
1127 | protected function set_status( $code ) {
1128 | status_header( $code );
1129 | }
1130 |
1131 | /**
1132 | * Sends an HTTP header.
1133 | *
1134 | * @since 4.4.0
1135 | * @access public
1136 | *
1137 | * @param string $key Header key.
1138 | * @param string $value Header value.
1139 | */
1140 | public function send_header( $key, $value ) {
1141 | /*
1142 | * Sanitize as per RFC2616 (Section 4.2):
1143 | *
1144 | * Any LWS that occurs between field-content MAY be replaced with a
1145 | * single SP before interpreting the field value or forwarding the
1146 | * message downstream.
1147 | */
1148 | $value = preg_replace( '/\s+/', ' ', $value );
1149 | header( sprintf( '%s: %s', $key, $value ) );
1150 | }
1151 |
1152 | /**
1153 | * Sends multiple HTTP headers.
1154 | *
1155 | * @since 4.4.0
1156 | * @access public
1157 | *
1158 | * @param array $headers Map of header name to header value.
1159 | */
1160 | public function send_headers( $headers ) {
1161 | foreach ( $headers as $key => $value ) {
1162 | $this->send_header( $key, $value );
1163 | }
1164 | }
1165 |
1166 | /**
1167 | * Retrieves the raw request entity (body).
1168 | *
1169 | * @since 4.4.0
1170 | * @access public
1171 | *
1172 | * @global string $HTTP_RAW_POST_DATA Raw post data.
1173 | *
1174 | * @return string Raw request data.
1175 | */
1176 | public static function get_raw_data() {
1177 | global $HTTP_RAW_POST_DATA;
1178 |
1179 | /*
1180 | * A bug in PHP < 5.2.2 makes $HTTP_RAW_POST_DATA not set by default,
1181 | * but we can do it ourself.
1182 | */
1183 | if ( ! isset( $HTTP_RAW_POST_DATA ) ) {
1184 | $HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );
1185 | }
1186 |
1187 | return $HTTP_RAW_POST_DATA;
1188 | }
1189 |
1190 | /**
1191 | * Extracts headers from a PHP-style $_SERVER array.
1192 | *
1193 | * @since 4.4.0
1194 | * @access public
1195 | *
1196 | * @param array $server Associative array similar to `$_SERVER`.
1197 | * @return array Headers extracted from the input.
1198 | */
1199 | public function get_headers( $server ) {
1200 | $headers = array();
1201 |
1202 | // CONTENT_* headers are not prefixed with HTTP_.
1203 | $additional = array( 'CONTENT_LENGTH' => true, 'CONTENT_MD5' => true, 'CONTENT_TYPE' => true );
1204 |
1205 | foreach ( $server as $key => $value ) {
1206 | if ( strpos( $key, 'HTTP_' ) === 0 ) {
1207 | $headers[ substr( $key, 5 ) ] = $value;
1208 | } elseif ( isset( $additional[ $key ] ) ) {
1209 | $headers[ $key ] = $value;
1210 | }
1211 | }
1212 |
1213 | return $headers;
1214 | }
1215 | }
1216 |
--------------------------------------------------------------------------------