/** * 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; } } Ankama | ZIRAN Comunicación https://www.ziran.es Comunicación Estratégica Mon, 24 Feb 2025 16:29:01 +0000 es hourly 1 https://wordpress.org/?v=7.1 https://www.ziran.es/wp-content/uploads/2020/07/favicon.svg Ankama | ZIRAN Comunicación https://www.ziran.es 32 32 LA PRIMERA DEMO DE MALIKI: POISON OF THE PAST YA ESTÁ DISPONIBLE EN STEAM NEXT FEST https://www.ziran.es/nota_prensa/la-primera-demo-de-maliki-poison-of-the-past-ya-esta-disponible-en-steam-next-fest/ Mon, 24 Feb 2025 16:29:01 +0000 https://www.ziran.es/?post_type=nota_prensa&p=56854   Roubaix y Auray, 22 de noviembre de 2024 – Ankama y Blue Banshee han compartido un nuevo trailer en Mix Showcase mostrando nuevos detalles de la jugabilidad y la fecha de lanzamiento de su próximo videojuego, Maliki: Poison of the Past, que estará disponible a partir del 11 de marzo de 2025 para PC […]

La entrada LA PRIMERA DEMO DE MALIKI: POISON OF THE PAST YA ESTÁ DISPONIBLE EN STEAM NEXT FEST apareció primero en ZIRAN Comunicación.]]>
 

Roubaix y Auray, 22 de noviembre de 2024 – Ankama y Blue Banshee han compartido un nuevo trailer en Mix Showcase mostrando nuevos detalles de la jugabilidad y la fecha de lanzamiento de su próximo videojuego, Maliki: Poison of the Past, que estará disponible a partir del 11 de marzo de 2025 para PC y Nintendo Switch.
El nuevo tráiler ofrece a los jugadores un vistazo a las características principales del juego, entre las que se incluye un innovador sistema de manipulación del tiempo en los combates por turnos, un mundo fantástico repleto de aventuras por descubrir, cautivadores puzles por resolver y la gestión del Dominio.
Los jugadores interesados en Maliki: Poison of the Past ya pueden añadirlo a su lista de deseos de Steam visitando la página del juego en Steam. De esta manera, serán notificados cuando la demo esté disponible, próximamente.
Inspirada en el cómic homónimo, Maliki: Poison of the Past es una aventura RPG para un jugador que se basa en el combate por turnos y la exploración. Maliki: Poison of the Past es la culminación de los 20 años de existencia del cómic Maliki que, con sus 8 volúmenes, ha vendido más de 300.000 copias en todo el mundo. El juego está siendo desarrollado por Blue Banshee en colaboración con Souillon, el autor y creador de Maliki.
Para saber más acerca de Maliki: Poison of the Past, los usuarios interesados pueden visitar la página del juego en Steam, donde encontrarán un vídeo de los desarrolladores. Los amantes de los juegos independientes pueden ver la repetición completa del MIX Showcase en el canal del evento en Twitch.
 
Acerca de Ankama:
Ankama es una empresa francesa independiente dedicada a la creación, publicación y distribución de obras de entretenimiento en todo el mundo. Desde el éxito de su juego en línea DOFUS en 2004 (con más de 100 millones de cuentas creadas en todo el mundo), Ankama ha invertido en varios ámbitos de actividad para convertirse en un verdadero grupo transmedia. Desde los videojuegos a los juegos de mesa, del manga al anime, Ankama controla toda la cadena de producción de sus creaciones. Creador de un vasto universo llamado «Krosmoz», conocido por sus juegos de rol multijugador en línea DOFUS y WAKFU y su serie de animación homónima, también produce largometrajes de animación como DOFUS – Libro I:  Julith o Dragon Princess. Impulsada por la creatividad, Ankama también diseña y desarrolla juegos de mesa y figuritas como Krosmaster Arena, Monster Slaughter y Arkeis; juegos para móviles como Tactile Wars, King Tongue, Super Nano Blaster o DOFUS Touch; así como cómics y manga como Radiant, Shangri-La, Carbon & Silicon, In the Mind of Sherlock Holmes, DOFUS o Mutafukaz. Siguiendo los principios del transmedia, los equipos de Ankama crean universos que desarrollan en múltiples historias, a través de diferentes medios, ofreciendo a sus comunidades leales de jugadores, lectores y espectadores una experiencia narrativa inmersiva y completa en todas las plataformas populares. Con Ankama, cada historia es única, pero todas están conectadas. En 2020, Ankama batió el récord europeo de crowdfunding para una serie de animación en Kickstarter, recaudando más de 1,5 millones de euros para producir la cuarta temporada de WAKFU. En 2023, la compañía volvió a demostrar su poder creativo con el lanzamiento de dos series animadas, The Last Adventure of Count Lancedur en ADN, y su nuevo gran juego en el universo Krosmoz: WAVEN en acceso anticipado para PC. La cuarta temporada de la serie WAKFU se estrenó en febrero de 2024 con índices de audiencia para una serie animada en France Télévisions.
Acerca de Blue Banshee:
Blue Banshee es un estudio francés independiente, nacido del encuentro entre un autor de cómics, un diseñador de juegos y un pionero de la animación francesa. Su ambición es crear juegos independientes en universos fantásticos y caprichosos, con la cantidad justa de poesía y jugabilidad para atraer tanto al cerebro como al controlador. La Banshee, una deidad femenina celta, a veces un monstruo carismático de los juegos de rol, a veces una entidad protectora de la familia, es un símbolo que corresponde perfectamente a sus valores, ¡y que nunca pasa desapercibido! Su primer juego, Maliki – Poison of the Past, se lanzará a inicios de 2025. Proviene del universo del famoso webtoon Maliki, lanzado en 2004 por Souillon, vendido en más de 300.000 copias en diversos medios y nunca antes adaptado a un videojuego.
La entrada LA PRIMERA DEMO DE MALIKI: POISON OF THE PAST YA ESTÁ DISPONIBLE EN STEAM NEXT FEST apareció primero en ZIRAN Comunicación.]]>
ANKAMA y BLUE BANSHEE anuncian MALIKI: POISON OF THE PAST https://www.ziran.es/nota_prensa/ankama-y-blue-banshee-anuncian-maliki-poison-of-the-past/ Thu, 21 Mar 2024 16:48:32 +0000 https://www.ziran.es/?post_type=nota_prensa&p=54716 La colorida heroína francesa de pelo rosa, Maliki, se prepara para su debut en los videojuegos en una nueva aventura para PC y Switch Visualiza el tráiler aquí Descarga las imágenes aquí París, Francia, 21 de marzo de 2024 – Ankama, editora de los MMO Wakfu y Dofus, se enorgullece de anunciar con la desarrolladora […]

La entrada ANKAMA y BLUE BANSHEE anuncian MALIKI: POISON OF THE PAST apareció primero en ZIRAN Comunicación.]]>
La colorida heroína francesa de pelo rosa, Maliki, se prepara para su debut en los videojuegos en una nueva aventura para PC y Switch
Visualiza el tráiler aquí
Descarga las imágenes aquí
París, Francia, 21 de marzo de 2024 – Ankama, editora de los MMO Wakfu y Dofus, se enorgullece de anunciar con la desarrolladora Blue Banshee el próximo lanzamiento del videojuego de aventuras RPG Maliki: Poison of the Past para PC y Nintendo Switch. Inspirado en el cómic del mismo nombre, Maliki: Poison of the Past es un juego de rol de aventuras para un solo jugador centrado en la exploración y los combates por turnos, disponible para reservar a partir de hoy en la plataforma independiente Kunekune.fr, que llegará completamente localizado al castellano.
Maliki: Poison of the Past es la culminación de 20 años de existencia del cómic Maliki que, con sus 8 volúmenes, ha vendido más de 300.000 copias en todo el mundo. El juego está siendo desarrollado por el estudio Blue Banshee, co-creado por Souillon, el autor y creador de Maliki, quien añade:
«Maliki siempre ha sido un universo transmedia, con el cómic, la novela, la música y la animación. Explorarlo como un videojuego era algo que debería haberse hecho hace mucho tiempo, solo necesitaba que las estrellas se alinearan (las empujamos un poco…)».
Los jugadores están invitados a unirse a Maliki y su excéntrica familia en su épica batalla contra Poison, una formidable criatura vegetal que ha colonizado el espacio-tiempo. Se embarcarán en una fantástica odisea a través de los tiempos, ofreciendo una combinación única de exploración, acertijos temporales y combates por turnos llenos de sorpresas.
Características principales:
▫ Una narrativa peculiar y atractiva – En un mundo donde una humanidad derrotada se enfrenta a fuerzas naturales invasoras y vengativas, un pequeño grupo de coloridos aventureros intenta restaurar el equilibrio del mundo y salvar su hogar. Pero, ¿quién es Poison? ¿Qué esconde Maliki? ¿Podemos acariciar a los gatos?
▫ Un nuevo tipo de batallas: utiliza una mecánica única en el combate por turnos: ¡la manipulación temporal! ¡Altera el curso del tiempo para crear poderosos combos entre aliados, traslada a tus oponentes al pasado y encadena tus ataques deslizándolos hacia el futuro!
▫ Un refugio fuera del tiempo y el caos – Entre exploraciones temporales, el Dominio es el lugar ideal para recargar las pilas. Desarrolla tu remanso de paz: planta, cosecha, cocina, juega y nutre el Árbol de las Mil Raíces que detiene el tiempo.
▫ Un estilo gráfico único: Maliki: Poison of the Past, que reúne el talento de renombrados artistas de cómics y creadores de juegos experimentados, ofrece gráficos dinámicos y vibrantes que abrazan la fusión de la cultura visual francesa y el chibi japonés.
▫ 20 años de historias fabulosas – Un viaje nostálgico para muchos lectores, el cómic de Maliki hace su primera incursión en el mundo de los videojuegos y ofrece un nuevo punto de entrada para descubrir fácilmente su universo.
Kunekune es una nueva plataforma de pedidos anticipados fundada por destacados creadores de contenido con la ambición de apoyar a los creadores de proyectos sin ninguna especulación. Por esa razón, el equipo de Maliki ha decidido asociarse con ésta. Los fanáticos pueden visitar la página dedicada en el sitio a partir de hoy. El equipo del juego desvelará muchas sorpresas exclusivas a lo largo de la campaña.
La página de Steam Maliki: Poison of the Past también está activa, y los jugadores de todo el mundo ahora pueden mantenerse informados agregando el juego a su lista de deseos.
Descubre el tráiler aquí y descarga las imágenes aquí.
Acerca de Ankama:
Ankama es una empresa francesa independiente dedicada a la creación, publicación y distribución de obras de entretenimiento en todo el mundo. Desde el éxito de su juego en línea DOFUS en 2004 (con más de 100 millones de cuentas creadas en todo el mundo), Ankama ha invertido en varios ámbitos de actividad para convertirse en un verdadero grupo trasmedia. Desde los videojuegos hasta los juegos de mesa, desde el manga hasta el anime, controla toda la cadena de producción de sus creaciones, de la A a la Z. Creadora de un vasto universo llamado «Krosmoz», conocido por sus juegos de rol multijugador en línea DOFUS y WAKFU y su serie de animación homónima, también produce largometrajes de animación como DOFUS – Libro I:  Julith o Princess Dragon. Impulsada por la creatividad, Ankama también diseña y desarrolla juegos de mesa y figuritas como Krosmaster Arena, Monster Slaughter y Arkeis; juegos para móviles como Tactile Wars, King Tongue, Super Nano Blaster o DOFUS Touch; así como cómics y mangas como Radiant, Shangri-La, Carbon & Silicon, In the Mind of Sherlock Holmes, DOFUS o Mutafukaz. Siguiendo los principios del transmedia, los equipos de Ankama crean universos que desarrollan en múltiples historias, a través de diferentes medios, ofreciendo a sus fieles comunidades de jugadores, lectores y espectadores una experiencia narrativa inmersiva y completa en todas las plataformas populares. Con Ankama, cada historia es única, pero todas están conectadas. En 2020, Ankama batió el récord europeo de crowdfunding para una serie de animación en Kickstarter, recaudando más de 1,5 millones de euros para producir la cuarta temporada de WAKFU. En 2023, la compañía volvió a demostrar su poder creativo con el lanzamiento de dos series animadas, The Last Adventure of Count Lancedur en ADN, y su nuevo gran juego en el universo Krosmoz, WAVEN, en early access para PC. La cuarta temporada de la serie WAKFU se estrenó en febrero de 2024 con índices de audiencia para una serie animada en France Télévisions.
La entrada ANKAMA y BLUE BANSHEE anuncian MALIKI: POISON OF THE PAST apareció primero en ZIRAN Comunicación.]]>
¡La nueva temporada de los susurradores ya está disponible en WAVEN! https://www.ziran.es/nota_prensa/la-nueva-temporada-de-los-susurradores-ya-esta-disponible-en-waven/ Tue, 16 Jan 2024 10:20:57 +0000 https://www.ziran.es/?post_type=nota_prensa&p=54331 Naufrago, ¡prepárate para un un jaleo infernal! La horda de los susurradores toma el control de WAVEN por la nueva temporada de los susurradores y hará mucho ruido a partir de este martes 16 de enero de 2024. Hasta el 13 de febrero, los susurradores invaden WAVEN y están acompañados del héroe Astramantis, ahora disponible […]

La entrada ¡La nueva temporada de los susurradores ya está disponible en WAVEN! apareció primero en ZIRAN Comunicación.]]>
Naufrago, ¡prepárate para un un jaleo infernal! La horda de los susurradores toma el control de WAVEN por la nueva temporada de los susurradores y hará mucho ruido a partir de este martes 16 de enero de 2024.
Hasta el 13 de febrero, los susurradores invaden WAVEN y están acompañados del héroe Astramantis, ahora disponible gratuitamente. El astramantis, que presentamos en primicia en la temporada de Albuera, ya está disponible para todos los jugadores de WAVEN en el menú de creación de personaje.
Ponte en la piel de este poderoso lancero YOPUKA y utiliza bien su pasivo Salto Mantis para meterte de cabeza en la batalla.
Y para celebrar que este nuevo héroe está accesible a un público más amplio, un skin definitivo, fruto de una colaboración con Tony Valente, el autor del manga RADIANT, ¡ya está disponible en la tienda en el juego a cambio de gemas!
La nueva temporada también introduce un nuevo portal temporal que te permite encarnar a la horda de los susurradores. No te fíes de las apariencias. Los susurradores son unos diminutos seres misteriosos en armaduras que actúan en grupo y son capaces de sorprender a más de uno.
Acude al portal temporal y trata de repeler los ataques de la guardia de Bonta. Utiliza la mecánica «Refuerzo» para invocar a tus tropas y llegar hasta lo más alto de la clasificación de temporada.
Por último, esta temporada, los jugadores encontrarán nuevos Gold y Platinum Pass llenos de objetos relacionados con la horda de los susurradores:
Una mascota susurrador que te seguirá fielmente en tus aventuras.
·         Un lote de efectos visuales para presumir en combate y en el mundo.
·         Emotes chulísimos de los susurradores.
·         Tickets de temporada para contar con más oportunidades de ir a combatir a los bontarianos.
Y un montón de gemas con las que podrás conseguir más skins, efectos visuales o emotes de la tienda de WAVEN del juego.
No olvides nunca que el tamaño no importa… ¡Para muestra, los susurradores! Que redoblen los tambores y suenen las trompetas, ¡porque ya puedes entrar en el juego y disfrutar de esta nueva temporada.
La entrada ¡La nueva temporada de los susurradores ya está disponible en WAVEN! apareció primero en ZIRAN Comunicación.]]>
Una oleada de nuevos contenidos en WAVEN para enriquecer la experiencia de juego https://www.ziran.es/nota_prensa/una-oleada-de-nuevos-contenidos-en-waven-para-enriquecer-la-experiencia-de-juego/ Tue, 19 Dec 2023 09:23:50 +0000 https://www.ziran.es/?post_type=nota_prensa&p=54217 Llega el último capítulo de Lance Dur y trae consigo una impresionante cantidad de contenido nuevo. En diciembre llegará una actualización con el prólogo de la misión principal, un nuevo héroe, el capítulo final de la temporada de Lance Dur, dos nuevas naciones insulares y mucho más… ¡Preparaos, intrépidos exploradores del Mundo de los Doce! […]

La entrada Una oleada de nuevos contenidos en WAVEN para enriquecer la experiencia de juego apareció primero en ZIRAN Comunicación.]]>
Llega el último capítulo de Lance Dur y trae consigo una impresionante cantidad de contenido nuevo. En diciembre llegará una actualización con el prólogo de la misión principal, un nuevo héroe, el capítulo final de la temporada de Lance Dur, dos nuevas naciones insulares y mucho más…

¡Preparaos, intrépidos exploradores del Mundo de los Doce! Desde su primera travesía en Early Access en PC y Mac el pasado agosto, vuestra inagotable pasión ha empujado a WAVEN hacia aguas inexploradas. Con más de 500.000 almas a bordo que se han unido a nuestra búsqueda, estamos encantados de anunciar que una oleada de actualizaciones llegará a las costas del Mundo de los Doce en diciembre.

Esto es lo que viene:

  • El prólogo de la misión principal: Descubre los misterios de WAVEN mientras revelamos el épico comienzo de la historia principal. Sumérgete en una historia cautivadora que te encantará y te desafiará.
  • Un nuevo héroe: ¡Un personaje legendario se alza! Reverencia al dios Iop gracias a los Astramantises y su nuevo modo de juego que añade habilidades únicas y una dinámica fresca en el campo de batalla.
  • Último capítulo de la temporada de Lance Dur: El gran final de la temporada de Lance Dur te está esperando y promete batallas épicas, una gran cantidad de recompensas y una conclusión que te mantendrá cautivado hasta el final.

Nuevas naciones insulares: Explora dos nuevas naciones insulares: Amakna y Albuera, cada una con sus propios desafíos, habitantes pintorescos y secretos. Albuera se desvelará durante la temporada, añadiendo una capa extra de emoción al final de temporada de Lance Dur. La zona favorita del famoso conde, estará dedicada al combate de alto nivel para los jugadores más experimentados.

  • Chat abierto: Conecta con tus compañeros de naufragio más fácilmente con la introducción de una función de chat abierto. Comparte tus estrategias, coordina tus aventuras o simplemente charla amistosamente mientras luchas.
  • Llega la banda sonora original: Prepárate para sumergirte más profundamente en el universo WAVEN con el próximo lanzamiento de la banda sonora del juego en tres volúmenes (Conciertos, Vol. 1 y Vol. 2), ¡disponible en todas las plataformas de streaming el 22 de diciembre!

Más allá del horizonte

Mientras la aventura en el Mundo de los Doce se intensifica, ¡el equipo de Ankama Games tiene aún más reservado para ti! Estamos trabajando diligentemente en una versión para móviles, ¡asegurándonos de que la experiencia WAVEN pueda disfrutarse en cualquier momento y lugar!

Sin embargo, para hacer de WAVEN la odisea definitiva, hemos decidido, en colaboración con nuestros socios coeditores de New Tales, ajustar nuestras velas y retrasar el lanzamiento de la versión 1.0 en PC y móvil. Pero no temas, porque 2024 traerá contenidos apasionantes que harán que tus viajes por el Mundo de los Doce sean realmente legendarios.

WAVEN está disponible en acceso anticipado en Ankama Launcher y Steam.

Los jugadores pueden mantenerse al día de las novedades de WAVEN suscribiéndose al boletín y siguiendo las cuentas del juego en Facebook, Twitter, Instagram y TikTok.

 

 

La entrada Una oleada de nuevos contenidos en WAVEN para enriquecer la experiencia de juego apareció primero en ZIRAN Comunicación.]]>
WAVEN – Nueva temporada de Lance-Dur – Capítulo 2: Belladonna https://www.ziran.es/nota_prensa/waven-nueva-temporada-de-lance-dur-capitulo-2-belladonna/ Tue, 21 Nov 2023 09:11:13 +0000 https://www.ziran.es/?post_type=nota_prensa&p=54104 ¡Un nuevo capítulo de la temporada de Lance-Dur! ¡Llega la aterradora Belladonna y sus zarzas serán tu sepultura! La lucha de Belladonna en esta temporada ha ocupado el lugar de Cire Momore, y los jugadores pueden encontrarse con ella todos los días en el portal del tiempo de la isla competitiva. Con su entrada diaria, […]

La entrada WAVEN – Nueva temporada de Lance-Dur – Capítulo 2: Belladonna apareció primero en ZIRAN Comunicación.]]>
¡Un nuevo capítulo de la temporada de Lance-Dur! ¡Llega la aterradora Belladonna y sus zarzas serán tu sepultura!

La lucha de Belladonna en esta temporada ha ocupado el lugar de Cire Momore, y los jugadores pueden encontrarse con ella todos los días en el portal del tiempo de la isla competitiva. Con su entrada diaria, podrán probar suerte en el desafío hasta el 19 de diciembre.

La clasificación se resetea en esta nueva temporada, pero sigue recompensando a los mejores jugadores con cofres, fragmentos de Wakfu y gemas para comprar objetos cosméticos en la tienda del juego de Waven. Todos los participantes recibirán 500 Fragmentos de Wakfu para canjearlos por cofres de Albuera, que les permitirán añadir anillos, brazaletes, hechizos y compañeros a sus mazos.

Los desafíos del Pase de Batalla están disponibles cada semana y los jugadores pueden ganar entre 10 y 50 estrellas. A medida que se acumulen estas estrellas podrán desbloquear progresivamente las recompensas de este capítulo. A lo largo del capítulo Belladonna se pueden conseguir emotes, mascotas, efectos visuales y aspectos de acompañante.

Cada Pase de Batalla tiene sus propias recompensas, pero el Platinum Pass las desbloquea todas. Puedes conseguir todas las recompensas en cualquier momento, siempre que tengas suficientes estrellas.

Todas estas recompensas solo se pueden desbloquear en el Pase de Combate mientras dure el capítulo 2: Belladona. Si te olvidas, las recompensas esperarán pacientemente en un buzón, dentro del juego, cuando se lance el siguiente capítulo. Los pases de temporada, por su parte, solo pueden utilizarse para el capítulo actual.

A la venenosa Belladonna le seguirá el último capítulo de la temporada de Lance-Dur: llamado Albuera, durará hasta mediados de enero y ofrecerá nuevas batallas de temporada, desafíos, recompensas y mucho más.

 

La entrada WAVEN – Nueva temporada de Lance-Dur – Capítulo 2: Belladonna apareció primero en ZIRAN Comunicación.]]>
WAVEN – Nueva temporada de Lance Dur – Capítulo 1: Cire Momore https://www.ziran.es/nota_prensa/waven-nueva-temporada-de-lance-dur-capitulo-1-cire-momore/ Tue, 24 Oct 2023 10:00:32 +0000 https://www.ziran.es/?post_type=nota_prensa&p=53933 ¡Un nuevo modo de juego, más contenido y desafíos originales para esta nueva temporada que promete ser espeluznante! Llega a WAVEN un modo de juego PvE competitivo. En los combates de temporada, los jugadores llegan con su ticket diario al portal del tiempo de la isla competitiva, para unirse a la batalla como una leyenda […]

La entrada WAVEN – Nueva temporada de Lance Dur – Capítulo 1: Cire Momore apareció primero en ZIRAN Comunicación.]]>
¡Un nuevo modo de juego, más contenido y desafíos originales para esta nueva temporada que promete ser espeluznante!

Llega a WAVEN un modo de juego PvE competitivo. En los combates de temporada, los jugadores llegan con su ticket diario al portal del tiempo de la isla competitiva, para unirse a la batalla como una leyenda del Krosmoz – el conde Lance Dur – y enfrentarse al tenebroso Cire Momore. Como este temible guerrero, los jugadores descubrirán todos sus hechizos, ataques y habilidades pasivas a medida que vayan escalando posiciones en las tablas de clasificación.

Esta nueva función de clasificación recompensa a los 100 mejores jugadores con cofres, fragmentos de Wakfu y gemas para comprar cosméticos en la tienda del juego en WAVEN. Todos los participantes recibirán 500 fragmentos de Wakfu para gastar en cofres de Albuera, que sirven para añadir anillos, brazaletes, hechizos y compañeros a su mazo.

El pase de combate se renueva para WAVEN e introduce una nueva mecánica de progreso. Cada semana habrá desafíos y los jugadores podrán ganar entre 10 y 50 estrellas. A medida que ganen más, irán desbloqueando recompensas del pase de combate. Esta temporada también introduce figuras importantes, incluidos los compañeros de viaje del famoso conde.
Junto con las mascotas, efectos visuales, emotes y otras recompensas habituales, este nuevo pase también puede aumentar el número de veces que puedes luchar contra Cire Momore y su ejército de Deathburns gracias a los tickets extra.

El primer capítulo de esta temporada de Lance Dur ya está sacudiendo el Mundo de los Doce, y la historia continuará a lo largo de dos capítulos extra centrados en la venenosa Belladona y después en la nación de Albuera.

Hasta mediados de enero, cada capítulo ofrecerá nuevos desafíos, recompensas y combates de temporada.

 

La entrada WAVEN – Nueva temporada de Lance Dur – Capítulo 1: Cire Momore apareció primero en ZIRAN Comunicación.]]>
El Acceso Anticipado De Waven, Ya Disponible Y Gratis En Pc Y Mac https://www.ziran.es/nota_prensa/el-acceso-anticipado-de-waven-ya-disponible-y-gratis-en-pc-y-mac/ Wed, 16 Aug 2023 15:31:11 +0000 https://www.ziran.es/?post_type=nota_prensa&p=53240 La ola de WAVEN llega este 16 de agosto de 2023 a las 17:00 CEST. Un nuevo tráiler del juego ofrece una visión general de lo que espera a los jugadores durante el acceso anticipado. Los aventureros de los mares podrán navegar con elementos cosméticos de categoría gracias a los packs fundadores y a los […]

La entrada El Acceso Anticipado De Waven, Ya Disponible Y Gratis En Pc Y Mac apareció primero en ZIRAN Comunicación.]]>
La ola de WAVEN llega este 16 de agosto de 2023 a las 17:00 CEST.

Un nuevo tráiler del juego ofrece una visión general de lo que espera a los jugadores durante el acceso anticipado.

Los aventureros de los mares podrán navegar con elementos cosméticos de categoría gracias a los packs fundadores y a los primerísimos Gold y Platinum Pass.

Paris y Roubaix, 16 de agosto de 2023 – Soltad amarras, levad anclas e izad la vela mayor: ¡el acceso anticipado de WAVEN empieza hoy!

El nuevo juego tan esperado de la empresa de Roubaix (Lille, Francia) por fin estará disponible el 16 de agosto de 2023 en un acceso anticipado totalmente gratis para Windows PC y Mac. Se puede acceder a WAVEN desde el Ankama Launcher, pero también desde Steam. El juego estará disponible más adelante, este año, en móviles y tabletas iOS y Android, con su lanzamiento definitivo, que ofrecerá una experiencia crossplay.

Para celebrar el inicio del acceso anticipado, se ha desvelado un nuevo tráiler gameplay donde se da una visión general de lo que espera a los exploradores una vez lanzado el juego:

Waven | PC Early Access Launch Trailer | Ankama Games

En un mundo sumergido en el que solo han sobrevivido unos cuantos islotes, encarnarás a un aventurero que surcará los mares en busca de respuestas a esta gran transformación. Antaño, dioses y dragones dominaban sobre todos. Hoy, la magia pervive, pero ¿qué ha pasado con las entidades mitológicas de la antigüedad? Entra en la gran crónica de WAVEN para forjar tu destino: elige tu personaje, equipa tus mejores hechizos y navega de isla en isla para conquistar nuevos honores y fortunas. Haz que tu héroe y tu estrategia vayan avanzando en este RPG táctico multijugador gratuito y con unos gráficos expresivos y únicos.

El acceso anticipado comenzará con un corto prólogo narrativo antes de sumergirse de lleno en la aventura WAVEN con sus más de 250 misiones por hacer, 17 destinos que explorar, 25 héroes que descubrir, 90 compañeros que coleccionar, 300 hechizos que equipar y muchos más misterios por descubrir. Los jugadores también podrán enfrentarse en JcJ en arena o combatir codo con codo con amigos.

Algunas funcionalidades del juego, como la misión principal o la defensa de la isla, estarán disponibles más tarde, cuando el buque insignia esté listo para zarpar a finales de 2023.

Si quieres más información sobre el contenido del acceso anticipado, visita: https://forum.waven-game.com/en/47-noticias/2188-waven-acceso-anticipado-rumbo-16-agosto

Tras varias alfas abiertas y, más recientemente, una versión demo que permitieron recibir valiosos comentarios de la comunidad, el acceso anticipado de WAVEN representa la última etapa importante con vistas al lanzamiento definitivo del juego a finales de 2023, y permitirá al equipo de WAVEN testear los últimos sistemas de juego.

«Este acceso anticipado es un momento fundamental para todo el equipo, y una rampa de lanzamiento para la salida definitiva del juego a finales de año», declaró Bastien Guers, game producer de Ankama Games. «Estamos impacientes por recibir los comentarios y las opiniones de los jugadores, así podremos perfeccionar al máximo la experiencia de WAVEN y convertirlo en una aventurarealmente única para la comunidad».

El lanzamiento de este acceso anticipado viene con tres packs fundadores, que se podrán comprar con el lanzamiento del juego. Con estos packs, los jugadores podrán desbloquear cosméticos exclusivos, apoyando así a los creadores y desarrolladores del juego.

Por último, los jugadores podrán conseguir más elementos cosméticos para personalizar su progresión gracias a un pase de temporada gratuito y dos pases de pago (Gold y Platinum), que les darán recompensas adicionales al superar retos en el juego.

El acceso anticipado también ofrece a todos los participantes un cosmético FX especial, un efecto visual tipo aura. Este goodie de bienvenida estará disponible desde el acceso anticipado hasta la salida oficial del juego WAVEN.

El acceso anticipado de WAVEN empieza el 16 de agosto para Windows PC y Mac. La versión definitiva del juego saldrá a finales de 2023 para Windows PC, Mac, tabletas y móviles iOS y Android. WAVEN está desarrollado por Ankama Games y coeditado por Ankama Games y New Tales.

Para estar informados de la actualidad de WAVEN, los jugadores pueden inscribirse a la newsletter y seguir las cuentas de Facebook, Twitter, Instagram y TikTok del juego.

 

 

 

La entrada El Acceso Anticipado De Waven, Ya Disponible Y Gratis En Pc Y Mac apareció primero en ZIRAN Comunicación.]]>
Anunciada una serie de vídeos documentales de WAVEN; ya disponible el episodio 1 centrado en el lore https://www.ziran.es/nota_prensa/anunciada-una-serie-de-videos-documentales-de-waven-ya-disponible-el-episodio-1-centrado-en-el-lore/ Wed, 09 Aug 2023 19:38:50 +0000 https://www.ziran.es/?post_type=nota_prensa&p=53109 Ankama ha anunciado hoy el lanzamiento de una serie de vídeos destinados a ofrecer más información sobre diferentes aspectos de WAVEN, el nuevo RPG táctico que ya prepara su lanzamiento en acceso anticipado. Para empezar nuestra serie de vídeos documentales, vamos a echar un vistazo a la historia, el trasfondo y la narrativa de WAVEN. […]

La entrada Anunciada una serie de vídeos documentales de WAVEN; ya disponible el episodio 1 centrado en el lore apareció primero en ZIRAN Comunicación.]]>
Ankama ha anunciado hoy el lanzamiento de una serie de vídeos destinados a ofrecer más información sobre diferentes aspectos de WAVEN, el nuevo RPG táctico que ya prepara su lanzamiento en acceso anticipado.

Para empezar nuestra serie de vídeos documentales, vamos a echar un vistazo a la historia, el trasfondo y la narrativa de WAVEN. En primer lugar, Aurore explica los diversos aspectos que tuvo que considerar para construir la narrativa de este mundo postapocalíptico.

A continuación, la diseñadora narrativa del equipo de WAVEN en Ankama explica cómo se reflejan estas cuestiones en el juego a través de los personajes, el trasfondo, las historias, la jugabilidad y la atmósfera de las distintas ciudades, para que los jugadores puedan sentir realmente lo que significa sobrevivir en un mundo que ha sido totalmente devastado.

Pero no te preocupes: ¡toda la aventura y el humor que esperas de Ankama siguen ahí!

Puedes ver el vídeo al completo en este enlace: https://www.youtube.com/watch?v=55-Xbb2bfN0&ab_channel=WAVEN

El acceso anticipado para PC comenzará con un breve prólogo narrativo y luego se sumergirá rápidamente en la aventura de WAVEN con más de 250 misiones que completar, 17 destinos que visitar, 25 héroes que descubrir, 90 compañeros que coleccionar, 300 hechizos que equipar y muchos, muchos más misterios que descubrir.

Algunas características del juego, como la misión principal y la defensa de la isla, estarán disponibles más adelante, una vez que el buque insignia esté listo para zarpar a finales de 2023.

Para más detalles sobre lo que contiene el acceso anticipado, dirígete a: https://forum.waven-game.com/en/47-noticias/2188-waven-acceso-anticipado-rumbo-16-agosto

El acceso anticipado de WAVEN en PC y Mac comenzará este 16 de agosto, y el lanzamiento oficial está previsto para finales de 2023 en PC y Mac, así como en móviles y tabletas iOS y Android. El juego está siendo desarrollado por Ankama Games y coeditado por Ankama y New Tales.

 

 

La entrada Anunciada una serie de vídeos documentales de WAVEN; ya disponible el episodio 1 centrado en el lore apareció primero en ZIRAN Comunicación.]]>
Acceso anticipado gratuito en PC y Mac para WAVEN: ¡Izad las velas el 16 de agosto! https://www.ziran.es/nota_prensa/acceso-anticipado-gratuito-en-pc-y-mac-para-waven-izad-las-velas-el-16-de-agosto/ Wed, 19 Jul 2023 15:16:47 +0000 https://www.ziran.es/?post_type=nota_prensa&p=52886 ¡Hola, grumete! Prepárate para levar anclas el 16 de agosto, cuando comience la fase de acceso anticipado gratuito al juego. Embárcate en una aventura en un mundo inundado en el que sólo unas pocas islas han sobrevivido milagrosamente. Encarna a un aventurero marinero en busca de respuestas tras la gran convulsión. Hace mucho tiempo, dioses […]

La entrada Acceso anticipado gratuito en PC y Mac para WAVEN: ¡Izad las velas el 16 de agosto! apareció primero en ZIRAN Comunicación.]]>
¡Hola, grumete! Prepárate para levar anclas el 16 de agosto, cuando comience la fase de acceso anticipado gratuito al juego.

Embárcate en una aventura en un mundo inundado en el que sólo unas pocas islas han sobrevivido milagrosamente. Encarna a un aventurero marinero en busca de respuestas tras la gran convulsión. Hace mucho tiempo, dioses y dragones gobernaban el mundo. Hoy, la magia permanece… ¿pero qué hay de los seres míticos de antaño?

En los albores de una nueva era elige a tu héroe, equipa tus mejores hechizos y zarpa hacia una odisea increíble. Navega de isla en isla por un vasto mundo lleno de coloridas criaturas, gana poder, perfecciona tu estrategia y descubre los secretos de un mundo a la deriva.

Los valientes aventureros pueden partir equipados hacia alta mar con tres Packs de Fundador que estarán disponibles para su compra a partir del 16 de agosto:

El pack fundador estándar contiene:
– 3.000 gemas
– 4 emotes espada rota
– 1 trofeo trono

El Pack fundador deluxe contiene:
– 5.000 gemas
– 1 mascota simbionte
– 4 efectos visuales en combate
– 4 efectos visuales mundo
– todos los objetos cosméticos del pack estándar

El Pack fundador premium contiene
– 7.000 gemas
– 1 mascota Dark Vlad
– 4 efectos visuales mundo premium
– 4 efectos visuales en combate premium
– 4 burbujas de diálogo de Gúltar
– todos los objetos cosméticos del pack deluxe

 

La entrada Acceso anticipado gratuito en PC y Mac para WAVEN: ¡Izad las velas el 16 de agosto! apareció primero en ZIRAN Comunicación.]]>
Juega a la demo de WAVEN hoy en PC y Mac durante el Steam Next Fest https://www.ziran.es/nota_prensa/juega-a-la-demo-de-waven-hoy-en-pc-y-mac-durante-el-steam-next-fest/ Mon, 19 Jun 2023 16:22:03 +0000 https://www.ziran.es/?post_type=nota_prensa&p=52434 La demo también estará disponible en la plataforma de Ankama Ankama Games se complace en invitar a los jugadores a experimentar una demo de su próximo RPG táctico multijugador WAVEN como parte del Steam Next Fest, desde hoy, 19 de junio, a las 7pm CEST hasta el 26 de junio de 2023. Descarga la demo […]

La entrada Juega a la demo de WAVEN hoy en PC y Mac durante el Steam Next Fest apareció primero en ZIRAN Comunicación.]]>
La demo también estará disponible en la plataforma de Ankama

Ankama Games se complace en invitar a los jugadores a experimentar una demo de su próximo RPG táctico multijugador WAVEN como parte del Steam Next Fest, desde hoy, 19 de junio, a las 7pm CEST hasta el 26 de junio de 2023.

Descarga la demo en Steam https://store.steampowered.com/app/2343650/Waven/*

Descarga la demo en la plataforma de Ankama: http://www.waven-game.com/*

*La descarga estará disponible a partir de las 7pm de hoyWAVEN es una nueva experiencia de juego de rol accesible, divertida y envolvente. Combina aventura y estrategia por turnos, inspirada en los mejores RPG tácticos, con mecánicas de construcción de mazos y valiosos objetos para coleccionar.

En esta demo, los jugadores podrán elegir entre 25 héroes y estilos de juego (entre 5 clases) y subir hasta nivel 20. Una interfaz de selección de héroes simplificada y un tutorial permitirán a los jugadores conocer mejor las mecánicas del juego y el escenario en el que se desarrolla WAVEN. Los jugadores zarparán hacia las costas de 4 islas PvE, incluida la Isla Nación de Astrub, y descubrirán todos sus secretos (en solitario o en cooperativo) o lucharán en la arena en el modo PvP.

La demo también contiene algunas de las características que los jugadores encontrarán en la versión final del juego, como un árbol de habilidades, un sistema de interacción con los jugadores y un diario que registra la progresión de los jugadores. Esta versión del juego estará disponible en inglés, portugués de Brasil, español, alemán y francés.

Todos los detalles de la demo pueden consultarse en la entrada del blog correspondiente.

Los jugadores tendrán acceso anticipado a WAVEN a finales de este verano para probar una última versión del juego y dar su opinión final antes del lanzamiento mundial free-to-play a finales de 2023. El juego será coeditado por Ankama y New Tales.
Para no perderse nada, los jugadores pueden suscribirse al boletín y añadir el juego a sus listas de deseos de Steam.

 

La entrada Juega a la demo de WAVEN hoy en PC y Mac durante el Steam Next Fest apareció primero en ZIRAN Comunicación.]]>