├── .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 | --------------------------------------------------------------------------------