*/
public function check_authentication() {
/**
- * Pass an authentication error to the API
+ * Filters REST authentication errors.
*
* This is used to pass a WP_Error from an authentication method back to
* the API.
public function serve_request( $path = null ) {
$content_type = isset( $_GET['_jsonp'] ) ? 'application/javascript' : 'application/json';
$this->send_header( 'Content-Type', $content_type . '; charset=' . get_option( 'blog_charset' ) );
+ $this->send_header( 'X-Robots-Tag', 'noindex' );
+
+ $api_root = get_rest_url();
+ if ( ! empty( $api_root ) ) {
+ $this->send_header( 'Link', '<' . esc_url_raw( $api_root ) . '>; rel="https://api.w.org/"' );
+ }
/*
* Mitigate possible JSONP Flash attacks.
*
- * http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
+ * https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
*/
$this->send_header( 'X-Content-Type-Options', 'nosniff' );
$this->send_header( 'Access-Control-Expose-Headers', 'X-WP-Total, X-WP-TotalPages' );
- $this->send_header( 'Access-Control-Allow-Headers', 'Authorization' );
+ $this->send_header( 'Access-Control-Allow-Headers', 'Authorization, Content-Type' );
/**
* Send nocache headers on authenticated requests.
}
/**
- * Filter whether the REST API is enabled.
+ * Filters whether the REST API is enabled.
*
* @since 4.4.0
+ * @deprecated 4.7.0 Use the rest_authentication_errors filter to restrict access to the API
*
* @param bool $rest_enabled Whether the REST API is enabled. Default true.
*/
- $enabled = apply_filters( 'rest_enabled', true );
+ apply_filters_deprecated( 'rest_enabled', array( true ), '4.7.0', 'rest_authentication_errors', __( 'The REST API can no longer be completely disabled, the rest_authentication_errors can be used to restrict access to the API, instead.' ) );
/**
- * Filter whether jsonp is enabled.
+ * Filters whether jsonp is enabled.
*
* @since 4.4.0
*
$jsonp_callback = null;
- if ( ! $enabled ) {
- echo $this->json_error( 'rest_disabled', __( 'The REST API is disabled on this site.' ), 404 );
- return false;
- }
if ( isset( $_GET['_jsonp'] ) ) {
if ( ! $jsonp_enabled ) {
echo $this->json_error( 'rest_callback_disabled', __( 'JSONP support is disabled on this site.' ), 400 );
return false;
}
- // Check for invalid characters (only alphanumeric allowed).
- if ( is_string( $_GET['_jsonp'] ) ) {
- $jsonp_callback = preg_replace( '/[^\w\.]/', '', wp_unslash( $_GET['_jsonp'] ), -1, $illegal_char_count );
- if ( 0 !== $illegal_char_count ) {
- $jsonp_callback = null;
- }
- }
- if ( null === $jsonp_callback ) {
- echo $this->json_error( 'rest_callback_invalid', __( 'The JSONP callback function is invalid.' ), 400 );
+ $jsonp_callback = $_GET['_jsonp'];
+ if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) {
+ echo $this->json_error( 'rest_callback_invalid', __( 'Invalid JSONP callback function.' ), 400 );
return false;
}
}
$request = new WP_REST_Request( $_SERVER['REQUEST_METHOD'], $path );
- $request->set_query_params( $_GET );
- $request->set_body_params( $_POST );
+ $request->set_query_params( wp_unslash( $_GET ) );
+ $request->set_body_params( wp_unslash( $_POST ) );
$request->set_file_params( $_FILES );
- $request->set_headers( $this->get_headers( $_SERVER ) );
+ $request->set_headers( $this->get_headers( wp_unslash( $_SERVER ) ) );
$request->set_body( $this->get_raw_data() );
/*
}
/**
- * Filter the API response.
+ * Filters the API response.
*
* Allows modification of the response before returning.
*
* @since 4.4.0
+ * @since 4.5.0 Applied to embedded responses.
*
* @param WP_HTTP_Response $result Result to send to the client. Usually a WP_REST_Response.
* @param WP_REST_Server $this Server instance.
$this->set_status( $code );
/**
- * Filter whether the request has already been served.
+ * Filters whether the request has already been served.
*
* Allow sending the request manually - by returning true, the API result
* will not be sent to the client.
}
if ( $jsonp_callback ) {
- // Prepend '/**/' to mitigate possible JSONP Flash attacks
- // http://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
+ // Prepend '/**/' to mitigate possible JSONP Flash attacks.
+ // https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
echo '/**/' . $jsonp_callback . '(' . $result . ')';
} else {
echo $result;
*/
public function response_to_data( $response, $embed ) {
$data = $response->get_data();
- $links = $this->get_response_links( $response );
+ $links = $this->get_compact_response_links( $response );
if ( ! empty( $links ) ) {
// Convert links to part of the data.
*/
public static function get_response_links( $response ) {
$links = $response->get_links();
-
if ( empty( $links ) ) {
return array();
}
return $data;
}
+ /**
+ * Retrieves the CURIEs (compact URIs) used for relations.
+ *
+ * Extracts the links from a response into a structured hash, suitable for
+ * direct output.
+ *
+ * @since 4.5.0
+ * @access public
+ * @static
+ *
+ * @param WP_REST_Response $response Response to extract links from.
+ * @return array Map of link relation to list of link hashes.
+ */
+ public static function get_compact_response_links( $response ) {
+ $links = self::get_response_links( $response );
+
+ if ( empty( $links ) ) {
+ return array();
+ }
+
+ $curies = $response->get_curies();
+ $used_curies = array();
+
+ foreach ( $links as $rel => $items ) {
+
+ // Convert $rel URIs to their compact versions if they exist.
+ foreach ( $curies as $curie ) {
+ $href_prefix = substr( $curie['href'], 0, strpos( $curie['href'], '{rel}' ) );
+ if ( strpos( $rel, $href_prefix ) !== 0 ) {
+ continue;
+ }
+
+ // Relation now changes from '$uri' to '$curie:$relation'.
+ $rel_regex = str_replace( '\{rel\}', '(.+)', preg_quote( $curie['href'], '!' ) );
+ preg_match( '!' . $rel_regex . '!', $rel, $matches );
+ if ( $matches ) {
+ $new_rel = $curie['name'] . ':' . $matches[1];
+ $used_curies[ $curie['name'] ] = $curie;
+ $links[ $new_rel ] = $items;
+ unset( $links[ $rel ] );
+ break;
+ }
+ }
+ }
+
+ // Push the curies onto the start of the links array.
+ if ( $used_curies ) {
+ $links['curies'] = array_values( $used_curies );
+ }
+
+ return $links;
+ }
+
/**
* Embeds the links from the data into the request.
*
}
$embedded = array();
- $api_root = rest_url();
foreach ( $data['_links'] as $rel => $links ) {
// Ignore links to self, for obvious reasons.
foreach ( $links as $item ) {
// Determine if the link is embeddable.
- if ( empty( $item['embeddable'] ) || strpos( $item['href'], $api_root ) !== 0 ) {
+ if ( empty( $item['embeddable'] ) ) {
// Ensure we keep the same order.
$embeds[] = array();
continue;
}
// Run through our internal routing and serve.
- $route = substr( $item['href'], strlen( untrailingslashit( $api_root ) ) );
- $query_params = array();
-
- // Parse out URL query parameters.
- $parsed = parse_url( $route );
- if ( empty( $parsed['path'] ) ) {
+ $request = WP_REST_Request::from_url( $item['href'] );
+ if ( ! $request ) {
$embeds[] = array();
continue;
}
- if ( ! empty( $parsed['query'] ) ) {
- parse_str( $parsed['query'], $query_params );
-
- // Ensure magic quotes are stripped.
- if ( get_magic_quotes_gpc() ) {
- $query_params = stripslashes_deep( $query_params );
- }
- }
-
// Embedded resources get passed context=embed.
- if ( empty( $query_params['context'] ) ) {
- $query_params['context'] = 'embed';
+ if ( empty( $request['context'] ) ) {
+ $request['context'] = 'embed';
}
- $request = new WP_REST_Request( 'GET', $parsed['path'] );
-
- $request->set_query_params( $query_params );
$response = $this->dispatch( $request );
+ /** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
+ $response = apply_filters( 'rest_post_dispatch', rest_ensure_response( $response ), $this, $request );
+
$embeds[] = $this->response_to_data( $response, false );
}
// Determine if any real links were found.
$has_links = count( array_filter( $embeds ) );
+
if ( $has_links ) {
$embedded[ $rel ] = $embeds;
}
);
/**
- * Filter the enveloped form of a response.
+ * Filters the enveloped form of a response.
*
* @since 4.4.0
*
* @param string $namespace Namespace.
* @param string $route The REST route.
* @param array $route_args Route arguments.
- * @param bool $override Optional. Whether the route should be overriden if it already exists.
+ * @param bool $override Optional. Whether the route should be overridden if it already exists.
* Default false.
*/
public function register_route( $namespace, $route, $route_args, $override = false ) {
public function get_routes() {
/**
- * Filter the array of available endpoints.
+ * Filters the array of available endpoints.
*
* @since 4.4.0
*
// Allow comma-separated HTTP methods.
if ( is_string( $handler['methods'] ) ) {
$methods = explode( ',', $handler['methods'] );
- } else if ( is_array( $handler['methods'] ) ) {
+ } elseif ( is_array( $handler['methods'] ) ) {
$methods = $handler['methods'];
} else {
$methods = array();
}
}
}
+
return $endpoints;
}
*/
public function dispatch( $request ) {
/**
- * Filter the pre-calculated result of a REST dispatch request.
+ * Filters the pre-calculated result of a REST dispatch request.
*
* Allow hijacking the request before dispatching by returning a non-empty. The returned value
* will be used to serve the request instead.
$callback = $handler['callback'];
$response = null;
- $checked_method = 'HEAD' === $method ? 'GET' : $method;
+ // Fallback to GET method if no HEAD method is registered.
+ $checked_method = $method;
+ if ( 'HEAD' === $method && empty( $handler['methods']['HEAD'] ) ) {
+ $checked_method = 'GET';
+ }
if ( empty( $handler['methods'][ $checked_method ] ) ) {
continue;
}
$request->set_url_params( $args );
$request->set_attributes( $handler );
- $request->sanitize_params();
-
$defaults = array();
foreach ( $handler['args'] as $arg => $options ) {
$check_required = $request->has_valid_params();
if ( is_wp_error( $check_required ) ) {
$response = $check_required;
+ } else {
+ $check_sanitized = $request->sanitize_params();
+ if ( is_wp_error( $check_sanitized ) ) {
+ $response = $check_sanitized;
+ }
}
}
+ /**
+ * Filters the response before executing any REST API callbacks.
+ *
+ * Allows plugins to perform additional validation after a
+ * request is initialized and matched to a registered route,
+ * but before it is executed.
+ *
+ * Note that this filter will not be called for requests that
+ * fail to authenticate or match to a registered route.
+ *
+ * @since 4.7.0
+ *
+ * @param WP_HTTP_Response $response Result to send to the client. Usually a WP_REST_Response.
+ * @param WP_REST_Server $handler ResponseHandler instance (usually WP_REST_Server).
+ * @param WP_REST_Request $request Request used to generate the response.
+ */
+ $response = apply_filters( 'rest_request_before_callbacks', $response, $handler, $request );
+
if ( ! is_wp_error( $response ) ) {
// Check permission specified on the route.
if ( ! empty( $handler['permission_callback'] ) ) {
if ( is_wp_error( $permission ) ) {
$response = $permission;
- } else if ( false === $permission || null === $permission ) {
- $response = new WP_Error( 'rest_forbidden', __( "You don't have permission to do this." ), array( 'status' => 403 ) );
+ } elseif ( false === $permission || null === $permission ) {
+ $response = new WP_Error( 'rest_forbidden', __( 'Sorry, you are not allowed to do that.' ), array( 'status' => 403 ) );
}
}
}
if ( ! is_wp_error( $response ) ) {
/**
- * Filter the REST dispatch request result.
+ * Filters the REST dispatch request result.
*
* Allow plugins to override dispatching the request.
*
* @since 4.4.0
+ * @since 4.5.0 Added `$route` and `$handler` parameters.
*
* @param bool $dispatch_result Dispatch result, will be used if not empty.
* @param WP_REST_Request $request Request used to generate the response.
+ * @param string $route Route matched for the request.
+ * @param array $handler Route handler used for the request.
*/
- $dispatch_result = apply_filters( 'rest_dispatch_request', null, $request );
+ $dispatch_result = apply_filters( 'rest_dispatch_request', null, $request, $route, $handler );
// Allow plugins to halt the request via this filter.
if ( null !== $dispatch_result ) {
}
}
+ /**
+ * Filters the response immediately after executing any REST API
+ * callbacks.
+ *
+ * Allows plugins to perform any needed cleanup, for example,
+ * to undo changes made during the {@see 'rest_request_before_callbacks'}
+ * filter.
+ *
+ * Note that this filter will not be called for requests that
+ * fail to authenticate or match to a registered route.
+ *
+ * Note that an endpoint's `permission_callback` can still be
+ * called after this filter - see `rest_send_allow_header()`.
+ *
+ * @since 4.7.0
+ *
+ * @param WP_HTTP_Response $response Result to send to the client. Usually a WP_REST_Response.
+ * @param WP_REST_Server $handler ResponseHandler instance (usually WP_REST_Server).
+ * @param WP_REST_Request $request Request used to generate the response.
+ */
+ $response = apply_filters( 'rest_request_after_callbacks', $response, $handler, $request );
+
if ( is_wp_error( $response ) ) {
$response = $this->error_to_response( $response );
} else {
'name' => get_option( 'blogname' ),
'description' => get_option( 'blogdescription' ),
'url' => get_option( 'siteurl' ),
+ 'home' => home_url(),
'namespaces' => array_keys( $this->namespaces ),
'authentication' => array(),
'routes' => $this->get_data_for_routes( $this->get_routes(), $request['context'] ),
$response->add_link( 'help', 'http://v2.wp-api.org/' );
/**
- * Filter the API root index data.
+ * Filters the API root index data.
*
* This contains the data describing the API. This includes information
* about supported authentication schemes, supported namespaces, routes
$response->add_link( 'up', rest_url( '/' ) );
/**
- * Filter the namespace index data.
+ * Filters the namespace index data.
*
* This typically is just the route data for the namespace, but you can
* add any data you'd like here.
}
/**
- * Filter the REST endpoint data.
+ * Filters the REST endpoint data.
*
* @since 4.4.0
*
}
/**
- * Filter the publicly-visible data for routes.
+ * Filters the publicly-visible data for routes.
*
* This data is exposed on indexes and can be used by clients or
* developers to investigate the site and find out how to use it. It
if ( isset( $opts['description'] ) ) {
$arg_data['description'] = $opts['description'];
}
+ if ( isset( $opts['type'] ) ) {
+ $arg_data['type'] = $opts['type'];
+ }
+ if ( isset( $opts['items'] ) ) {
+ $arg_data['items'] = $opts['items'];
+ }
$endpoint_data['args'][ $key ] = $arg_data;
}
}