/** * 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; } } Amazon | ZIRAN Comunicación https://www.ziran.es Comunicación Estratégica Tue, 18 Aug 2026 13:21:29 +0000 es hourly 1 https://wordpress.org/?v=7.1 https://www.ziran.es/wp-content/uploads/2020/07/favicon.svg Amazon | ZIRAN Comunicación https://www.ziran.es 32 32 Los desarrolladores de Tomb Raider: Legacy of Atlantis hablan sobre el combate, la creación de objetos y la progresión del árbol de habilidades https://www.ziran.es/nota_prensa/los-desarrolladores-de-tomb-raider-legacy-of-atlantis-hablan-sobre-el-combate-la-creacion-de-objetos-y-la-progresion-del-arbol-de-habilidades/ Tue, 18 Aug 2026 13:21:29 +0000 https://www.ziran.es/?post_type=nota_prensa&p=68385 Amazon Game Studios y Crystal Dynamics acaban de publicar el sexto episodio de la serie de minidocumentales sobre la creación de Tomb Raider: Legacy of Atlantis, una impresionante reinterpretación de la aventura debut de Lara Croft en 1996, que marcó un hito en su carrera. . Este episodio se centra en cómo los equipos de Crystal Dynamics […]

La entrada Los desarrolladores de Tomb Raider: Legacy of Atlantis hablan sobre el combate, la creación de objetos y la progresión del árbol de habilidades apareció primero en ZIRAN Comunicación.]]>

Amazon Game Studios y Crystal Dynamics acaban de publicar el sexto episodio de la serie de minidocumentales sobre la creación de Tomb Raider: Legacy of Atlantis, una impresionante reinterpretación de la aventura debut de Lara Croft en 1996, que marcó un hito en su carrera. .

Este episodio se centra en cómo los equipos de Crystal Dynamics y Flying Wild Hog han adaptado el combate y la progresión al público actual con sistemas diseñados para realzar todos los aspectos de Lara Croft. El equipo analiza en profundidad el sistema «Focus», las diversas armas que Lara utilizará a lo largo de su aventura —incluidas sus emblemáticas pistolas dobles— y destaca los distintos estilos de juego que ofrece cada arma para combatir y contrarrestar a los enemigos.

Uno de los principales cambios en la jugabilidad de Legacy of Atlantis que estamos introduciendo es un sistema llamado Focus”, afirma Raúl Siqueira, director del juego Tomb Raider: Legacy of Atlantis. «Cuando realizas acciones en combate —recibir daño, infligir daño, esquivar ataques—, vas llenando tu medidor de Focus. Focus te permite ralentizar el tiempo a tu alrededor y mostrar algunas de las cosas que Lara solía hacer en 1996. El componente acrobático es realmente importante para el personaje, así que queríamos asegurarnos de que hubiera una forma de plasmarlo en los mandos”.

Además, se han dado a conocer por primera vez detalles sobre la progresión del árbol de habilidades y la creación de objetos del juego.

Tomb Raider: Legacy of Atlantis saldrá a la venta el 12 de febrero de 2027 para PlayStation 5, Xbox Series X|S, Steam y Nintendo Switch 2.

Puedes ver el episodio AQUÍ.

Los episodios anteriores se pueden encontrar aquí:


Acerca de Tomb Raider: El legado de la Atlántida 

Tomb Raider: Legacy of Atlantis es un juego de acción y aventura para un solo jugador, centrado en la narrativa, que reinterpreta el emblemático título debut de 1996. En Tomb Raider: Legacy of Atlantis, los jugadores se pondrán en la piel de Lara Croft mientras se embarca en una arriesgada aventura por todo el mundo para descubrir los secretos perdidos de la Atlántida, con unos gráficos impresionantes, una jugabilidad modernizada y nuevas sorpresas que rinden homenaje al espíritu del juego original. Entornos envolventes, acertijos conocidos redefinidos con nuevas mecánicas y momentos inolvidables —como la épica batalla contra el T-Rex— se transforman en encuentros de juego cinematográficos a gran escala. Esta es la legendaria aventura que los aficionados recuerdan, reinventada de formas que antes no eran posibles y con nuevas sorpresas.


 

La entrada Los desarrolladores de Tomb Raider: Legacy of Atlantis hablan sobre el combate, la creación de objetos y la progresión del árbol de habilidades apareció primero en ZIRAN Comunicación.]]>
Actualización de contenido de Luna de agosto https://www.ziran.es/nota_prensa/actualizacion-de-contenido-de-luna-de-agosto/ Tue, 18 Aug 2026 10:19:37 +0000 https://www.ziran.es/?post_type=nota_prensa&p=68378 Además de los títulos anteriores, los miembros Prime pueden encontrar los siguientes juegos de PC disponibles durante todo el mes, (haz clic aquí para reclamar los juegos) A finales de agosto llegarán al catálogo de Luna dos títulos aclamados por la crítica de Ubisoft: Assassin’s Creed™ Valhalla y Assassin’s Creed™ Mirage. Además de los títulos […]

La entrada Actualización de contenido de Luna de agosto apareció primero en ZIRAN Comunicación.]]>

Además de los títulos anteriores, los miembros Prime pueden encontrar los siguientes juegos de PC disponibles durante todo el mes, (haz clic aquí para reclamar los juegos)

A finales de agosto llegarán al catálogo de Luna dos títulos aclamados por la crítica de Ubisoft: Assassin’s Creed™ Valhalla y Assassin’s Creed™ Mirage.

Además de los títulos anteriores, los miembros Prime pueden encontrar los siguientes juegos de PC disponibles durante todo el mes,

Juegos de agosto de 2026 – Incluidos con Prime

Una vez más, Luna ofrece a los miembros Prime una fantástica selección en agosto, con juegos fáciles de empezar y disfrutar en solitario o con familiares y amigos. Estos nuevos títulos se suman a los más de 80 juegos disponibles a través de Luna Standard. Visita luna.amazon.com para explorar la biblioteca, que se actualiza periódicamente.

Además de los títulos anteriores, los miembros Prime pueden encontrar los siguientes juegos de PC disponibles durante todo el mes,

Ya disponible: Batman: Caped Crusader – Chronicles – Reúne a tu equipo y adéntrate en la Gotham City de los años 40, impregnada de estética noir, de la serie de animación Batman: Caped Crusader. Cada callejón esconde un secreto y cada caso conduce más profundamente a las oscuras entrañas de la mente criminal de Gotham, incluidos Espantapájaros y el maniático Joker.
Además de los títulos anteriores, los miembros Prime pueden encontrar los siguientes juegos de PC disponibles durante todo el mes.
Ya disponible: Bluey: The Videogame – La diversión con la familia Heeler es ahora más grande y mejor, con nuevos personajes, minijuegos, objetos desbloqueables y coleccionables por descubrir. Únete a Bluey, Bingo, Mamá, Papá, además de su prima Muffin y su tío Stripe, en aventuras interactivas ambientadas en lugares emblemáticos.
Además de los títulos anteriores, los miembros Prime pueden encontrar los siguientes juegos de PC disponibles durante todo el mes,
· Hiveswap Friendsim (6 de agosto, GOG)
· Pyramids and Aliens: Escape Room (13 de agosto, GOG)
· Airship: Kingdoms Adrift (13 de agosto, Epic Games Store)
· Steelrising (13 de agosto, Epic Games Store)
· Royal Romances: Forbidden Magic (20 de agosto)
· Spray Paint Simulator (20 de agosto, Amazon Games App)
· Havendock (27 de agosto, Amazon Games App)
· Clash: Artifacts of Chaos (27 de agosto, Epic Games Store)
Visita esta página para conocer las últimas noticias de Luna

Acerca de Amazon Luna
Amazon Luna es el servicio de streaming de videojuegos de Amazon del que pueden disfrutar los miembros Prime sin coste adicional, sin necesidad de descargas ni de una consola o un ordenador específico para videojuegos. Los miembros Prime tienen acceso a una amplia variedad de juegos en Amazon Luna, entre los que se incluyen la comedia de superhéroes con una trama muy cuidada y ganadora de un premio BAFTA, Dispatch; EA SPORTS FC™ 26; la alocada Courtroom Chaos, protagonizada por Arnold Schwarzenegger; y el juego cooperativo de construcción de mazos tipo roguelike Masters of the Universe: Legends Unite.
La entrada Actualización de contenido de Luna de agosto apareció primero en ZIRAN Comunicación.]]>
Tomb Raider: Legacy of Atlantis: los desarrolladores analizan los desafiantes rompecabezas y trampas https://www.ziran.es/nota_prensa/tomb-raider-legacy-of-atlantis-los-desarrolladores-analizan-los-desafiantes-rompecabezas-y-trampas/ Mon, 10 Aug 2026 09:53:14 +0000 https://www.ziran.es/?post_type=nota_prensa&p=67942   Amazon Game Studios y Crystal Dynamics acaban de publicar el quinto episodio de la serie de minidocumentales sobre la creación de Tomb Raider: Legacy of Atlantis, una impresionante reinterpretación de la aventura debut de Lara Croft en 1996, que marcó un hito en su carrera. ¿Qué tipo de reto te hace seguir dándole vueltas, mucho tiempo […]

La entrada Tomb Raider: Legacy of Atlantis: los desarrolladores analizan los desafiantes rompecabezas y trampas apareció primero en ZIRAN Comunicación.]]>

 

Amazon Game Studios y Crystal Dynamics acaban de publicar el quinto episodio de la serie de minidocumentales sobre la creación de Tomb Raider: Legacy of Atlantis, una impresionante reinterpretación de la aventura debut de Lara Croft en 1996, que marcó un hito en su carrera.

¿Qué tipo de reto te hace seguir dándole vueltas, mucho tiempo después de haberlo resuelto?

Este episodio se centra en cómo los equipos de Crystal Dynamics y Flying Wild Hog han sabido captar la esencia de los acertijos del juego original y los han adaptado al público actual. En el episodio, descubrirás cómo los desarrolladores han combinado el diseño de niveles y la creación del mundo para crear los acertijos y las trampas mortales que Lara tendrá que superar en su aventura.

Tomb Raider: Legacy of Atlantis saldrá a la venta el 12 de febrero de 2027 para PlayStation 5, Xbox Series X|S, Steam y Nintendo Switch 2.

Puedes ver el episodio AQUÍ.

Los episodios anteriores se pueden encontrar aquí:


Acerca de Tomb Raider: El legado de la Atlántida 

Tomb Raider: Legacy of Atlantis es un juego de acción y aventura para un solo jugador, centrado en la narrativa, que reinterpreta el emblemático título debut de 1996. En Tomb Raider: Legacy of Atlantis, los jugadores se pondrán en la piel de Lara Croft mientras se embarca en una arriesgada aventura por todo el mundo para descubrir los secretos perdidos de la Atlántida, con unos gráficos impresionantes, una jugabilidad modernizada y nuevas sorpresas que rinden homenaje al espíritu del juego original. Entornos envolventes, acertijos conocidos redefinidos con nuevas mecánicas y momentos inolvidables —como la épica batalla contra el T-Rex— se transforman en encuentros de juego cinematográficos a gran escala. Esta es la legendaria aventura que los aficionados recuerdan, reinventada de formas que antes no eran posibles y con nuevas sorpresas.

La entrada Tomb Raider: Legacy of Atlantis: los desarrolladores analizan los desafiantes rompecabezas y trampas apareció primero en ZIRAN Comunicación.]]>
‘Batman: Caped Crusader – Chronicles’ ya está disponible, en exclusiva en Amazon Luna https://www.ziran.es/nota_prensa/batman-caped-crusader-chronicles-ya-esta-disponible-en-exclusiva-en-amazon-luna/ Mon, 10 Aug 2026 09:46:08 +0000 https://www.ziran.es/?post_type=nota_prensa&p=67938 Batman: Caped Crusader – Chronicles ya está disponible y se estrena coincidiendo con el estreno de la segunda temporada de la exitosa serie de animación Batman: Caped Crusader (Batman: el cruzado enmascarado) en Prime Video. Disponible en exclusiva en Amazon Luna e incluido en la suscripción Prime sin coste adicional. Juega hoy mismo y disfruta de Amazon Luna en los dispositivos que ya […]

La entrada ‘Batman: Caped Crusader – Chronicles’ ya está disponible, en exclusiva en Amazon Luna apareció primero en ZIRAN Comunicación.]]>

Batman: Caped Crusader – Chronicles ya está disponible y se estrena coincidiendo con el estreno de la segunda temporada de la exitosa serie de animación Batman: Caped Crusader (Batman: el cruzado enmascarado) en Prime Video. Disponible en exclusiva en Amazon Luna e incluido en la suscripción Prime sin coste adicional. Juega hoy mismo y disfruta de Amazon Luna en los dispositivos que ya tienes: sin necesidad de descargas, de hardware específico para videojuegos ni de mandos adicionales.

Juega al videojuego, ve la serie. Los miembros de Prime pueden ver la temporada 2 de Batman: Caped Crusader (Batman: el cruzado enmascarado) en Prime Video y jugar a Batman: Caped Crusader – Chronicles a través de Amazon Luna.

¿Quieres saber qué les espera a los jugadores? Echa un vistazo al nuevo vídeo de Let’s Play aquí.

Únete a un grupo de trabajo especial del GCPD y colabora con Batman y la detective Montoya para resolver los casos y salvar la ciudad. Forma equipo con hasta tres amigos para investigar 11 casos episódicos, descubrir pistas a través de minijuegos únicos de temática detectivesca y llevar ante la justicia a villanos tan famosos como el Espantapájaros, Babyface y el Joker.

Al igual que todos los títulos de Amazon Luna GameNight, Batman: Caped Crusader – Chronicles se transmite al instante a dispositivos compatibles, utilizando los smartphones como mandos; no es necesario descargar nada, ni se necesitan consolas de videojuegos ni mandos adicionales.

Características principales

  • Ya disponible en exclusiva en Amazon Luna, incluido en la suscripción Prime sin coste adicional.
  • Se estrena junto con la segunda temporada de Batman: Caped Crusader (Batman: el cruzado enmascarado): ve la serie y luego sumérgete directamente en el juego.
  • Convierte GameNight en una noche de detectives con un juego cooperativo de resolución de crímenes para hasta cuatro jugadores a lo largo de 11 casos únicos; no se necesita ninguna consola ni mandos adicionales.
  • Cuenta con el reparto de voces original de la serie de animación, entre los que se incluyen Hamish Linklater (Batman), Michelle C. Bonilla (la detective Montoya) y el recién anunciado Matthew Needham como el Joker.

Puedes descargar material gráfico aquí.


Acerca de Amazon Luna

Amazon Luna es el servicio de streaming de videojuegos de Amazon del que pueden disfrutar los miembros Prime sin coste adicional, sin necesidad de descargas ni de una consola o un ordenador específico para videojuegos. Los miembros Prime tienen acceso a una amplia variedad de juegos en Amazon Luna, entre los que se incluyen la comedia de superhéroes con una trama muy cuidada y ganadora de un premio BAFTA, Dispatch; EA SPORTS FC™ 26; la alocada Courtroom Chaos, protagonizada por Arnold Schwarzenegger; y el juego cooperativo de construcción de mazos tipo roguelike Masters of the Universe: Legends Unite.


 

La entrada ‘Batman: Caped Crusader – Chronicles’ ya está disponible, en exclusiva en Amazon Luna apareció primero en ZIRAN Comunicación.]]>
Batman: Caped Crusader – Chronicles, próximamente en exclusiva en Amazon Luna https://www.ziran.es/nota_prensa/batman-caped-crusader-chronicles-proximamente-en-exclusiva-en-amazon-luna/ Tue, 28 Jul 2026 18:44:19 +0000 https://www.ziran.es/?post_type=nota_prensa&p=67563   Recién llegado de la Comic-Con de San Diego, donde se dio a conocer como el último anuncio durante el panel de la segunda temporada de Batman: Caped Crusader (Batman: el cruzado enmascarado), aquí tienes todos los detalles que ha facilitado Amazon sobre el nuevo juego Batman: Caped Crusader – Chronicles. Estreno el 31 de julio: Batman: Caped Crusader […]

La entrada Batman: Caped Crusader – Chronicles, próximamente en exclusiva en Amazon Luna apareció primero en ZIRAN Comunicación.]]>

 

Recién llegado de la Comic-Con de San Diego, donde se dio a conocer como el último anuncio durante el panel de la segunda temporada de Batman: Caped Crusader (Batman: el cruzado enmascarado), aquí tienes todos los detalles que ha facilitado Amazon sobre el nuevo juego Batman: Caped Crusader – Chronicles.
  • Estreno el 31 de julio: Batman: Caped Crusader – Chronicles llega en exclusiva a Amazon Luna el mismo día en que se estrena la segunda temporada de Batman: Caped Crusader (Batman: el cruzado enmascarado) en Prime Video. Disfruta de Amazon Luna en los dispositivos que ya tienes: sin necesidad de descargas, de hardware específico para videojuegos ni de mandos adicionales.
  • Incluido con Prime, sin coste adicional: los socios de Prime pueden disfrutar de una aventura detectivesca cooperativa de 11 episodios para entre 1 y 4 jugadores con amigos y familiares, utilizando sus smartphones como mandos.
  • El mejor actor de doblaje de Gotham City: tal y como se acaba de anunciar en la SDCC, Matthew Needham, la nueva voz del Joker en la segunda temporada de Batman: Caped Crusader (Batman: el cruzado enmascarado), también interpretará al emblemático villano en el videojuego, junto a Hamish Linklater y Michelle C. Bonilla, que vuelven a meterse en la piel de Batman y la detective Montoya, respectivamente.
  • Ver y jugar en la misma aplicación: a partir de hoy, los usuarios de Fire TV en EE. UU. y el Reino Unido pueden acceder a los juegos de Amazon Luna directamente desde la aplicación de Prime Video, lo que facilita pasar directamente de la serie al juego.

Más información a continuación. Puedes descargar el material gráfico aquí.

Batman: Caped Crusader – Chronicles, próximamente en exclusiva en Amazon Luna

¡Gotham City te necesita! Amazon Luna ha anunciado que Batman: Caped Crusader – Chronicles, con licencia oficial de Warner Bros. Interactive Entertainment, se lanzará el 31 de julio, coincidiendo con el estreno de la segunda temporada de la aclamada serie de animación Batman: Caped Crusader (Batman: el cruzado enmascarado) en Prime Video. Los miembros Prime podrán disfrutar del juego en Luna sin coste adicional. Juega junto a personajes emblemáticos de DC, con las voces de algunos de los actores originales de la serie, en una aventura de 11 episodios repleta de minijuegos desafiantes que pondrán a prueba tus habilidades detectivescas.

La próxima semana será un momento emocionante para los seguidores de la franquicia. A partir de hoy, los miembros de Prime que tengan un Fire TV en EE. UU. y el Reino Unido podrán jugar a los juegos de Luna directamente en Prime Video, junto con series, películas y deportes en directo. Esto significa que los seguidores de Batman podrán acceder al nuevo juego y a la serie de televisión en una sola aplicación, lo que les facilitará sumergirse directamente en la acción tras ver la serie. Juega en solitario o con amigos, utilizando tu smartphone como mando, sin necesidad de hardware adicional ni descargas.

 

«JUGAR» LA SERIE

Una verdad que Batman ha ido descubriendo a lo largo de los años, tras una dura lucha, es que los crímenes más oscuros de Gotham City no se resuelven por sí solos. En este juego cooperativo para entre 1 y 4 jugadores, tú y tu equipo os adentraréis en la Gotham City impregnada de atmósfera noir de Batman: Caped Crusader (Batman: el cruzado enmascarado), la serie de animación, donde cada callejón esconde un secreto y cada caso os lleva más allá, a las sombrías profundidades del mundo criminal de la ciudad.

Jugando junto a la Detective Montoya, como parte del círculo más cercano de Batman y del grupo de trabajo del GCPD, los jugadores descifrarán pruebas, seguirán pistas y se enfrentarán a emblemáticos supervillanos de DC a lo largo de 11 episodios apasionantes. A lo largo del juego, cada episodio ofrece nuevos minijuegos de investigación que pondrán a prueba tus habilidades detectivescas. Investiga, interroga y deduce para abrirte camino a través de las enrevesadas tramas delictivas tejidas por los criminales más retorcidos de Gotham City, entre los que se incluyen “Scarecrow”, “Babyface”, “Tweedledee and Tweedledum”, “Dollmaker”, “Lynx”, “Music Meister”, y, por supuesto, el maníaco “The Joker”.

Para ofrecer a los fans la conexión definitiva entre la serie y el videojuego, las estrellas de la serie, Hamish Linklater y Michelle C. Bonilla, vuelven a interpretar sus papeles de Batman y la detective Montoya en Batman: Caped Crusader – Chronicles. Además, Prime Video ha anunciado hoy que Matthew Needam será la voz del «Joker» en la segunda temporada de Batman: Caped Crusader (Batman: el cruzado enmascarado), y que también prestará su voz al famoso supervillano de DC en el videojuego.

“Estamos encantados de presentar el juego Batman: Caped Crusader – Chronicles para Amazon Luna, que se lanzará coincidiendo con la segunda temporada de la exitosa serie de Prime Video, Batman: Caped Crusader (Batman: el cruzado enmascarado)”, afirmó J.C. Connors, director de Amazon Game Studios. “Y ahora, gracias a la nueva ampliación de la disponibilidad de Luna a través de Prime Video, los jugadores que tengan un Fire TV podrán sumergirse directamente en el juego segundos después de ver la serie, todo ello dentro de una experiencia de entretenimiento unificada”.

Cómo la emblemática serie consiguió un nuevo compañero en la lucha contra el crimen

La primera temporada de Batman: Caped Crusader (Batman: el cruzado enmascarado) caló hondo entre el público, llegando a alcanzar el puesto n.º 1 en la lista global de las 10 más vistas de Prime Video. Ante la gran expectación generada por la segunda temporada y con el número de seguidores de Batman más alto que nunca, Amazon Luna y Prime Video han creado una oportunidad para que los fans no solo vean Gotham City, sino que también puedan sumergirse en ella y jugar en su interior.

Batman: Caped Crusader – Chronicles es una importante novedad de GameNight, una colección cada vez más amplia de juegos disponibles exclusivamente a través de Amazon Luna y gratuitos para los miembros Prime. Diseñado para facilitar el juego en grupo, Batman: Caped Crusader – Chronicles convertirá la noche de juegos en una operación de vigilancia en Gotham City, todo ello disfrutado en la pantalla más grande de tu casa. Tu móvil es tu mando. Sin hardware adicional. Sin descargas. Sin diferencias de habilidad. Ya sea una noche de juegos en familia o con amigos, todo el mundo puede sumarse y jugar, desde los que se estrenan hasta los detectives más experimentados.

Lanzamiento en varios países

El 31 de julio, Batman: Caped Crusader – Chronicles estará disponible para los miembros de Prime y los suscriptores de Luna en la aplicación de Luna en EE. UU., Reino Unido, Canadá, Alemania, Francia, Italia, España, Austria, Países Bajos, Polonia, Suecia, Portugal, Bélgica y Luxemburgo.

¡Reúne a tu equipo, ponte el sombrero de detective y prepárate: los expedientes de Gotham City están a punto de abrirse!

BATMAN CAPED CRUSADER and all related characters and elements © & ™ DC and Warner Bros. Entertainment Inc. (s26)

La entrada Batman: Caped Crusader – Chronicles, próximamente en exclusiva en Amazon Luna apareció primero en ZIRAN Comunicación.]]>
Masters of the Universe: Legends Unite presenta nuevo contenido extra https://www.ziran.es/nota_prensa/masters-of-the-universe-legends-unite-presenta-nuevo-contenido-extra/ Tue, 21 Jul 2026 13:14:57 +0000 https://www.ziran.es/?post_type=nota_prensa&p=67087 2 de julio de 2026 – ¡Más noticias importantes desde Eternia! Es la temporada de los blockbusters del verano y, el 22 de julio, llega a Prime Video la película de acción real Masters of the Universe. Al mismo tiempo, Luna lanza una importante actualización de contenido extra para Masters of the Universe: Legends Unite, que incluye nuevos personajes, […]

La entrada Masters of the Universe: Legends Unite presenta nuevo contenido extra apareció primero en ZIRAN Comunicación.]]>

2 de julio de 2026 – ¡Más noticias importantes desde Eternia! Es la temporada de los blockbusters del verano y, el 22 de julio, llega a Prime Video la película de acción real Masters of the Universe. Al mismo tiempo, Luna lanza una importante actualización de contenido extra para Masters of the Universe: Legends Unite, que incluye nuevos personajes, cartas y formas de jugar. Disfruta de la película y juega a la versión ampliada del juego a partir del miércoles, sin coste adicional para los miembros Prime.

Los miembros Prime pueden solicitar estas ofertas en luna.amazon.com. Para obtener información sobre la oferta de este mes, así como las últimas y mejores noticias sobre Luna, visita amazongamestudios.com y echa un vistazo a la entrada del blog de este mes aquí . Puedes descargar los materiales gráficos aquí.

Más jugadores

Los títulos de GameNight de Luna están pensados para entretener a los jugadores, y Masters of the Universe: Legends Unite ha ampliado la fiesta. Con nuestra actualización de contenido extra, ahora puedes jugar con entre 1 y 6 jugadores, en lugar de entre 1 y 4.

Más personajes

Además de los seis personajes jugables legendarios originales, Masters of the Universe: Legends Unite incorpora a dos nuevos eternianos jugables y a seis nuevos enemigos en esta actualización:

  • Juega en el papel del heroico guerrero mecánico Roboto, construido originalmente por Man-At-Arms, o, si necesitas fuerza bruta, juega como Spikor, el secuaz de múltiples tentáculos de Skeletor.
  • No todos los personajes de esta actualización son aliados potenciales. Los jugadores también se enfrentarán a Teela Anti-Eternia, Rock Man Anti-Eternia, Shadow Beast Anti-Eternia, Man-At-Arms Anti-Eternia, He-Man Anti-Eternia, Ice Man Anti-Eternia, Horde Trooper Anti-Eternia, Horde Wraith Anti-Eternia, Ram Man Anti-Eternia y Fakor, un jefe de batalla de élite.

Más cosas que descubrir

En esencia, Masters of the Universe: Legends Unite es un juego estratégico de construcción de mazos que permite a los jugadores crear mazos únicos basados en sus personajes favoritos, enfrentarse a extraños enemigos y retarse entre sí en minijuegos de arcade.

  • La actualización de contenido extra añade 80 cartas nuevas específicas de Spikor y Roboto a la colección original de 400.
  • Un nuevo bioma, Anti-Eternia, trae consigo nuevos retos y paisajes sombríos que explorar.

Juega ahora en GameNight

Sin ningún coste adicional para los miembros Prime, Masters of the Universe: Legends Unite forma parte de la colección GameNight de Amazon Luna, que incluye juegos divertidos y accesibles a los que puedes jugar desde cualquier lugar donde te reúnas con tu familia y amigos. No se necesitan mandos, consolas ni ordenadores gamer: lo único que necesitas es tu móvil y un televisor.

Masters of the Universe: Legends Unite está disponible para los miembros de Prime y los suscriptores de Luna en EE. UU., Reino Unido, Canadá, Alemania, Francia, Italia, España, Austria, Países Bajos, Polonia, Suecia, Portugal, Bélgica y Luxemburgo.

MASTERS OF THE UNIVERSE™️and associated trademarks and trade dress are owned by, and used under license from, Mattel. ©2026 Mattel.

La entrada Masters of the Universe: Legends Unite presenta nuevo contenido extra apareció primero en ZIRAN Comunicación.]]>
Actualización de contenidos de Luna de julio https://www.ziran.es/nota_prensa/actualizacion-de-contenidos-de-luna-de-julio/ Mon, 06 Jul 2026 08:58:37 +0000 https://www.ziran.es/?post_type=nota_prensa&p=66225 2 de julio de 2026 – El verano se va calentando, ya han llegado los días soleados y, con ellos, ¡la actualización de contenido de julio de Luna! Este mes, Luna presenta Dispatch, una comedia sobre el mundo laboral de los superhéroes en la que los jugadores dirigen un equipo disfuncional de héroes inadaptados. Además, irrumpe con fuerza […]

La entrada Actualización de contenidos de Luna de julio apareció primero en ZIRAN Comunicación.]]>

2 de julio de 2026 – El verano se va calentando, ya han llegado los días soleados y, con ellos, ¡la actualización de contenido de julio de Luna! Este mes, Luna presenta Dispatch, una comedia sobre el mundo laboral de los superhéroes en la que los jugadores dirigen un equipo disfuncional de héroes inadaptados. Además, irrumpe con fuerza en la selección el icónico erizo azul de los videojuegos en Sonic Mania Plus. También llega el nuevo título exclusivo de GameNightCentipede Swarm, una vibrante reinterpretación de un clásico de las salas recreativas. Estos nuevos juegos se suman a una selección rotativa de juegos favoritos que se actualiza a lo largo del mes. En julio, Juegos gratis con Prime ofrece a los jugadores aún más motivos para emocionarse con 12 juegos para PC recién añadidos que pueden reclamar y añadir a su biblioteca.

Juega a Luna con tu suscripción Prime: una ventaja todo en uno que ofrece ahorro, comodidad y entretenimiento. Con Luna, los miembros Prime pueden disfrutar de una biblioteca de juegos en constante evolución allí donde esté disponible el servicio de distribución en la nube de Luna, así como de una colección mensual y renovada de juegos para PC descargables.

Los miembros Prime pueden solicitar estas ofertas en luna.amazon.com. Para obtener información sobre la oferta de este mes, así como las últimas y mejores noticias sobre Luna, visita amazongamestudios.com y echa un vistazo a la entrada del blog de este mes aquí . Puedes descargar los materiales gráficos aquí.

Juegos de julio de 2026 incluidos con Prime

Una vez más, Luna ofrece a los miembros Prime otra fantástica selección este mes, que incluye nuevos y exclusivos juegos de mesa de GameNight, fáciles de aprender y de disfrutar tanto en solitario como con la familia y los amigos, utilizando únicamente un teléfono y un televisor. Estos nuevos títulos se suman a los más de 50 juegos disponibles a través de Luna Standard. Visita luna.amazon.com para explorar la biblioteca, que se actualiza periódicamente.

Para tu próxima GameNight

  • Ya disponible: Courtroom Chaos: Starring Arnold Schwarzenegger – Todo el mundo en pie ante el juez Arnold Schwarzenegger! ¡Comienza la sesión de Courtroom Chaos! Con el legendario juez de IA Arnold Schwarzenegger presidiendo el tribunal, improvisarás testimonios descabellados, crearás personajes ridículos y defenderás casos imposibles de ganar. Pero no te dejes engañar por las risas: este juez no se anda con rodeos.
  • Ya disponible: Centipede Swarm  – Otra exclusiva de GameNight: el clásico juego arcade de Atari se ha reinventado como un shooter cooperativo de doble joystick en el que de uno a cuatro jugadores se enfrentan a oleadas interminables de hordas de insectos de colores pastel en un campo de batalla dinámico, para acabar enfrentándose a un Centipede que se divide, muta y ataca desde todas las direcciones.
  • 22 de julio: Jackbox Party Pack 8 – Tu próxima noche de juegos se está organizando sola con la octava entrega del popular Jackbox Party Pack. En esta ocasión, hasta 10 jugadores podrán disfrutar de 5 nuevos y divertidísimos juegos de fiesta, entre los que se incluyen Drawful Animate, Job Job, The Poll Mine, Weapons Drawn The Wheel of Enormous Proportions. No se necesitan mandos adicionales.
  • 23 de julio: Angry Birds Mystery Island – ¡Descubre un nuevo nivel que se suma a la diversión y la emoción trepidante de Angry Birds Flock Party!

Disponible a partir de este mes en Luna Standard

  • Ya disponible: Dispatch – Dispatch es una comedia de superhéroes ambientada en el mundo laboral en la que las decisiones cuentan. Dirige un equipo disfuncional de héroes inadaptados y elabora estrategias para decidir a quién enviar a las emergencias que surgen por toda la ciudad, todo ello mientras gestionas las intrigas de la oficina, las relaciones personales y tu propia búsqueda para convertirte en un héroe.
  • Ya disponible:  Sonic Mania Plus – Vive la galardonada celebración definitiva del pasado y el futuro. Sonic Mania Plus es una nueva aventura de Sonic en 2D con una velocidad vertiginosa de 60 fps y unos impresionantes gráficos en HD de estilo retro. Ahora, con los nuevos personajes jugables Mighty y Ray, el nuevo modo Encore y mucho más, ¡Sonic Mania Plus es la experiencia definitiva de Sonic!

Además, a partir de este mes, los jugadores podrán disfrutar de los siguientes juegos en Luna Standard:

PC:
Además de lo anterior, los miembros Prime pueden disfrutar de los siguientes títulos para PC disponibles durante todo el mes (haz clic aquí para canjear los juegos)
  • 2 de julio CyClones (Código de GOG)
  • 2 de julio LoneStar (Epic Games Store)
  • 2 de julio Symphony of War: The Nephilim Saga (Epic Games Store)
  • 9 de julio Still There (Código de GOG)
  • 9 de julio Regular Factory: Escape Room (Código de GOG)
  • 16 de julio Poly Vita (Código de Legacy Games)
  • 16 de julio Framed Collection (Código de GOG)
  • 16 de julio ​​​​​​​Escape Academy (Epic Games Store)
  • 23 de julio ​​​​​​​​​​​​​​In Sound Mind (Amazon Games app)
  • 23 de julio ​​​​​​​​​​​​​​Mystic Academy: Escape Room (Código de GOG)
  • 30 de julio ​​​​​​​​​​​​​​Zoria: Age of Shattering (Código de GOG)
  • 30 de julio ​​​​​​​​​​​​​​Weakless (Código de GOG)

​​​​​​​Visita esta página para conocer las últimas noticias sobre Luna y mantente al tanto de las redes sociales oficiales de Luna X, TikTok e Instagram.

La entrada Actualización de contenidos de Luna de julio apareció primero en ZIRAN Comunicación.]]>
Tomb Raider: Legacy of Atlantis: nuevo trailer de gameplay, comienzo de reservas, ediciones y fecha de lanzamiento. https://www.ziran.es/nota_prensa/tomb-raider-legacy-of-atlantis-nuevo-trailer-de-gameplay-comienzo-de-reservas-ediciones-y-fecha-de-lanzamiento/ Wed, 03 Jun 2026 14:07:02 +0000 https://www.ziran.es/?post_type=nota_prensa&p=65228 Hoy, Amazon Game Studios, Crystal Dynamics y Flying Wild Hog han anunciado que ya se pueden realizar reservas de Tomb Raider: Legacy of Atlantis, la esperadísima nueva versión del juego original Tomb Raider de 1996. El juego saldrá a la venta el 12 de febrero de 2027 para PlayStation 5, Xbox Series X|S, Steam y Nintendo Switch 2. Durante la […]

La entrada Tomb Raider: Legacy of Atlantis: nuevo trailer de gameplay, comienzo de reservas, ediciones y fecha de lanzamiento. apareció primero en ZIRAN Comunicación.]]>

Hoy, Amazon Game Studios, Crystal Dynamics y Flying Wild Hog han anunciado que ya se pueden realizar reservas de Tomb Raider: Legacy of Atlantis, la esperadísima nueva versión del juego original Tomb Raider de 1996. El juego saldrá a la venta el 12 de febrero de 2027 para PlayStation 5, Xbox Series X|S, Steam y Nintendo Switch 2. Durante la retransmisión del State of Play de hoy se ha estrenado el tráiler que ofrece a los fans el primer vistazo al nuevo gameplay.
A continuación, encontrarás todos los detalles del anuncio de hoy. Puedes descargar nuevas capturas de pantalla del juego e imágenes de las ediciones de reserva aquí.
Ediciones en preventa
Ya se pueden realizar reservas para PlayStation 5, Xbox Series X|S y Steam. Los jugadores pueden elegir entre tres ediciones:
Standard Edition (59.99 €)
  • Tomb Raider: Legacy of Atlantis (Descarga digital del juego completo)
  • Atuendo de superviviente (contenido adicional al reservar)
Deluxe Edition (69.99 €)
  • Todo lo incluido en la Standard Edition, además de:
  • 48 horas de acceso anticipado (contenido adicional al reservar)
  • Atuendo Parisian Fugitive
  • DLC de la historia tras el lanzamiento
Collector’s Edition (199.99 €)
  • Todo lo incluido en la Deluxe Edition, además de:
  • Caja de acero premium
  • Estatua de Lara Croft contra el T-Rex
  • Minilibro de arte
  • Pin de sello Croft
  • Llavero Talismán del Triunvirato
Versión de Nintendo Switch 2 disponible el día del lanzamiento
El anuncio de hoy ha confirmado que Tomb Raider: Legacy of Atlantis también estará disponible para Nintendo Switch 2 el día de su lanzamiento, el 12 de febrero de 2027. Los jugadores podrán disfrutar de la misma grandiosidad cinematográfica y la misma jugabilidad fluida, totalmente optimizadas para el hardware de Nintendo Switch 2, ya sea en casa, de viaje o dondequiera que les lleve la aventura.
Localización
El día del lanzamiento, Tomb Raider: Legacy of Atlantis estará completamente localizado con audio y texto en inglés, francés, alemán, español (LATAM), portugués (Brasil), polaco y chino simplificado. También tendrá localización solo de textos en español (España), italiano, japonés, coreano y chino tradicional.
Sobre Tomb Raider: Legacy of Atlantis
En Tomb Raider: Legacy of Atlantis, los jugadores se pondrán en la piel de Lara Croft mientras se embarca en una peligrosa aventura por todo el mundo para descubrir los secretos perdidos de la Atlántida, con unos gráficos impresionantes, una jugabilidad modernizada y nuevas sorpresas que rinden homenaje al espíritu del juego original. Entornos envolventes, puzles familiares redefinidos con nuevas mecánicas y momentos inolvidables —como la legendaria batalla contra el T-Rex— se transforman en encuentros épicos a escala cinematográfica. Esta es la legendaria aventura que los fans recuerdan, reinventada de formas que antes no eran posibles y con nuevas sorpresas.
En Tomb Raider: Legacy of Atlantis, los jugadores podrán:
  • Revelar los misterios del pasado: la historia clásica se ha ampliado con vínculos más profundos que conectan la aventura que marcó a Lara Croft con su futuro.
  • Descubre extraordinarios y peligrosos mundos perdidos en el tiempo: explora paisajes traicioneros repletos de artefactos antiguos y secretos ocultos, desde las selvas de Perú hasta las ruinas derruidas de Grecia, pasando por los desiertos de Egipto y una misteriosa isla envuelta en leyendas.
  • Ábrete paso con destreza por entornos mortales: utiliza tu fuerza, agilidad y gancho de escalada para trepar, saltar y balancearte por terrenos peligrosos.
  • Resuelve ingeniosos artilugios antiguos: manipula mecanismos ocultos y el propio entorno para desvelar secretos que llevan siglos ocultos. Cada solución combina los conocimientos que has adquirido y tu ingenio.
  • Enfréntate a depredadores letales: armado con las emblemáticas pistolas dobles de Lara y otras armas desbloqueables, utiliza tus habilidades acrobáticas para lanzar ataques devastadores contra animales salvajes, mercenarios y criaturas mitológicas.
Tomb Raider: Legacy of Atlantis es la versión definitiva de este juego emblemático. Para los seguidores de toda la vida, es una oportunidad de revivir la aventura que lo empezó todo con una nueva perspectiva y nuevas sorpresas. Para los recién llegados, es una oportunidad de disfrutar de una de las aventuras más aclamadas de la historia de los videojuegos con un enfoque moderno.

La entrada Tomb Raider: Legacy of Atlantis: nuevo trailer de gameplay, comienzo de reservas, ediciones y fecha de lanzamiento. apareció primero en ZIRAN Comunicación.]]>
Actualización de contenidos de Luna de junio https://www.ziran.es/nota_prensa/actualizacion-de-contenidos-de-luna-de-junio/ Wed, 03 Jun 2026 13:00:14 +0000 https://www.ziran.es/?post_type=nota_prensa&p=65223 2 de junio de 2026 – Con el Prime Day a la vuelta de la esquina y el verano a la vista, ya está aquí la actualización de contenidos de junio de Luna, ¡que incluye una oferta especial de Prime Day para el mando de Amazon Luna! A medida que los días se vuelven más cálidos […]

La entrada Actualización de contenidos de Luna de junio apareció primero en ZIRAN Comunicación.]]>

2 de junio de 2026 – Con el Prime Day a la vuelta de la esquina y el verano a la vista, ya está aquí la actualización de contenidos de junio de Luna, ¡que incluye una oferta especial de Prime Day para el mando de Amazon Luna! A medida que los días se vuelven más cálidos y largos, Luna ofrece una selección de nuevos títulos, entre los que se incluyen Masters of the Universe: Legends Unite, junto con una selección actualizada de juegos durante todo el mes de junio. Y este mes, Juegos gratis con Prime ofrece a los jugadores un montón de razones para quedarse en casa con nada menos que 15 juegos para descargar y conservar.

Por si fuera poco, los miembros Prime pueden ahorrar un 43 % en el mando de Amazon Luna durante todo el evento Prime Day, del 23 al 26 de junio.

Juega en Luna con tu suscripción Prime: una ventaja todo en uno que ofrece ahorros, comodidad y entretenimiento. Con Luna, los miembros Prime pueden disfrutar de una biblioteca en constante evolución con más de 50 juegos en cualquier lugar donde esté disponible la entrega en la nube de Luna, además de una colección mensual rotativa de juegos descargables para PC.

Los miembros Prime pueden solicitar estas ofertas en luna.amazon.com. Para obtener información sobre la oferta de este mes, así como las últimas y mejores noticias sobre Luna, visita amazongamestudios.com y echa un vistazo a la entrada del blog de este mes aquí. Puedes descargar los materiales gráficos aquí.

Juegos de junio de 2026: incluidos con Prime

Este mes, Luna ofrece a los miembros Prime otra emocionante selección de títulos, entre los que se incluyen nuevos y exclusivos juegos de mesa de GameNight, fáciles de aprender y de disfrutar tanto en solitario como con la familia y los amigos, utilizando únicamente un teléfono y un televisor. Estos nuevos títulos se suman a más de 50 juegos disponibles a través de Luna Standard. Visita luna.amazon.com para explorar la biblioteca, que se actualiza periódicamente.

Para tu próxima GameNight
  • 5 de junio: Masters of the Universe: Legends Unite – Justo a tiempo para el estreno de la película Masters of the Universe, ¡Luna lanza un nuevo y exclusivo juego cooperativo para jugar en el sofá que reúne a los héroes (y villanos) de Eternia! A través de una mecánica de juego única, combates con cartas coleccionables y pruebas competitivas, la estrategia y la colaboración se combinan para ofrecer un entretenimiento dinámico. ¡Ve la película, juega al juego!
Disponible a partir de este mes en Luna Standard
  • Ya a la venta: Hollow Knight: Edición Voidheart -¡Sumérgete en el mundo de Hollow Knight, el galardonado juego de acción y aventuras protagonizado por insectos y héroes! Explora cavernas laberínticas, ciudades antiguas y páramos mortíferos mientras luchas contra criaturas corruptas y te haces amigo de extraños insectos. Descubre la historia antigua y resuelve los misterios enterrados en el corazón del reino.
  • Ya a la venta: Mega Man 11  ¡Vuelve Mega Man! La última entrega de esta emblemática serie combina la acción clásica y desafiante de los juegos de plataformas en 2D con un aspecto renovado. El impresionante nuevo estilo visual revitaliza el característico colorido de la serie, combinando entornos dibujados a mano con modelos de personajes en 3D muy detallados.
  • Ya a la venta: ULTRAKILL – ULTRAKILL es un FPS retro ultraviolento y trepidante que combina el sistema de puntuación basado en la habilidad de los juegos de acción con personajes con una carnicería sin límites inspirada en los mejores shooters de los años 90.

PC:
Además de lo anterior, los miembros Prime pueden disfrutar de los siguientes títulos para PC disponibles durante todo el mes (haz clic aquí para canjear los juegos)
  • 2 de junio Tomb Raider IV-VI Remastered (Epic Games Store)
  • 2 de junio Mafia III: Definitive Edition (Código de GOG)
  • 2 de junio XCOM: Chimera Squad (Código de GOG)
  • 11 de junio Tested on Humans: Escape Room (Código de GOG)
  • 11 de junio Sin Slayers: Reign of the 8th  (Código GOG)
  • 11 de junio G.I. Joe: Wrath of Cobra (Epic Games Store)
  • 11 de junio Paradise Killer (Código de GOG)
  • 18 de junio Between Time: Escape Room (Código GOG)
  • 18 de junio Sugardew Island (Código GOG)
  • 18 de junio Wargame Construction Set III: Age of Rifles 1846-1905 (18 de junio, código de GOG)
  • 18 de junio Space Grunts 2 (Código de GOG)
  • 25 de junio Space Grunts: Chrono Shard (Epic Games Store)
  • 25 de junio Please Touch the Artwork (Código de Legacy Games)
  • 25 de junio Terraforming Mars (Amazon Games app)
  • 25 de junio Lost Eidolons: Veil of the Witch (Amazon Games app)
Visita esta página para conocer las últimas noticias sobre Luna y mantente al tanto de las redes sociales oficiales de Luna X, TikTok e Instagram.
La entrada Actualización de contenidos de Luna de junio apareció primero en ZIRAN Comunicación.]]>
Actualización de contenido de Luna de mayo https://www.ziran.es/nota_prensa/actualizacion-de-contenido-de-luna-de-mayo/ Wed, 13 May 2026 19:10:39 +0000 https://www.ziran.es/?post_type=nota_prensa&p=64323 7 de mayo de 2026 – ¡La primavera ya está aquí, y con más sol y diversión por delante, llega la actualización de contenidos de Luna de mayo! Los días más cálidos están subiendo la temperatura y, a su vez, Luna ofrece una lista de nuevos títulos de lo más interesante, junto con una selección actualizada […]

La entrada Actualización de contenido de Luna de mayo apareció primero en ZIRAN Comunicación.]]>

7 de mayo de 2026 – ¡La primavera ya está aquí, y con más sol y diversión por delante, llega la actualización de contenidos de Luna de mayo! Los días más cálidos están subiendo la temperatura y, a su vez, Luna ofrece una lista de nuevos títulos de lo más interesante, junto con una selección actualizada de juegos favoritos durante todo el mes de mayo. Juega en Luna con tu suscripción Prime: una ventaja todo en uno que ofrece ahorros, comodidad y entretenimiento. Con Luna, los miembros Prime pueden disfrutar de una biblioteca en constante evolución con más de 50 juegos en cualquier lugar donde esté disponible la entrega en la nube de Luna, además de una colección mensual rotativa de juegos descargables para PC.

Los miembros Prime pueden solicitar estas ofertas en luna.amazon.com. Para obtener información sobre la oferta de este mes, así como las últimas y mejores noticias sobre Luna, visita amazongamestudios.com y echa un vistazo a la entrada del blog de este mes aquí. Puedes descargar los materiales gráficos aquí.

Juegos de mayo de 2026: incluidos con Prime

Este mes, Luna vuelve a ofrecer varios títulos de videojuegos adicionales a los miembros Prime, que se suman a la biblioteca de más de 50 juegos disponibles a través de Luna Standard. Visita luna.amazon.com para ver la colección completa, que se actualiza periódicamente.

Disponible a partir de este mes en Luna Standard

  • Ya disponible: Guardianes de la Galaxia – Pon en marcha las botas propulsoras de Star-Lord para vivir una aventura trepidante por el cosmos en este juego de acción y aventuras en tercera persona, una nueva versión de «Guardianes de la Galaxia» de Marvel. Con los emblemáticos e impredecibles Guardianes a tu lado, ábrete paso a tiros de una situación explosiva a otra, junto a personajes originales e icónicos de Marvel enzarzados en una lucha por el destino del universo.
  • Ya disponible: Resident Evil 2 – Un virus mortal se propaga entre los habitantes de Raccoon City en septiembre de 1998, sumiendo a la ciudad en el caos. Una descarga de adrenalina sin igual, una trama apasionante y horrores inimaginables te esperan en esta nueva versión de una obra maestra que marcó un hito en el género.

Además, a partir de este mes, los jugadores podrán disfrutar de los siguientes juegos en Luna Standard:

  • Ya disponible: Alien Isolation
  • Ya disponible: Middle Earth: Shadow of War
  • Ya disponible: LEGO Star Wars III: The Clone Wars
  • Ya disponible: LEGO Star Wars: The Complete Saga

PC:
Además de los juegos mencionados anteriormente, que se pueden transmitir a través de Luna, los jugadores pueden reclamar aquí los siguientes títulos para PC disponibles durante todo el mes.
  • 7 de mayo Código de GOG – Mafia II: Definitive Edition
  • 7 de mayo Código de GOG – Fruitbus  
  • 14 de mayo Código de Amazon Games – Survival: Fountain of Youth
  • 14 de mayo Código de GOG – 60 Minutes to Extinction: Escape Room
  • 14 de mayo Epic Games Store – Lethal Honor: Order of the Apocalypse
  • 21 de mayo Código de GOG – Space Grunts
  • 21 de mayo Código de GOG – Palindrome Syndrome: Escape Room
  • 21 de mayo Código de GOG – Hot Brass
  • 28 de mayo Código de Legacy Games – Nordic Storm Solitaire
  • 28 de mayo Epic Games Store – Moon Mystery
  • 28 de mayo Código de Amazon Games – Pro Basket Ball Manager 2026

Visita esta página para conocer las últimas noticias sobre Luna y mantente al tanto de las redes sociales oficiales de Luna X, TikTok e Instagram.

La entrada Actualización de contenido de Luna de mayo apareció primero en ZIRAN Comunicación.]]>