/**
* REST API: WP_REST_Server class
*
* @package WordPress
* @subpackage REST_API
* @since 4.4.0
*/
/**
* Core class used to implement the WordPress REST API server.
*
* @since 4.4.0
*/
#[AllowDynamicProperties]
class WP_REST_Server {
/**
* Alias for GET transport method.
*
* @since 4.4.0
* @var string
*/
const READABLE = 'GET';
/**
* Alias for POST transport method.
*
* @since 4.4.0
* @var string
*/
const CREATABLE = 'POST';
/**
* Alias for POST, PUT, PATCH transport methods together.
*
* @since 4.4.0
* @var string
*/
const EDITABLE = 'POST, PUT, PATCH';
/**
* Alias for DELETE transport method.
*
* @since 4.4.0
* @var string
*/
const DELETABLE = 'DELETE';
/**
* Alias for GET, POST, PUT, PATCH & DELETE transport methods together.
*
* @since 4.4.0
* @var string
*/
const ALLMETHODS = 'GET, POST, PUT, PATCH, DELETE';
/**
* Namespaces registered to the server.
*
* @since 4.4.0
* @var array
*/
protected $namespaces = array();
/**
* Endpoints registered to the server.
*
* @since 4.4.0
* @var array
*/
protected $endpoints = array();
/**
* Options defined for the routes.
*
* @since 4.4.0
* @var array
*/
protected $route_options = array();
/**
* Caches embedded requests.
*
* @since 5.4.0
* @var array
*/
protected $embed_cache = array();
/**
* Stores request objects that are currently being handled.
*
* @since 6.5.0
* @var array
*/
protected $dispatching_requests = array();
/**
* Instantiates the REST server.
*
* @since 4.4.0
*/
public function __construct() {
$this->endpoints = array(
// Meta endpoints.
'/' => array(
'callback' => array( $this, 'get_index' ),
'methods' => 'GET',
'args' => array(
'context' => array(
'default' => 'view',
),
),
),
'/batch/v1' => array(
'callback' => array( $this, 'serve_batch_request_v1' ),
'methods' => 'POST',
'args' => array(
'validation' => array(
'type' => 'string',
'enum' => array( 'require-all-validate', 'normal' ),
'default' => 'normal',
),
'requests' => array(
'required' => true,
'type' => 'array',
'maxItems' => $this->get_max_batch_size(),
'items' => array(
'type' => 'object',
'properties' => array(
'method' => array(
'type' => 'string',
'enum' => array( 'POST', 'PUT', 'PATCH', 'DELETE' ),
'default' => 'POST',
),
'path' => array(
'type' => 'string',
'required' => true,
),
'body' => array(
'type' => 'object',
'properties' => array(),
'additionalProperties' => true,
),
'headers' => array(
'type' => 'object',
'properties' => array(),
'additionalProperties' => array(
'type' => array( 'string', 'array' ),
'items' => array(
'type' => 'string',
),
),
),
),
),
),
),
),
);
}
/**
* Checks the authentication headers if supplied.
*
* @since 4.4.0
*
* @return WP_Error|null|true WP_Error if authentication error occurred, null if authentication
* method wasn't used, true if authentication succeeded.
*/
public function check_authentication() {
/**
* Filters REST API authentication errors.
*
* This is used to pass a WP_Error from an authentication method back to
* the API.
*
* Authentication methods should check first if they're being used, as
* multiple authentication methods can be enabled on a site (cookies,
* HTTP basic auth, OAuth). If the authentication method hooked in is
* not actually being attempted, null should be returned to indicate
* another authentication method should check instead. Similarly,
* callbacks should ensure the value is `null` before checking for
* errors.
*
* A WP_Error instance can be returned if an error occurs, and this should
* match the format used by API methods internally (that is, the `status`
* data should be used). A callback can return `true` to indicate that
* the authentication method was used, and it succeeded.
*
* @since 4.4.0
*
* @param WP_Error|null|true $errors WP_Error if authentication error occurred, null if authentication
* method wasn't used, true if authentication succeeded.
*/
return apply_filters( 'rest_authentication_errors', null );
}
/**
* Converts an error to a response object.
*
* This iterates over all error codes and messages to change it into a flat
* array. This enables simpler client behavior, as it is represented as a
* list in JSON rather than an object/map.
*
* @since 4.4.0
* @since 5.7.0 Converted to a wrapper of {@see rest_convert_error_to_response()}.
*
* @param WP_Error $error WP_Error instance.
* @return WP_REST_Response List of associative arrays with code and message keys.
*/
protected function error_to_response( $error ) {
return rest_convert_error_to_response( $error );
}
/**
* Retrieves an appropriate error representation in JSON.
*
* Note: This should only be used in WP_REST_Server::serve_request(), as it
* cannot handle WP_Error internally. All callbacks and other internal methods
* should instead return a WP_Error with the data set to an array that includes
* a 'status' key, with the value being the HTTP status to send.
*
* @since 4.4.0
*
* @param string $code WP_Error-style code.
* @param string $message Human-readable message.
* @param int|null $status Optional. HTTP status code to send. Default null.
* @return string JSON representation of the error.
*/
protected function json_error( $code, $message, $status = null ) {
if ( $status ) {
$this->set_status( $status );
}
$error = compact( 'code', 'message' );
return wp_json_encode( $error );
}
/**
* Gets the encoding options passed to {@see wp_json_encode}.
*
* @since 6.1.0
*
* @param \WP_REST_Request $request The current request object.
*
* @return int The JSON encode options.
*/
protected function get_json_encode_options( WP_REST_Request $request ) {
$options = 0;
if ( $request->has_param( '_pretty' ) ) {
$options |= JSON_PRETTY_PRINT;
}
/**
* Filters the JSON encoding options used to send the REST API response.
*
* @since 6.1.0
*
* @param int $options JSON encoding options {@see json_encode()}.
* @param WP_REST_Request $request Current request object.
*/
return apply_filters( 'rest_json_encode_options', $options, $request );
}
/**
* Handles serving a REST API request.
*
* Matches the current server URI to a route and runs the first matching
* callback then outputs a JSON representation of the returned value.
*
* @since 4.4.0
*
* @see WP_REST_Server::dispatch()
*
* @global WP_User $current_user The currently authenticated user.
*
* @param string|null $path Optional. The request route. If not set, `$_SERVER['PATH_INFO']` will be used.
* Default null.
* @return null|false Null if not served and a HEAD request, false otherwise.
*/
public function serve_request( $path = null ) {
// Refuse to start a fresh top-level REST cycle while another dispatch
// is already in flight. Internal sub-requests must use dispatch().
if ( $this->is_dispatching() ) {
return false;
}
/* @var WP_User|null $current_user */
global $current_user;
if ( $current_user instanceof WP_User && ! $current_user->exists() ) {
/*
* If there is no current user authenticated via other means, clear
* the cached lack of user, so that an authenticate check can set it
* properly.
*
* This is done because for authentications such as Application
* Passwords, we don't want it to be accepted unless the current HTTP
* request is a REST API request, which can't always be identified early
* enough in evaluation.
*/
$current_user = null;
}
/**
* Filters whether JSONP is enabled for the REST API.
*
* @since 4.4.0
*
* @param bool $jsonp_enabled Whether JSONP is enabled. Default true.
*/
$jsonp_enabled = apply_filters( 'rest_jsonp_enabled', true );
$jsonp_callback = false;
if ( isset( $_GET['_jsonp'] ) ) {
$jsonp_callback = $_GET['_jsonp'];
}
$content_type = ( $jsonp_callback && $jsonp_enabled ) ? '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', '<' . sanitize_url( $api_root ) . '>; rel="https://api.w.org/"' );
}
/*
* Mitigate possible JSONP Flash attacks.
*
* https://miki.it/blog/2014/7/8/abusing-jsonp-with-rosetta-flash/
*/
$this->send_header( 'X-Content-Type-Options', 'nosniff' );
/**
* Filters whether the REST API is enabled.
*
* @since 4.4.0
* @deprecated 4.7.0 Use the {@see 'rest_authentication_errors'} filter to
* restrict access to the REST API.
*
* @param bool $rest_enabled Whether the REST API is enabled. Default true.
*/
apply_filters_deprecated(
'rest_enabled',
array( true ),
'4.7.0',
'rest_authentication_errors',
sprintf(
/* translators: %s: rest_authentication_errors */
__( 'The REST API can no longer be completely disabled, the %s filter can be used to restrict access to the API, instead.' ),
'rest_authentication_errors'
)
);
if ( $jsonp_callback ) {
if ( ! $jsonp_enabled ) {
echo $this->json_error( 'rest_callback_disabled', __( 'JSONP support is disabled on this site.' ), 400 );
return false;
}
if ( ! wp_check_jsonp_callback( $jsonp_callback ) ) {
echo $this->json_error( 'rest_callback_invalid', __( 'Invalid JSONP callback function.' ), 400 );
return false;
}
}
if ( empty( $path ) ) {
$path = $_SERVER['PATH_INFO'] ?? '/';
}
$request = new WP_REST_Request( $_SERVER['REQUEST_METHOD'], $path );
$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( wp_unslash( $_SERVER ) ) );
$request->set_body( self::get_raw_data() );
/*
* HTTP method override for clients that can't use PUT/PATCH/DELETE. First, we check
* $_GET['_method']. If that is not set, we check for the HTTP_X_HTTP_METHOD_OVERRIDE
* header.
*/
$method_overridden = false;
if ( isset( $_GET['_method'] ) ) {
$request->set_method( $_GET['_method'] );
} elseif ( isset( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] ) ) {
$request->set_method( $_SERVER['HTTP_X_HTTP_METHOD_OVERRIDE'] );
$method_overridden = true;
}
$expose_headers = array( 'X-WP-Total', 'X-WP-TotalPages', 'Link' );
/**
* Filters the list of response headers that are exposed to REST API CORS requests.
*
* @since 5.5.0
* @since 6.3.0 The `$request` parameter was added.
*
* @param string[] $expose_headers The list of response headers to expose.
* @param WP_REST_Request $request The request in context.
*/
$expose_headers = apply_filters( 'rest_exposed_cors_headers', $expose_headers, $request );
$this->send_header( 'Access-Control-Expose-Headers', implode( ', ', $expose_headers ) );
$allow_headers = array(
'Authorization',
'X-WP-Nonce',
'Content-Disposition',
'Content-MD5',
'Content-Type',
);
/**
* Filters the list of request headers that are allowed for REST API CORS requests.
*
* The allowed headers are passed to the browser to specify which
* headers can be passed to the REST API. By default, we allow the
* Content-* headers needed to upload files to the media endpoints.
* As well as the Authorization and Nonce headers for allowing authentication.
*
* @since 5.5.0
* @since 6.3.0 The `$request` parameter was added.
*
* @param string[] $allow_headers The list of request headers to allow.
* @param WP_REST_Request $request The request in context.
*/
$allow_headers = apply_filters( 'rest_allowed_cors_headers', $allow_headers, $request );
$this->send_header( 'Access-Control-Allow-Headers', implode( ', ', $allow_headers ) );
$result = $this->check_authentication();
if ( ! is_wp_error( $result ) ) {
$result = $this->dispatch( $request );
}
// Normalize to either WP_Error or WP_REST_Response...
$result = rest_ensure_response( $result );
// ...then convert WP_Error across.
if ( is_wp_error( $result ) ) {
$result = $this->error_to_response( $result );
}
/**
* Filters the REST 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 $server Server instance.
* @param WP_REST_Request $request Request used to generate the response.
*/
$result = apply_filters( 'rest_post_dispatch', rest_ensure_response( $result ), $this, $request );
// Wrap the response in an envelope if asked for.
if ( isset( $_GET['_envelope'] ) ) {
$embed = isset( $_GET['_embed'] ) ? rest_parse_embed_param( $_GET['_embed'] ) : false;
$result = $this->envelope_response( $result, $embed );
}
// Send extra data from response objects.
$headers = $result->get_headers();
$this->send_headers( $headers );
$code = $result->get_status();
$this->set_status( $code );
/**
* Filters whether to send no-cache headers on a REST API request.
*
* @since 4.4.0
* @since 6.3.2 Moved the block to catch the filter added on rest_cookie_check_errors() from wp-includes/rest-api.php.
*
* @param bool $rest_send_nocache_headers Whether to send no-cache headers.
*/
$send_no_cache_headers = apply_filters( 'rest_send_nocache_headers', is_user_logged_in() );
/*
* Send no-cache headers if $send_no_cache_headers is true,
* OR if the HTTP_X_HTTP_METHOD_OVERRIDE is used but resulted a 4xx response code.
*/
if ( $send_no_cache_headers || ( true === $method_overridden && str_starts_with( $code, '4' ) ) ) {
foreach ( wp_get_nocache_headers() as $header => $header_value ) {
if ( empty( $header_value ) ) {
$this->remove_header( $header );
} else {
$this->send_header( $header, $header_value );
}
}
}
/**
* Filters whether the REST API request has already been served.
*
* Allow sending the request manually - by returning true, the API result
* will not be sent to the client.
*
* @since 4.4.0
*
* @param bool $served Whether the request has already been served. Default false.
* @param WP_HTTP_Response $result Result to send to the client. Usually a `WP_REST_Response`.
* @param WP_REST_Request $request Request used to generate the response.
* @param WP_REST_Server $server Server instance.
*/
$served = apply_filters( 'rest_pre_serve_request', false, $result, $request, $this );
if ( ! $served ) {
if ( 'HEAD' === $request->get_method() ) {
return null;
}
// Embed links inside the request.
$embed = isset( $_GET['_embed'] ) ? rest_parse_embed_param( $_GET['_embed'] ) : false;
$result = $this->response_to_data( $result, $embed );
/**
* Filters the REST API response.
*
* Allows modification of the response data after inserting
* embedded data (if any) and before echoing the response data.
*
* @since 4.8.1
*
* @param array $result Response data to send to the client.
* @param WP_REST_Server $server Server instance.
* @param WP_REST_Request $request Request used to generate the response.
*/
$result = apply_filters( 'rest_pre_echo_response', $result, $this, $request );
// The 204 response shouldn't have a body.
if ( 204 === $code || null === $result ) {
return null;
}
$result = wp_json_encode( $result, $this->get_json_encode_options( $request ) );
$json_error_message = $this->get_json_last_error();
if ( $json_error_message ) {
$this->set_status( 500 );
$json_error_obj = new WP_Error(
'rest_encode_error',
$json_error_message,
array( 'status' => 500 )
);
$result = $this->error_to_response( $json_error_obj );
$result = wp_json_encode( $result->data, $this->get_json_encode_options( $request ) );
}
if ( $jsonp_callback ) {
// 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;
}
}
return null;
}
/**
* Converts a response to data to send.
*
* @since 4.4.0
* @since 5.4.0 The `$embed` parameter can now contain a list of link relations to include.
*
* @param WP_REST_Response $response Response object.
* @param bool|string[] $embed Whether to embed all links, a filtered list of link relations, or no links.
* @return array {
* Data with sub-requests embedded.
*
* @type array $_links Links.
* @type array $_embedded Embedded objects.
* }
*/
public function response_to_data( $response, $embed ) {
$data = $response->get_data();
$links = self::get_compact_response_links( $response );
if ( ! empty( $links ) ) {
// Convert links to part of the data.
$data['_links'] = $links;
}
if ( $embed ) {
$this->embed_cache = array();
// Determine if this is a numeric array.
if ( wp_is_numeric_array( $data ) ) {
foreach ( $data as $key => $item ) {
$data[ $key ] = $this->embed_links( $item, $embed );
}
} else {
$data = $this->embed_links( $data, $embed );
}
$this->embed_cache = array();
}
return $data;
}
/**
* Retrieves links from a response.
*
* Extracts the links from a response into a structured hash, suitable for
* direct output.
*
* @since 4.4.0
*
* @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_response_links( $response ) {
$links = $response->get_links();
if ( empty( $links ) ) {
return array();
}
// Convert links to part of the data.
$data = array();
foreach ( $links as $rel => $items ) {
$data[ $rel ] = array();
foreach ( $items as $item ) {
$attributes = $item['attributes'];
$attributes['href'] = $item['href'];
if ( 'self' !== $rel ) {
$data[ $rel ][] = $attributes;
continue;
}
$target_hints = self::get_target_hints_for_link( $attributes );
if ( $target_hints ) {
$attributes['targetHints'] = $target_hints;
}
$data[ $rel ][] = $attributes;
}
}
return $data;
}
/**
* Gets the target hints for a REST API Link.
*
* @since 6.7.0
*
* @param array $link The link to get target hints for.
* @return array|null
*/
protected static function get_target_hints_for_link( $link ) {
// Prefer targetHints that were specifically designated by the developer.
if ( isset( $link['targetHints']['allow'] ) ) {
return null;
}
$request = WP_REST_Request::from_url( $link['href'] );
if ( ! $request ) {
return null;
}
$server = rest_get_server();
$match = $server->match_request_to_handler( $request );
if ( is_wp_error( $match ) ) {
return null;
}
if ( is_wp_error( $request->has_valid_params() ) ) {
return null;
}
if ( is_wp_error( $request->sanitize_params() ) ) {
return null;
}
$target_hints = array();
$response = new WP_REST_Response();
$response->set_matched_route( $match[0] );
$response->set_matched_handler( $match[1] );
$headers = rest_send_allow_header( $response, $server, $request )->get_headers();
foreach ( $headers as $name => $value ) {
$name = WP_REST_Request::canonicalize_header_name( $name );
$target_hints[ $name ] = array_map( 'trim', explode( ',', $value ) );
}
return $target_hints;
}
/**
* 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
*
* @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 ( ! str_starts_with( $rel, $href_prefix ) ) {
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.
*
* @since 4.4.0
* @since 5.4.0 The `$embed` parameter can now contain a list of link relations to include.
*
* @param array $data Data from the request.
* @param bool|string[] $embed Whether to embed all links or a filtered list of link relations.
* Default true.
* @return array {
* Data with sub-requests embedded.
*
* @type array $_links Links.
* @type array $_embedded Embedded objects.
* }
*/
protected function embed_links( $data, $embed = true ) {
if ( empty( $data['_links'] ) ) {
return $data;
}
$embedded = array();
foreach ( $data['_links'] as $rel => $links ) {
/*
* If a list of relations was specified, and the link relation
* is not in the list of allowed relations, don't process the link.
*/
if ( is_array( $embed ) && ! in_array( $rel, $embed, true ) ) {
continue;
}
$embeds = array();
foreach ( $links as $item ) {
// Determine if the link is embeddable.
if ( empty( $item['embeddable'] ) ) {
// Ensure we keep the same order.
$embeds[] = array();
continue;
}
if ( ! array_key_exists( $item['href'], $this->embed_cache ) ) {
// Run through our internal routing and serve.
$request = WP_REST_Request::from_url( $item['href'] );
if ( ! $request ) {
$embeds[] = array();
continue;
}
// Embedded resources get passed context=embed.
if ( empty( $request['context'] ) ) {
$request['context'] = 'embed';
}
if ( empty( $request['per_page'] ) ) {
$matched = $this->match_request_to_handler( $request );
if ( ! is_wp_error( $matched ) && isset( $matched[1]['args']['per_page']['maximum'] ) ) {
$request['per_page'] = (int) $matched[1]['args']['per_page']['maximum'];
}
}
$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 );
$this->embed_cache[ $item['href'] ] = $this->response_to_data( $response, false );
}
$embeds[] = $this->embed_cache[ $item['href'] ];
}
// Determine if any real links were found.
$has_links = count( array_filter( $embeds ) );
if ( $has_links ) {
$embedded[ $rel ] = $embeds;
}
}
if ( ! empty( $embedded ) ) {
$data['_embedded'] = $embedded;
}
return $data;
}
/**
* Wraps the response in an envelope.
*
* The enveloping technique is used to work around browser/client
* compatibility issues. Essentially, it converts the full HTTP response to
* data instead.
*
* @since 4.4.0
* @since 6.0.0 The `$embed` parameter can now contain a list of link relations to include.
*
* @param WP_REST_Response $response Response object.
* @param bool|string[] $embed Whether to embed all links, a filtered list of link relations, or no links.
* @return WP_REST_Response New response with wrapped data
*/
public function envelope_response( $response, $embed ) {
$envelope = array(
'body' => $this->response_to_data( $response, $embed ),
'status' => $response->get_status(),
'headers' => $response->get_headers(),
);
/**
* Filters the enveloped form of a REST API response.
*
* @since 4.4.0
*
* @param array $envelope {
* Envelope data.
*
* @type array $body Response data.
* @type int $status The 3-digit HTTP status code.
* @type array $headers Map of header name to header value.
* }
* @param WP_REST_Response $response Original response data.
*/
$envelope = apply_filters( 'rest_envelope_response', $envelope, $response );
// Ensure it's still a response and return.
return rest_ensure_response( $envelope );
}
/**
* Registers a route to the server.
*
* @since 4.4.0
*
* @param string $route_namespace Namespace.
* @param string $route The REST route.
* @param array $route_args Route arguments.
* @param bool $override Optional. Whether the route should be overridden if it already exists.
* Default false.
*/
public function register_route( $route_namespace, $route, $route_args, $override = false ) {
if ( ! isset( $this->namespaces[ $route_namespace ] ) ) {
$this->namespaces[ $route_namespace ] = array();
$this->register_route(
$route_namespace,
'/' . $route_namespace,
array(
array(
'methods' => self::READABLE,
'callback' => array( $this, 'get_namespace_index' ),
'args' => array(
'namespace' => array(
'default' => $route_namespace,
),
'context' => array(
'default' => 'view',
),
),
),
)
);
}
// Associative to avoid double-registration.
$this->namespaces[ $route_namespace ][ $route ] = true;
$route_args['namespace'] = $route_namespace;
if ( $override || empty( $this->endpoints[ $route ] ) ) {
$this->endpoints[ $route ] = $route_args;
} else {
$this->endpoints[ $route ] = array_merge( $this->endpoints[ $route ], $route_args );
}
}
/**
* Retrieves the route map.
*
* The route map is an associative array with path regexes as the keys. The
* value is an indexed array with the callback function/method as the first
* item, and a bitmask of HTTP methods as the second item (see the class
* constants).
*
* Each route can be mapped to more than one callback by using an array of
* the indexed arrays. This allows mapping e.g. GET requests to one callback
* and POST requests to another.
*
* Note that the path regexes (array keys) must have @ escaped, as this is
* used as the delimiter with preg_match()
*
* @since 4.4.0
* @since 5.4.0 Added `$route_namespace` parameter.
*
* @param string $route_namespace Optionally, only return routes in the given namespace.
* @return array `'/path/regex' => array( $callback, $bitmask )` or
* `'/path/regex' => array( array( $callback, $bitmask ), ...)`.
*/
public function get_routes( $route_namespace = '' ) {
$endpoints = $this->endpoints;
if ( $route_namespace ) {
$endpoints = wp_list_filter( $endpoints, array( 'namespace' => $route_namespace ) );
}
/**
* Filters the array of available REST API endpoints.
*
* @since 4.4.0
*
* @param array $endpoints The available endpoints. An array of matching regex patterns, each mapped
* to an array of callbacks for the endpoint. These take the format
* `'/path/regex' => array( $callback, $bitmask )` or
* `'/path/regex' => array( array( $callback, $bitmask ).
*/
$endpoints = apply_filters( 'rest_endpoints', $endpoints );
// Normalize the endpoints.
$defaults = array(
'methods' => '',
'accept_json' => false,
'accept_raw' => false,
'show_in_index' => true,
'args' => array(),
);
foreach ( $endpoints as $route => &$handlers ) {
if ( isset( $handlers['callback'] ) ) {
// Single endpoint, add one deeper.
$handlers = array( $handlers );
}
if ( ! isset( $this->route_options[ $route ] ) ) {
$this->route_options[ $route ] = array();
}
foreach ( $handlers as $key => &$handler ) {
if ( ! is_numeric( $key ) ) {
// Route option, move it to the options.
$this->route_options[ $route ][ $key ] = $handler;
unset( $handlers[ $key ] );
continue;
}
$handler = wp_parse_args( $handler, $defaults );
// Allow comma-separated HTTP methods.
if ( is_string( $handler['methods'] ) ) {
$methods = explode( ',', $handler['methods'] );
} elseif ( is_array( $handler['methods'] ) ) {
$methods = $handler['methods'];
} else {
$methods = array();
}
$handler['methods'] = array();
foreach ( $methods as $method ) {
$method = strtoupper( trim( $method ) );
$handler['methods'][ $method ] = true;
}
}
}
return $endpoints;
}
/**
* Retrieves namespaces registered on the server.
*
* @since 4.4.0
*
* @return string[] List of registered namespaces.
*/
public function get_namespaces() {
return array_keys( $this->namespaces );
}
/**
* Retrieves specified options for a route.
*
* @since 4.4.0
*
* @param string $route Route pattern to fetch options for.
* @return array|null Data as an associative array if found, or null if not found.
*/
public function get_route_options( $route ) {
if ( ! isset( $this->route_options[ $route ] ) ) {
return null;
}
return $this->route_options[ $route ];
}
/**
* Matches the request to a callback and call it.
*
* @since 4.4.0
*
* @param WP_REST_Request $request Request to attempt dispatching.
* @return WP_REST_Response Response returned by the callback.
*/
public function dispatch( $request ) {
$this->dispatching_requests[] = $request;
/**
* Filters the pre-calculated result of a REST API dispatch request.
*
* Allow hijacking the request before dispatching by returning a non-empty. The returned value
* will be used to serve the request instead.
*
* @since 4.4.0
*
* @param mixed $result Response to replace the requested version with. Can be anything
* a normal endpoint can return, or null to not hijack the request.
* @param WP_REST_Server $server Server instance.
* @param WP_REST_Request $request Request used to generate the response.
*/
$result = apply_filters( 'rest_pre_dispatch', null, $this, $request );
if ( ! empty( $result ) ) {
// Normalize to either WP_Error or WP_REST_Response...
$result = rest_ensure_response( $result );
// ...then convert WP_Error across.
if ( is_wp_error( $result ) ) {
$result = $this->error_to_response( $result );
}
array_pop( $this->dispatching_requests );
return $result;
}
$error = null;
$matched = $this->match_request_to_handler( $request );
if ( is_wp_error( $matched ) ) {
$response = $this->error_to_response( $matched );
array_pop( $this->dispatching_requests );
return $response;
}
list( $route, $handler ) = $matched;
if ( ! is_callable( $handler['callback'] ) ) {
$error = new WP_Error(
'rest_invalid_handler',
__( 'The handler for the route is invalid.' ),
array( 'status' => 500 )
);
}
if ( ! is_wp_error( $error ) ) {
$check_required = $request->has_valid_params();
if ( is_wp_error( $check_required ) ) {
$error = $check_required;
} else {
$check_sanitized = $request->sanitize_params();
if ( is_wp_error( $check_sanitized ) ) {
$error = $check_sanitized;
}
}
}
$response = $this->respond_to_request( $request, $route, $handler, $error );
array_pop( $this->dispatching_requests );
return $response;
}
/**
* Returns whether the REST server is currently dispatching / responding to a request.
*
* This may be a standalone REST API request, or an internal request dispatched from within a regular page load.
*
* @since 6.5.0
*
* @return bool Whether the REST server is currently handling a request.
*/
public function is_dispatching() {
return (bool) $this->dispatching_requests;
}
/**
* Matches a request object to its handler.
*
* @access private
* @since 5.6.0
*
* @param WP_REST_Request $request The request object.
* @return array|WP_Error The route and request handler on success or a WP_Error instance if no handler was found.
*/
protected function match_request_to_handler( $request ) {
$method = $request->get_method();
$path = $request->get_route();
$with_namespace = array();
foreach ( $this->get_namespaces() as $namespace ) {
if ( str_starts_with( trailingslashit( ltrim( $path, '/' ) ), $namespace ) ) {
$with_namespace[] = $this->get_routes( $namespace );
}
}
if ( $with_namespace ) {
$routes = array_merge( ...$with_namespace );
} else {
$routes = $this->get_routes();
}
foreach ( $routes as $route => $handlers ) {
$match = preg_match( '@^' . $route . '$@i', $path, $matches );
if ( ! $match ) {
continue;
}
$args = array();
foreach ( $matches as $param => $value ) {
if ( ! is_int( $param ) ) {
$args[ $param ] = $value;
}
}
foreach ( $handlers as $handler ) {
$callback = $handler['callback'];
// 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;
}
if ( ! is_callable( $callback ) ) {
return array( $route, $handler );
}
$request->set_url_params( $args );
$request->set_attributes( $handler );
$defaults = array();
foreach ( $handler['args'] as $arg => $options ) {
if ( isset( $options['default'] ) ) {
$defaults[ $arg ] = $options['default'];
}
}
$request->set_default_params( $defaults );
return array( $route, $handler );
}
}
return new WP_Error(
'rest_no_route',
__( 'No route was found matching the URL and request method.' ),
array( 'status' => 404 )
);
}
/**
* Dispatches the request to the callback handler.
*
* @access private
* @since 5.6.0
*
* @param WP_REST_Request $request The request object.
* @param string $route The matched route regex.
* @param array $handler The matched route handler.
* @param WP_Error|null $response The current error object if any.
* @return WP_REST_Response
*/
protected function respond_to_request( $request, $route, $handler, $response ) {
/**
* 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_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
* Usually a WP_REST_Response or WP_Error.
* @param array $handler Route handler used for the request.
* @param WP_REST_Request $request Request used to generate the response.
*/
$response = apply_filters( 'rest_request_before_callbacks', $response, $handler, $request );
// Check permission specified on the route.
if ( ! is_wp_error( $response ) && ! empty( $handler['permission_callback'] ) ) {
$permission = call_user_func( $handler['permission_callback'], $request );
if ( is_wp_error( $permission ) ) {
$response = $permission;
} elseif ( false === $permission || null === $permission ) {
$response = new WP_Error(
'rest_forbidden',
__( 'Sorry, you are not allowed to do that.' ),
array( 'status' => rest_authorization_required_code() )
);
}
}
if ( ! is_wp_error( $response ) ) {
/**
* Filters the REST API dispatch request result.
*
* Allow plugins to override dispatching the request.
*
* @since 4.4.0
* @since 4.5.0 Added `$route` and `$handler` parameters.
*
* @param mixed $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, $route, $handler );
// Allow plugins to halt the request via this filter.
if ( null !== $dispatch_result ) {
$response = $dispatch_result;
} else {
$response = call_user_func( $handler['callback'], $request );
}
}
/**
* 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_REST_Response|WP_HTTP_Response|WP_Error|mixed $response Result to send to the client.
* Usually a WP_REST_Response or WP_Error.
* @param array $handler Route handler used for the request.
* @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 {
$response = rest_ensure_response( $response );
}
$response->set_matched_route( $route );
$response->set_matched_handler( $handler );
return $response;
}
/**
* Returns if an error occurred during most recent JSON encode/decode.
*
* Strings to be translated will be in format like
* "Encoding error: Maximum stack depth exceeded".
*
* @since 4.4.0
*
* @return false|string Boolean false or string error message.
*/
protected function get_json_last_error() {
if ( JSON_ERROR_NONE === json_last_error() ) {
return false;
}
return json_last_error_msg();
}
/**
* Retrieves the site index.
*
* This endpoint describes the capabilities of the site.
*
* @since 4.4.0
*
* @param WP_REST_Request $request Request data.
* @return WP_REST_Response The API root index data.
*/
public function get_index( $request ) {
// General site data.
$available = array(
'name' => get_option( 'blogname' ),
'description' => get_option( 'blogdescription' ),
'url' => get_option( 'siteurl' ),
'home' => home_url(),
'gmt_offset' => get_option( 'gmt_offset' ),
'timezone_string' => get_option( 'timezone_string' ),
'page_for_posts' => (int) get_option( 'page_for_posts' ),
'page_on_front' => (int) get_option( 'page_on_front' ),
'show_on_front' => get_option( 'show_on_front' ),
'namespaces' => array_keys( $this->namespaces ),
'authentication' => array(),
'routes' => $this->get_data_for_routes( $this->get_routes(), $request['context'] ),
);
// Add media processing settings for users who can upload files.
if ( wp_is_client_side_media_processing_enabled() && current_user_can( 'upload_files' ) ) {
// Image sizes keyed by name for client-side media processing.
$available['image_sizes'] = array();
foreach ( wp_get_registered_image_subsizes() as $name => $size ) {
$available['image_sizes'][ $name ] = $size;
}
/** This filter is documented in wp-admin/includes/image.php */
$available['image_size_threshold'] = (int) apply_filters( 'big_image_size_threshold', 2560, array( 0, 0 ), '', 0 );
/** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */
$available['image_strip_meta'] = (bool) apply_filters( 'image_strip_meta', true );
/*
* On the server, this filter receives the decoded image's actual bit depth.
* The client path never decodes the image on the server, so the filter is
* applied with 16 (the maximum depth the client encoder can produce) as
* both the value and the current depth. The client caps its output bit
* depth at the filtered value, so a plugin lowering it (e.g. to 8) takes
* effect on client-generated images too.
*/
/** This filter is documented in wp-includes/class-wp-image-editor-imagick.php */
$available['image_max_bit_depth'] = (int) apply_filters( 'image_max_bit_depth', 16, 16 );
}
$response = new WP_REST_Response( $available );
$fields = $request['_fields'] ?? '';
$fields = wp_parse_list( $fields );
if ( empty( $fields ) ) {
$fields[] = '_links';
}
if ( $request->has_param( '_embed' ) ) {
$fields[] = '_embedded';
}
if ( rest_is_field_included( '_links', $fields ) || rest_is_field_included( '_embedded', $fields ) ) {
$response->add_link( 'help', 'https://developer.wordpress.org/rest-api/' );
$this->add_active_theme_link_to_index( $response );
$this->add_site_logo_to_index( $response );
$this->add_site_icon_to_index( $response );
} else {
if ( rest_is_field_included( 'site_logo', $fields ) ) {
$this->add_site_logo_to_index( $response );
}
if ( rest_is_field_included( 'site_icon', $fields ) || rest_is_field_included( 'site_icon_url', $fields ) ) {
$this->add_site_icon_to_index( $response );
}
}
/**
* Filters the REST API root index data.
*
* This contains the data describing the API. This includes information
* about supported authentication schemes, supported namespaces, routes
* available on the API, and a small amount of data about the site.
*
* @since 4.4.0
* @since 6.0.0 Added `$request` parameter.
*
* @param WP_REST_Response $response Response data.
* @param WP_REST_Request $request Request data.
*/
return apply_filters( 'rest_index', $response, $request );
}
/**
* Adds a link to the active theme for users who have proper permissions.
*
* @since 5.7.0
*
* @param WP_REST_Response $response REST API response.
*/
protected function add_active_theme_link_to_index( WP_REST_Response $response ) {
$should_add = current_user_can( 'switch_themes' ) || current_user_can( 'manage_network_themes' );
if ( ! $should_add && current_user_can( 'edit_posts' ) ) {
$should_add = true;
}
if ( ! $should_add ) {
foreach ( get_post_types( array( 'show_in_rest' => true ), 'objects' ) as $post_type ) {
if ( current_user_can( $post_type->cap->edit_posts ) ) {
$should_add = true;
break;
}
}
}
if ( $should_add ) {
$theme = wp_get_theme();
$response->add_link( 'https://api.w.org/active-theme', rest_url( 'wp/v2/themes/' . $theme->get_stylesheet() ) );
}
}
/**
* Exposes the site logo through the WordPress REST API.
*
* This is used for fetching this information when user has no rights
* to update settings.
*
* @since 5.8.0
*
* @param WP_REST_Response $response REST API response.
*/
protected function add_site_logo_to_index( WP_REST_Response $response ) {
$site_logo_id = get_theme_mod( 'custom_logo', 0 );
$this->add_image_to_index( $response, $site_logo_id, 'site_logo' );
}
/**
* Exposes the site icon through the WordPress REST API.
*
* This is used for fetching this information when user has no rights
* to update settings.
*
* @since 5.9.0
*
* @param WP_REST_Response $response REST API response.
*/
protected function add_site_icon_to_index( WP_REST_Response $response ) {
$site_icon_id = get_option( 'site_icon', 0 );
$this->add_image_to_index( $response, $site_icon_id, 'site_icon' );
$response->data['site_icon_url'] = get_site_icon_url();
}
/**
* Exposes an image through the WordPress REST API.
* This is used for fetching this information when user has no rights
* to update settings.
*
* @since 5.9.0
*
* @param WP_REST_Response $response REST API response.
* @param int $image_id Image attachment ID.
* @param string $type Type of Image.
*/
protected function add_image_to_index( WP_REST_Response $response, $image_id, $type ) {
$response->data[ $type ] = (int) $image_id;
if ( $image_id ) {
$response->add_link(
'https://api.w.org/featuredmedia',
rest_url( rest_get_route_for_post( $image_id ) ),
array(
'embeddable' => true,
'type' => $type,
)
);
}
}
/**
* Retrieves the index for a namespace.
*
* @since 4.4.0
*
* @param WP_REST_Request $request REST request instance.
* @return WP_REST_Response|WP_Error WP_REST_Response instance if the index was found,
* WP_Error if the namespace isn't set.
*/
public function get_namespace_index( $request ) {
$namespace = $request['namespace'];
if ( ! isset( $this->namespaces[ $namespace ] ) ) {
return new WP_Error(
'rest_invalid_namespace',
__( 'The specified namespace could not be found.' ),
array( 'status' => 404 )
);
}
$routes = $this->namespaces[ $namespace ];
$endpoints = array_intersect_key( $this->get_routes(), $routes );
$data = array(
'namespace' => $namespace,
'routes' => $this->get_data_for_routes( $endpoints, $request['context'] ),
);
$response = rest_ensure_response( $data );
// Link to the root index.
$response->add_link( 'up', rest_url( '/' ) );
/**
* Filters the REST API namespace index data.
*
* This typically is just the route data for the namespace, but you can
* add any data you'd like here.
*
* @since 4.4.0
*
* @param WP_REST_Response $response Response data.
* @param WP_REST_Request $request Request data. The namespace is passed as the 'namespace' parameter.
*/
return apply_filters( 'rest_namespace_index', $response, $request );
}
/**
* Retrieves the publicly-visible data for routes.
*
* @since 4.4.0
*
* @param array $routes Routes to get data for.
* @param string $context Optional. Context for data. Accepts 'view' or 'help'. Default 'view'.
* @return array[] Route data to expose in indexes, keyed by route.
*/
public function get_data_for_routes( $routes, $context = 'view' ) {
$available = array();
// Find the available routes.
foreach ( $routes as $route => $callbacks ) {
$data = $this->get_data_for_route( $route, $callbacks, $context );
if ( empty( $data ) ) {
continue;
}
/**
* Filters the publicly-visible data for a single REST API route.
*
* @since 4.4.0
*
* @param array $data Publicly-visible data for the route.
*/
$available[ $route ] = apply_filters( 'rest_endpoints_description', $data );
}
/**
* Filters the publicly-visible data for REST API 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
* acts as a form of self-documentation.
*
* @since 4.4.0
*
* @param array[] $available Route data to expose in indexes, keyed by route.
* @param array $routes Internal route data as an associative array.
*/
return apply_filters( 'rest_route_data', $available, $routes );
}
/**
* Retrieves publicly-visible data for the route.
*
* @since 4.4.0
*
* @param string $route Route to get data for.
* @param array $callbacks Callbacks to convert to data.
* @param string $context Optional. Context for the data. Accepts 'view' or 'help'. Default 'view'.
* @return array|null Data for the route, or null if no publicly-visible data.
*/
public function get_data_for_route( $route, $callbacks, $context = 'view' ) {
$data = array(
'namespace' => '',
'methods' => array(),
'endpoints' => array(),
);
$allow_batch = false;
if ( isset( $this->route_options[ $route ] ) ) {
$options = $this->route_options[ $route ];
if ( isset( $options['namespace'] ) ) {
$data['namespace'] = $options['namespace'];
}
$allow_batch = $options['allow_batch'] ?? false;
if ( isset( $options['schema'] ) && 'help' === $context ) {
$data['schema'] = call_user_func( $options['schema'] );
}
}
$allowed_schema_keywords = array_flip( wp_get_json_schema_allowed_keywords( 'rest-api' ) );
$route = preg_replace( '#\(\?P<(\w+?)>.*?\)#', '{$1}', $route );
foreach ( $callbacks as $callback ) {
// Skip to the next route if any callback is hidden.
if ( empty( $callback['show_in_index'] ) ) {
continue;
}
$data['methods'] = array_merge( $data['methods'], array_keys( $callback['methods'] ) );
$endpoint_data = array(
'methods' => array_keys( $callback['methods'] ),
);
$callback_batch = $callback['allow_batch'] ?? $allow_batch;
if ( $callback_batch ) {
$endpoint_data['allow_batch'] = $callback_batch;
}
if ( isset( $callback['args'] ) ) {
$endpoint_data['args'] = array();
foreach ( $callback['args'] as $key => $opts ) {
if ( is_string( $opts ) ) {
$opts = array( $opts => 0 );
} elseif ( ! is_array( $opts ) ) {
$opts = array();
}
$arg_data = array_intersect_key( $opts, $allowed_schema_keywords );
$arg_data['required'] = ! empty( $opts['required'] );
$endpoint_data['args'][ $key ] = $arg_data;
}
}
$data['endpoints'][] = $endpoint_data;
// For non-variable routes, generate links.
if ( ! str_contains( $route, '{' ) ) {
$data['_links'] = array(
'self' => array(
array(
'href' => rest_url( $route ),
),
),
);
}
}
if ( empty( $data['methods'] ) ) {
// No methods supported, hide the route.
return null;
}
return $data;
}
/**
* Gets the maximum number of requests that can be included in a batch.
*
* @since 5.6.0
*
* @return int The maximum requests.
*/
protected function get_max_batch_size() {
/**
* Filters the maximum number of REST API requests that can be included in a batch.
*
* @since 5.6.0
*
* @param int $max_size The maximum size.
*/
return apply_filters( 'rest_get_max_batch_size', 25 );
}
/**
* Serves the batch/v1 request.
*
* @since 5.6.0
*
* @param WP_REST_Request $batch_request The batch request object.
* @return WP_REST_Response The generated response object.
*/
public function serve_batch_request_v1( WP_REST_Request $batch_request ) {
$requests = array();
foreach ( $batch_request['requests'] as $args ) {
$parsed_url = wp_parse_url( $args['path'] );
if ( false === $parsed_url ) {
$requests[] = new WP_Error( 'parse_path_failed', __( 'Could not parse the path.' ), array( 'status' => 400 ) );
continue;
}
$single_request = new WP_REST_Request( $args['method'] ?? 'POST', $parsed_url['path'] );
if ( ! empty( $parsed_url['query'] ) ) {
$query_args = array();
wp_parse_str( $parsed_url['query'], $query_args );
$single_request->set_query_params( $query_args );
}
if ( ! empty( $args['body'] ) ) {
$single_request->set_body_params( $args['body'] );
}
if ( ! empty( $args['headers'] ) ) {
$single_request->set_headers( $args['headers'] );
}
$requests[] = $single_request;
}
$matches = array();
$validation = array();
$has_error = false;
foreach ( $requests as $single_request ) {
if ( is_wp_error( $single_request ) ) {
$has_error = true;
$matches[] = $single_request;
$validation[] = $single_request;
continue;
}
$match = $this->match_request_to_handler( $single_request );
$matches[] = $match;
$error = null;
if ( is_wp_error( $match ) ) {
$error = $match;
}
if ( ! $error ) {
list( $route, $handler ) = $match;
if ( isset( $handler['allow_batch'] ) ) {
$allow_batch = $handler['allow_batch'];
} else {
$route_options = $this->get_route_options( $route );
$allow_batch = $route_options['allow_batch'] ?? false;
}
if ( ! is_array( $allow_batch ) || empty( $allow_batch['v1'] ) ) {
$error = new WP_Error(
'rest_batch_not_allowed',
__( 'The requested route does not support batch requests.' ),
array( 'status' => 400 )
);
}
}
if ( ! $error ) {
$check_required = $single_request->has_valid_params();
if ( is_wp_error( $check_required ) ) {
$error = $check_required;
}
}
if ( ! $error ) {
$check_sanitized = $single_request->sanitize_params();
if ( is_wp_error( $check_sanitized ) ) {
$error = $check_sanitized;
}
}
if ( $error ) {
$has_error = true;
$validation[] = $error;
} else {
$validation[] = true;
}
}
$responses = array();
if ( $has_error && 'require-all-validate' === $batch_request['validation'] ) {
foreach ( $validation as $valid ) {
if ( is_wp_error( $valid ) ) {
$responses[] = $this->envelope_response( $this->error_to_response( $valid ), false )->get_data();
} else {
$responses[] = null;
}
}
return new WP_REST_Response(
array(
'failed' => 'validation',
'responses' => $responses,
),
WP_Http::MULTI_STATUS
);
}
foreach ( $requests as $i => $single_request ) {
if ( is_wp_error( $single_request ) ) {
$result = $this->error_to_response( $single_request );
$responses[] = $this->envelope_response( $result, false )->get_data();
continue;
}
$clean_request = clone $single_request;
$clean_request->set_url_params( array() );
$clean_request->set_attributes( array() );
$clean_request->set_default_params( array() );
/** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
$result = apply_filters( 'rest_pre_dispatch', null, $this, $clean_request );
if ( empty( $result ) ) {
$match = $matches[ $i ];
$error = null;
if ( is_wp_error( $validation[ $i ] ) ) {
$error = $validation[ $i ];
}
if ( is_wp_error( $match ) ) {
$result = $this->error_to_response( $match );
} else {
list( $route, $handler ) = $match;
if ( ! $error && ! is_callable( $handler['callback'] ) ) {
$error = new WP_Error(
'rest_invalid_handler',
__( 'The handler for the route is invalid' ),
array( 'status' => 500 )
);
}
$result = $this->respond_to_request( $single_request, $route, $handler, $error );
}
}
/** This filter is documented in wp-includes/rest-api/class-wp-rest-server.php */
$result = apply_filters( 'rest_post_dispatch', rest_ensure_response( $result ), $this, $single_request );
$responses[] = $this->envelope_response( $result, false )->get_data();
}
return new WP_REST_Response( array( 'responses' => $responses ), WP_Http::MULTI_STATUS );
}
/**
* Sends an HTTP status code.
*
* @since 4.4.0
*
* @param int $code HTTP status.
*/
protected function set_status( $code ) {
status_header( $code );
}
/**
* Sends an HTTP header.
*
* @since 4.4.0
*
* @param string $key Header key.
* @param string $value Header value.
*/
public function send_header( $key, $value ) {
/*
* Sanitize as per RFC2616 (Section 4.2):
*
* Any LWS that occurs between field-content MAY be replaced with a
* single SP before interpreting the field value or forwarding the
* message downstream.
*/
$value = preg_replace( '/\s+/', ' ', $value );
header( sprintf( '%s: %s', $key, $value ) );
}
/**
* Sends multiple HTTP headers.
*
* @since 4.4.0
*
* @param array $headers Map of header name to header value.
*/
public function send_headers( $headers ) {
foreach ( $headers as $key => $value ) {
$this->send_header( $key, $value );
}
}
/**
* Removes an HTTP header from the current response.
*
* @since 4.8.0
*
* @param string $key Header key.
*/
public function remove_header( $key ) {
header_remove( $key );
}
/**
* Retrieves the raw request entity (body).
*
* @since 4.4.0
*
* @global string $HTTP_RAW_POST_DATA Raw post data.
*
* @return string Raw request data.
*/
public static function get_raw_data() {
// phpcs:disable PHPCompatibility.Variables.RemovedPredefinedGlobalVariables.http_raw_post_dataDeprecatedRemoved
global $HTTP_RAW_POST_DATA;
// $HTTP_RAW_POST_DATA was deprecated in PHP 5.6 and removed in PHP 7.0.
if ( ! isset( $HTTP_RAW_POST_DATA ) ) {
$HTTP_RAW_POST_DATA = file_get_contents( 'php://input' );
}
return $HTTP_RAW_POST_DATA;
// phpcs:enable
}
/**
* Extracts headers from a PHP-style $_SERVER array.
*
* @since 4.4.0
*
* @param array $server Associative array similar to `$_SERVER`.
* @return array Headers extracted from the input.
*/
public function get_headers( $server ) {
$headers = array();
// CONTENT_* headers are not prefixed with HTTP_.
$additional = array(
'CONTENT_LENGTH' => true,
'CONTENT_MD5' => true,
'CONTENT_TYPE' => true,
);
foreach ( $server as $key => $value ) {
if ( str_starts_with( $key, 'HTTP_' ) ) {
$headers[ substr( $key, 5 ) ] = $value;
} elseif ( 'REDIRECT_HTTP_AUTHORIZATION' === $key && empty( $server['HTTP_AUTHORIZATION'] ) ) {
/*
* In some server configurations, the authorization header is passed in this alternate location.
* Since it would not be passed in both places we do not check for both headers and resolve.
*/
$headers['AUTHORIZATION'] = $value;
} elseif ( isset( $additional[ $key ] ) ) {
$headers[ $key ] = $value;
}
}
return $headers;
}
}
Londres, 25 de julio de 2026: SQUARE ENIX® ha revelado hoy información inédita sobre Evercold, la sexta expansión del aplaudido MMORPG FINAL FANTASY XIV Online, cuya publicación está prevista para enero de 2027, y sobre los nuevos contenidos jugables, la serie de Crossover Raids de la trilogía remake de FINAL FANTASY VII y la fecha de lanzamiento de la versión para Nintendo Switch 2, entre otras cosas.
También se ha mostrado un avance extendido para Evercold que nos deja entrever el mundo de The Fourth y las aventuras que esperan al Warrior of Light al empezar el arco narrativo de la Godless Realms Saga. El avance extendido de Evercold puede verse aquí: https://youtu.be/urDT_At-AX4
Durante el discurso inaugural del Fan Festival 2026 de Berlín, el productor y director Naoki Yoshida ha revelado el primer oficio que debutará en Evercold: Bastion, un main tank que blande dos enormes escudos llamados skyltborg. También se ha mostrado un tráiler para ver el nuevo oficio en acción.
El tráiler del oficio Bastion puede verse aquí: https://youtu.be/uaZlrprwSq4
También se han compartido más detalles sobre las novedades que incluirá Evercold, como los cambios en la estructura narrativa que se implementarán en la historia principal, el mundo y el sistema de juego, de forma que tendremos más libertad para elegir qué área completar primero entre los distintos caminos de la historia principal. A través de Auto Content Balancing, la dificultad y el nivel de los enemigos se ajustarán a nuestro nivel a medida que vamos recorriendo Dungeons en el orden que queramos. Además, se ha mostrado por primera vez la función Character Action Skins, que nos permite asignar animaciones alternativas para personalizar acciones generales como Teleport y Warp, y en el futuro se ampliará a más acciones, como aquellas específicas de los oficios. La personalización se ampliará de forma que tendremos más formas de hacer que nuestro personaje destaque, ya sea con más opciones cromáticas en el nuevo selector de color, con la posibilidad de modificar pequeños detalles como los ojos o los labios o con nuevos elementos como la sombra de ojos, entre otras cosas.
Tras la revelación de la serie de Alliance Raids EVANGELION – Ghosts of Desire el pasado mes de abril, Yoshida ha compartido un nuevo avance y ha anunciado que el aplaudido director y animador Mahiro Maeda se sumará al proyecto como invitado especial en calidad de artista y diseñador. A lo largo de su carrera, Maheda ha contribuido a numerosos proyectos de éxito, incluyendo Mad Max: Fury Road (2015) como artista y diseñador de concepto, y Evangelion: 3.0+1.0 Thrice Upon a Time (2021) como director, director de arte conceptual, artista de storyboards, diseñador de ambientaciones y animador principal.
El nuevo avance puede verse aquí: https://youtu.be/ecjI-T1zP-o
Con director de la serie remake de FINAL FANTASY VII sobre el escenario, Naoki Hamaguchi, Yoshida también ha presentado Beyond the Lifestream, una nueva serie de Raids para 8 jugadores inspiradas en FINAL FANTASY VII con las que ponernos a prueba en Evercold. Ambos equipos de desarrollo están trabajando codo con codo para crear unas Raids que brindarán una experiencia de juego novedosa.
Por otra parte, FINAL FANTASY XIV se lanzará en Nintendo Switch 2 el martes 4 de agosto de 2026, y ya es posible reservarlo a partir de hoy desde Nintendo eShop. Con el objetivo de garantizar la estabilidad de los servidores, habrá un periodo de suscripción gratuita de un mes aproximadamente en Nintendo Switch 2. Ya sea tanteando la prueba gratuita, empezando la aventura con la Starter Edition o metiéndose de lleno en la historia con la Complete Edition, muy pronto el enorme mundo de FINAL FANTASY XIV estará disponible para un público aún más amplio.
Más información: https://sqex.to/Q0D06
A continuación se detallan los contenidos revelados en el discurso inaugural:
Por último, Yoshida ha presentado la Evercold Collector’s Bundle, que incluye los siguientes objetos físicos: una caja especial de Evercold, una detallada figura del Reaper, un llavero metálico de Ethos, un minipeluche de Köttr y un tapiz de The Fourth. Este lote también incluye la digital Collector’s Edition, que contiene tres objetos para el juego: el arma para Bastion Crystal Skyltborg, Valhallar Odin Mount y Entreat Crystal Emote. Próximamente se revelarán más detalles sobre la disponibilidad y las reservas.
Más información y novedades sobre Evercold en la página web correspondiente: https://sqex.to/LtSxV
El discurso inaugural (en inglés) puede verse en YouTube dentro de la transmisión en directo del primer día del Fan Festival 2026 en Berlín: https://www.youtube.com/watch?v=C0L5kqKlGcQ
El FINAL FANTASY XIV Fan Festival 2026 continúa en Berlín durante el domingo 26 de julio. Es posible seguir lo que ocurre sobre el escenario en directo desde Twitch y YouTube. También están a la venta en FINAL FANTASY XIV Online Store nuevos objetos para celebrar el evento dentro del juego. Estos objetos conmemorativos estarán disponibles por un precio reducido hasta el miércoles 12 de agosto de 2026. Este año los conciertos de piano y de THE PRIMALS también están disponibles en forma de streaming de pago hasta el domingo 16 de agosto de 2026.
El parche 7.55 de FINAL FANTASY XIV se publicará el martes 28 de julio y traerá novedades en cuanto a Occult Crescent, Phantom Weapons, Inconceivably Further Hildibrand Adventures y misiones Allied Society. Más información en la página web oficial del parche 7.5: https://na.finalfantasyxiv.com/dawntrail/patch_7_5/
Con más de 30 millones de jugadores registrados, este es el momento ideal para adentrarse por primera vez en la saga de FINAL FANTASY XIV Online, que tan buenas críticas ha recibido. La prueba gratuita ahora incluye todo el contenido de A Realm Reborn, Heavensward, Stormblood y Shadowbringers (y las actualizaciones hasta el parche 5.58). Con la prueba gratuita es posible disfrutar de cientos de horas de juego y de una historia que equivale a cuatro títulos de FINAL FANTASY completos, pero sin límite de tiempo. Más información: http://sqex.to/FFXIVFreeTrial
Enlaces relacionados
Londres (28 de abril de 2026) – Square Enix ha lanzado hoy el último gran parche de FINAL FANTASY XIV Online con la actualización 7.5: Trail to the Heavens.
El viaje del Guerrero de la Luz continúa con nuevas misiones del escenario principal en la primera parte de la conclusión en dos entregas de la historia desarrollada a lo largo de la serie de parches de Dawntrail. También llegan nuevas incorporaciones y desafíos, incluyendo la entrega final de la serie de incursiones de alianza Echoes of Vana’diel, la mazmorra The Clyteum, el desafío contra Enuo en dificultades Normal y Extrema, y la trial Shinryu’s Domain (Unreal).
El parche introduce además una amplia variedad de novedades para los jugadores, como la incorporación de The Dusk Vigil y Shisui of the Violet Tides al sistema de Duty Support, una nueva arena JcJ de Crystalline Conflict, actualizaciones en Ocean Fishing, mejoras en el sistema de vivienda y mucho más. Por último, los nuevos jugadores podrán disfrutar de la última actualización de la versión de prueba gratuita, que ahora permite acceder a la aclamada expansión Shadowbringers (incluyendo actualizaciones hasta el parche 5.58) sin limitaciones de tiempo de juego.
Puedes ver el tráiler del parche 7.5 aquí: https://youtu.be/nsMbfTpL63c
Más detalles sobre el contenido del parche 7.5:
Los jugadores también pueden esperar futuras actualizaciones que llegarán como parte de la serie de parches 7.5x:
Para obtener más información sobre el contenido del parche 7.5, visita la web promocional:
https://eu.finalfantasyxiv.com/dawntrail/patch_7_5/
La próxima expansión de FINAL FANTASY XIV Online fue anunciada por el productor y director Naoki Yoshida durante la keynote del FINAL FANTASY XIV Fan Festival 2026 en Anaheim el viernes 24 de abril de 2026. Bajo el título Evercold, la expansión continuará la historia del Guerrero de la Luz e iniciará el arco narrativo Godless Realms Saga, junto a una gran cantidad de nuevos contenidos y actualizaciones.
Más información sobre Evercold: https://eu.finalfantasyxiv.com/evercold/
Tráiler de presentación: https://youtu.be/b0IWVeqvQGk
Con más de 35 millones de cuentas registradas en todo el mundo, nunca ha habido un mejor momento para comenzar la aventura en el aclamado universo de FINAL FANTASY XIV Online. La versión de prueba gratuita incluye todo el contenido de A Realm Reborn, así como las expansiones Heavensward, Stormblood y Shadowbringers (incluyendo actualizaciones hasta el parche 5.58). Los jugadores pueden disfrutar de cientos de horas de contenido galardonado sin limitaciones de tiempo de juego.
Más información: https://sqex.to/FFXIVFreeTrial
Londres, 24 de abril de 2026: SQUARE ENIX® ha anunciado hoy Evercold, la sexta expansión del aplaudido FINAL FANTASY XIV Online, cuya publicación está prevista para enero de 2027.
El encargado de dar la noticia ha sido el productor y director Naoki Yoshida durante su discurso inaugural del FINAL FANTASY XIV Fan Festival 2026 en Anaheim, con un avance de Evercold que sienta las bases del arco narrativo de Godless Realms Saga. El avance deja entrever las aventuras que vivirá el Warrior of Light en The Fourth.
El avance de Evercold puede verse aquí: https://youtu.be/b0IWVeqvQGk
Yoshida también ha revelado que EVANGELION – Ghosts of Desire llegará con Evercold. Esta fabulosa nueva serie de Alliance Raids es un cruce con la popular serie de anime japonesa EVANGELION y surge de la colaboración con khara, inc., célebres por su trabajo en la serie EVANGELION. Desde que se emitió por primera vez en televisión en 1995, la misteriosa y profunda trama de EVANGELION, sumada a su innovador y elegante estilo visual, ha servido de inspiración para un sinnúmero de creadores de todo el mundo, y la inconfundible saga se ha ido expandiendo con varios largometrajes de animación y una adaptación a manga.
El avance de EVANGELION – Ghosts of Desire puede verse aquí: https://youtu.be/680o2J54FKY
Durante el discurso inaugural, Yoshida también ha anunciado una renovación sin precedentes del sistema de combate que supone la incorporación de los modos Reborn y Evolved al juego con el lanzamiento de Evercold. El modo Reborn se basará en el sistema de combate y mecánicas de los oficios actuales, mientras que el modo Evolved concederá más importancia a los oficios como identidad. Se presentarán más detalles e imágenes del juego en directo durante el panel del desarrollo del primer día del Fan Festival 2026 en Anaheim, que también podrá verse aquí: https://www.youtube.com/watch?v=_aCGzxWczCM (Solo será posible jugar con el modo Evolved los nuevos oficios que se introducirán con Evercold.)
A continuación se detalla todo lo que incluirá la próxima expansión, Evercold, tal como se ha anunciado durante el discurso inaugural de hoy:
El lanzamiento de Evercold y la sucesiva serie de parches nos permitirá disfrutar de numerosas novedades en cuanto al diseño del juego FINAL FANTASY XIV, además de cambios en el sistema de combate:
Más información y futuras novedades sobre Evercold en la página web correspondiente: https://sqex.to/LtSxV
Durante el discurso inaugural también se ha proporcionado más información sobre la serie de parches 7.5x para FINAL FANTASY XIV, incluyendo la Raid Dancing Mad (Ultimate). Se ha repasado el calendario para los próximos parches hasta el lanzamiento de 8.0, donde cabe esperar los siguientes contenidos adicionales:
Además, el 28 de abril la prueba gratuita del aplaudido FINAL FANTASY XIV Online se ampliará para incluir su tercera expansión: Shadowbringers. Pronto todo el contenido hasta el parche 5.58 estará incluido, lo cual abarca tanto A Realm Reborn como las expansiones Heavensward y Stormblood, así que esta es la ocasión perfecta para unirse a más de 30 millones de usuarios registrados y emprender nuestra propia aventura. Con la prueba gratuita es posible disfrutar de cientos de horas de juego y de una historia que equivale a cuatro títulos de FINAL FANTASY completos, pero sin límite de tiempo. Más información sobre la prueba gratuita: https://sqex.to/FFXIVFreeTrial
Por último, el presidente y CEO de SQUARE ENIX, Takashi Kiryu, ha cerrado el discurso inaugural anunciando la versión para Nintendo Switch™ 2 de FINAL FANTASY XIV. Esta versión estará disponible a partir de agosto de 2026. Está previsto un periodo de acceso anticipado para probar la estabilidad del servidor antes del lanzamiento oficial. Pronto los aventureros de más plataformas que nunca podrán unirse para disfrutar de este enorme mundo. Más información: https://sqex.to/GBs8O
El discurso inaugural (en inglés) puede verse en YouTube dentro de la transmisión en directo del primer día del Fan Festival 2026 en Anaheim: https://www.youtube.com/watch?v=_aCGzxWczCM
El FINAL FANTASY XIV Fan Festival 2026 continúa en Anaheim durante el sábado 25 de abril. Es posible seguir lo que ocurre sobre el escenario en directo desde Twitch y YouTube (primer día y segundo día). También están a la venta en FINAL FANTASY XIV Online Store nuevos objetos para celebrar el evento dentro del juego. Estos objetos conmemorativos estarán disponibles por un precio reducido hasta el lanzamiento del parche 7.5 el martes 28 de abril. Este año los conciertos de piano y de THE PRIMALS también están disponibles en forma de streaming de pago.
Londres, 17 de abril de 2026: SQUARE ENIX ha revelado hoy más información acerca del nuevo parche importante para FINAL FANTASY™ XIV Online durante el programa Letter from the Producer LIVE. Se ha vuelto a confirmar el 28 de abril como fecha de lanzamiento del parche 7.5, que llevará el título Trail to the Heavens.
El tráiler, presentado por el productor y director Naoki Yoshida, ha mostrado varias novedades y actualizaciones previstas para el parche, como nuevas misiones del escenario principal, la entrega final de la serie de asaltos aliados Echoes of Vana’diel, la nueva actualización de Occult Crescent, una nueva mazmorra, un nuevo asalto de dificultad Ultimate, un avance del nuevo oficio limitado Adiestrador y mucho más.
El tráiler del parche 7.5 puede verse aquí: https://youtu.be/nsMbfTpL63c
A continuación, se indican más detalles de los contenidos que incluirán los parches 7.5x:
Puedes ver una grabación del programa (audio solo en japonés) aquí: https://www.twitch.tv/videos/2750146881?t=0h39m35s
Por último, el director de sonido de FINAL FANTASY XIV Masayoshi Soken intervino durante el programa para dar más información acerca del reciente anuncio de que THE PRIMALS tocarán en el Download Festival del Reino Unido el viernes, 12 de junio de 2026. Refiriéndose al concierto y al propio festival, Soken expresó su emoción por poder tocar junto a algunos de los artistas más emblemáticos del mundo y dio información adicional acerca del concierto, como los artículos promocionales exclusivos que los fans podrán adquirir muy pronto. Para la última información acerca del Download Festival y THE PRIMALS, visita The Lodestone.
Con más de 30 millones de jugadores registrados, este es el momento ideal para adentrarse por primera vez en la saga de FINAL FANTASY XIV Online, que tan buenas críticas ha recibido. La prueba gratuita incluye todo el contenido de A Realm Reborn™ y las expansiones Heavensward™ y Stormblood™ (y las actualizaciones hasta el parche 4.58). Con la prueba gratuita es posible disfrutar de cientos de horas de juego y de una historia que equivalen a tres títulos de FINAL FANTASY completos, pero sin límite de tiempo. Más información: https://sqex.to/FFXIVFreeTrial
Sobre Square Enix Ltd.
Square Enix Ltd., como parte del grupo de empresas de Square Enix, desarrolla, publica, distribuye y licencia contenido de entretenimiento de las marcas SQUARE ENIX® y TAITO® en Europa y otros territorios PAL. El grupo de empresas de Square Enix cuenta con una valiosa cartera de propiedad intelectual que incluye: FINAL FANTASY®, que ha vendido más de 185 millones de unidades en todo el mundo; DRAGON QUEST®, que ha vendido más de 88 millones de unidades en todo el mundo; y el legendario SPACE INVADERS®. Square Enix Ltd. es una filial de propiedad absoluta de Square Enix Holdings Co., Ltd con sede en Londres.
Más información sobre Square Enix Ltd. en https://square-enix-games.com
La entrada ANUNCIO DEL PARCHE 7.5 DE FINAL FANTASY XIV DURANTE LA 92.ª LETTER FROM THE PRODUCER LIVE TRÁILER DE TRAIL TO THE HEAVENS Y MUCHO MÁS apareció primero en ZIRAN Comunicación.]]>
Londres, 13 de marzo de 2026: Hoy, durante la 91.º transmisión Letter from the Producer LIVE, SQUARE ENIX ha revelado el parche 7.5, titulado Trail to the Heavens. Se trata del próximo parche importante para FINAL FANTASY XIV Online y se lanzará el 28 de abril.
Durante la transmisión, el productor y director Naoki Yoshida ha repasado el calendario de lanzamientos para la serie de parches 7.5x presentando las diversas incorporaciones, entre las que se encuentran misiones de la historia principal, la entrega final de la serie de Alliance Raids, la próxima actualización de Occult Crescent, una nueva Dungeon, una nueva Raid de dificultad Ultimate, un primer vistazo al nuevo Limited Job Beastmaster y muchas cosas más.
La transmisión (solo con el audio en japonés) puede verse aquí: https://www.twitch.tv/videos/2721132350?t=0h36m48s
El parche 7.5 contiene un montón de novedades e incorporaciones, además de las sucesivas actualizaciones de la serie 7.5x:
El viernes 12 de junio de 2026, la banda oficial de FFXIV THE PRIMALS tocará en el Download Festival de Donington Park, en el Reino Unido. Tal como ha anunciado recientemente el director de sonido de FFXIV Masayoshi Soken en un mensaje de vídeo especial, THE PRIMALS brindarán su habitual energía, variedad musical y un espectáculo nunca visto en el festival. Para ver el mensaje y consultar más detalles sobre el festival y las entradas, visita The Lodestone: https://sqex.to/01GYo
Con más de 30 millones de jugadores registrados, este es el momento ideal para adentrarse por primera vez en la saga de FINAL FANTASY XIV Online, que tan buenas críticas ha recibido. La prueba gratuita incluye todo el contenido de A Realm Reborn™ y las expansiones Heavensward™ y Stormblood™ (y las actualizaciones hasta el parche 4.58). Con la prueba gratuita es posible disfrutar de cientos de horas de juego y de una historia que equivalen a tres títulos de FINAL FANTASY completos, pero sin límite de tiempo. Más información: https://sqex.to/FFXIVFreeTrial
Sobre Square Enix Ltd.
Square Enix Ltd., como parte del grupo de empresas de Square Enix, desarrolla, publica, distribuye y licencia contenido de entretenimiento de las marcas SQUARE ENIX® y TAITO® en Europa y otros territorios PAL. El grupo de empresas de Square Enix cuenta con una valiosa cartera de propiedad intelectual que incluye: FINAL FANTASY®, que ha vendido más de 185 millones de unidades en todo el mundo; DRAGON QUEST®, que ha vendido más de 88 millones de unidades en todo el mundo; y el legendario SPACE INVADERS®. Square Enix Ltd. es una filial de propiedad absoluta de Square Enix Holdings Co., Ltd con sede en Londres.
Más información sobre Square Enix Ltd. en https://square-enix-games.com
La banda llevará la música icónica del aclamado MMORPG a los legendarios escenarios del festival en Donington Park
LONDRES (23 de febrero de 2026) – Hoy, SQUARE ENIX ha anunciado que THE PRIMALS, la banda oficial de FINAL FANTASY XIV Online, actuará en Download Festival en Donington Park (Reino Unido) el viernes 12 de junio de 2026.
El anuncio, realizado junto a una amplia lista de otras bandas que participarán en el festival, se compartió a través de los canales oficiales de redes sociales de FFXIV, encabezado por un breve mensaje en vídeo presentado por el director de sonido de FFXIV, Masayoshi Soken. El vídeo puede verse aquí: https://youtu.be/K66TJpu9JCA
Desde su formación, THE PRIMALS han encabezado conciertos con entradas agotadas en todo Japón, incluyendo el Nippon Budokan y el Yokohama Arena, con entradas asignadas por sorteo que siguen teniendo una demanda extraordinaria, además de apariciones en eventos dedicados Final Fantasy XIV Fan Festival en Norteamérica, Europa, Corea del Sur y China. A continuación pueden verse dos vídeos cortos entre bambalinas de estos conciertos:
La actuación en Download Festival en 2026 marca la primera vez que la banda realiza un concierto independiente fuera de Asia (más allá de los esperadísimos eventos globales Fan Festival que se celebran por todo el mundo). Sumándose a la historia del rock y el metal junto a algunos de los artistas más icónicos del género, THE PRIMALS llevarán su energía característica, su variedad musical y un espectáculo como nunca antes se ha visto en el festival: ¡un evento que no te puedes perder! Para más información sobre el festival y las entradas, visita The Lodestone: https://sqex.to/01GYo
Download Festival es uno de los festivales de música rock y metal más importantes del mundo y, a lo largo de los años, ha acogido a algunas de las bandas más grandes del planeta. Con actuaciones históricas, tanto de cabezas de cartel como de bandas revelación, el festival sigue siendo un pilar del circuito de música en directo y un icono tanto para grupos nuevos como consagrados.
Ahora, en su 23.ª edición, del miércoles 10 de junio al domingo 14 de junio, Donington Park volverá a albergar los venerados escenarios del festival… y muy pronto, ¡también la música de FINAL FANTASY XIV! Para más información sobre el festival, incluyendo datos sobre el resto de bandas participantes, visita: Download Festival
Con más de 30 millones de jugadores registrados, este es el momento ideal para adentrarse por primera vez en la saga de FINAL FANTASY XIV Online, que tan buenas críticas ha recibido. La prueba gratuita incluye todo el contenido de A Realm Reborn™ y las expansiones Heavensward™ y Stormblood™ (y las actualizaciones hasta el parche 4.58). Con la prueba gratuita es posible disfrutar de cientos de horas de juego y de una historia que equivalen a tres títulos de FINAL FANTASY completos, pero sin límite de tiempo. Más información: https://sqex.to/FFXIVFreeTrial
Con más de 30 millones de jugadores registrados, este es el momento ideal para adentrarse por primera vez en la saga de FINAL FANTASY XIV Online, que tan buenas críticas ha recibido. La prueba gratuita incluye todo el contenido de A Realm Reborn™ y las expansiones Heavensward™ y Stormblood™ (y las actualizaciones hasta el parche 4.58). Con la prueba gratuita es posible disfrutar de cientos de horas de juego y de una historia que equivalen a tres títulos de FINAL FANTASY completos, pero sin límite de tiempo. Más información: https://sqex.to/FFXIVFreeTrial
Sobre Square Enix Ltd. Square Enix Ltd., como parte del grupo de empresas de Square Enix, desarrolla, publica, distribuye y licencia contenido de entretenimiento de las marcas SQUARE ENIX® y TAITO® en Europa y otros territorios PAL. El grupo de empresas de Square Enix cuenta con una valiosa cartera de propiedad intelectual que incluye: FINAL FANTASY®, que ha vendido más de 185 millones de unidades en todo el mundo; DRAGON QUEST®, que ha vendido más de 88 millones de unidades en todo el mundo; y el legendario SPACE INVADERS®. Square Enix Ltd. es una filial de propiedad absoluta de Square Enix Holdings Co., Ltd con sede en Londres. Más información sobre Square Enix Ltd. en https://square-enix-games.com
© SQUARE ENIX ILUSTRACIÓN DEL LOGOTIPO: © YOSHITAKA AMANO
Con más de 30 millones de jugadores registrados, este es el momento ideal para adentrarse por primera vez en la saga de FINAL FANTASY XIV Online, que tan buenas críticas ha recibido. La prueba gratuita incluye todo el contenido de A Realm Reborn™ y las expansiones Heavensward™ y Stormblood™ (y las actualizaciones hasta el parche 4.58). Con la prueba gratuita es posible disfrutar de cientos de horas de juego y de una historia que equivalen a tres títulos de FINAL FANTASY completos, pero sin límite de tiempo. Más información: https://sqex.to/FFXIVFreeTrial
Con más de 30 millones de jugadores registrados, este es el momento ideal para adentrarse por primera vez en la saga de FINAL FANTASY XIV Online, que tan buenas críticas ha recibido. La prueba gratuita incluye todo el contenido de A Realm Reborn™ y las expansiones Heavensward™ y Stormblood™ (y las actualizaciones hasta el parche 4.58). Con la prueba gratuita es posible disfrutar de cientos de horas de juego y de una historia que equivalen a tres títulos de FINAL FANTASY completos, pero sin límite de tiempo. Más información: https://sqex.to/FFXIVFreeTrial
Sobre Square Enix Ltd. Square Enix Ltd., como parte del grupo de empresas de Square Enix, desarrolla, publica, distribuye y licencia contenido de entretenimiento de las marcas SQUARE ENIX® y TAITO® en Europa y otros territorios PAL. El grupo de empresas de Square Enix cuenta con una valiosa cartera de propiedad intelectual que incluye: FINAL FANTASY®, que ha vendido más de 185 millones de unidades en todo el mundo; DRAGON QUEST®, que ha vendido más de 88 millones de unidades en todo el mundo; y el legendario SPACE INVADERS®. Square Enix Ltd. es una filial de propiedad absoluta de Square Enix Holdings Co., Ltd con sede en Londres. Más información sobre Square Enix Ltd. en https://square-enix-games.com
© SQUARE ENIX ILUSTRACIÓN DEL LOGOTIPO: © YOSHITAKA AMANO