Create New Item
Item Type
File
Folder
Item Name
Search file in folder and subfolders...
Are you sure want to rename?
File Manager
/
wp-content
/
plugins
/
woocommerce
/
src
/
Internal
/
PushNotifications
/
Controllers
:
PushNotificationRestController.php
Advanced Search
Upload
New Item
Settings
Back
Back Up
Advanced Editor
Save
<?php declare( strict_types = 1 ); namespace Automattic\WooCommerce\Internal\PushNotifications\Controllers; defined( 'ABSPATH' ) || exit; use Automattic\WooCommerce\Internal\PushNotifications\Dispatchers\InternalNotificationDispatcher; use Automattic\WooCommerce\Internal\PushNotifications\Notifications\Notification; use Automattic\WooCommerce\Internal\PushNotifications\PushNotifications; use Automattic\WooCommerce\Internal\PushNotifications\Services\NotificationProcessor; use Automattic\WooCommerce\StoreApi\Utilities\JsonWebToken; use Exception; use WP_Error; use WP_Http; use WP_REST_Request; use WP_REST_Response; use WP_REST_Server; /** * REST controller for the internal loopback send endpoint. * * Receives JWT-signed notification data from InternalNotificationDispatcher * and delegates each notification to NotificationProcessor. * * @since 10.7.0 */ class PushNotificationRestController { /** * The route namespace, shared with PushTokenRestController. */ const ROUTE_NAMESPACE = 'wc-push-notifications'; /** * Registers the REST API route on the rest_api_init hook. * * @return void * * @since 10.7.0 */ public function register(): void { add_action( 'rest_api_init', array( $this, 'register_routes' ) ); } /** * Registers the send route. * * @return void * * @since 10.7.0 */ public function register_routes(): void { register_rest_route( self::ROUTE_NAMESPACE, 'send', array( 'methods' => WP_REST_Server::CREATABLE, 'callback' => array( $this, 'create' ), 'permission_callback' => array( $this, 'authorize' ), ) ); } /** * Processes the send request by delegating each notification to the * processor. * * @param WP_REST_Request $request The request object. * @phpstan-param WP_REST_Request<array<string, mixed>> $request * @return WP_REST_Response|WP_Error * * @since 10.7.0 */ public function create( WP_REST_Request $request ) { wc_set_time_limit( 30 ); $body = json_decode( $request->get_body(), true ); $notifications = is_array( $body ) ? ( $body['notifications'] ?? array() ) : array(); $success_response = new WP_REST_Response( array( 'success' => true ), WP_Http::OK ); if ( empty( $notifications ) || ! is_array( $notifications ) ) { wc_get_logger()->warning( 'Loopback endpoint received empty or missing notifications array.', array( 'source' => PushNotifications::FEATURE_NAME ) ); return $success_response; } $processor = wc_get_container()->get( NotificationProcessor::class ); foreach ( $notifications as $data ) { try { $notification = Notification::from_array( $data ); $processor->process( $notification ); } catch ( Exception $e ) { wc_get_logger()->error( sprintf( 'Failed to process notification: %s', $e->getMessage() ), array( 'source' => PushNotifications::FEATURE_NAME ) ); } } return $success_response; } /** * Validates the JWT from the Authorization header, falling back to the * token query parameter on hosts that strip the header before it reaches * PHP (see {@see InternalNotificationDispatcher::TOKEN_QUERY_PARAM}). * * @param WP_REST_Request $request The request object. * @phpstan-param WP_REST_Request<array<string, mixed>> $request * @return true|WP_Error * * @since 10.7.0 */ public function authorize( WP_REST_Request $request ) { $header = trim( (string) $request->get_header( 'authorization' ) ); if ( '' !== $header ) { $token = strncasecmp( $header, 'Bearer ', 7 ) === 0 ? substr( $header, 7 ) : $header; } else { $token = $this->get_token_from_query( $request ); } if ( '' === $token ) { return new WP_Error( 'woocommerce_rest_unauthorized', 'Missing credential: no Authorization header and no token query parameter.', array( 'status' => WP_Http::UNAUTHORIZED ) ); } if ( ! JsonWebToken::validate( $token, wp_salt( 'auth' ) ) ) { return new WP_Error( 'woocommerce_rest_unauthorized', 'Invalid or expired token.', array( 'status' => WP_Http::UNAUTHORIZED ) ); } $parts = JsonWebToken::get_parts( $token ); if ( ! isset( $parts->payload->iss ) || get_site_url() !== $parts->payload->iss ) { return new WP_Error( 'woocommerce_rest_unauthorized', 'Invalid token issuer.', array( 'status' => WP_Http::UNAUTHORIZED ) ); } $body_hash = hash( 'sha256', $request->get_body() ); if ( ! isset( $parts->payload->body_hash ) || ! hash_equals( (string) $parts->payload->body_hash, $body_hash ) ) { return new WP_Error( 'woocommerce_rest_unauthorized', 'Body hash mismatch.', array( 'status' => WP_Http::UNAUTHORIZED ) ); } return true; } /** * Reads the credential from the query string. * * Reads the query parameters directly rather than through * {@see WP_REST_Request::get_param()}, which searches the JSON body and the * POST body first and is reorderable by the `rest_request_parameter_order` * filter. The credential is sent in the URL, so that is the only place it * should be read from. * * @param WP_REST_Request $request The request object. * @phpstan-param WP_REST_Request<array<string, mixed>> $request * @return string The token, or an empty string when absent or not a string. */ private function get_token_from_query( WP_REST_Request $request ): string { $params = $request->get_query_params(); $token = $params[ InternalNotificationDispatcher::TOKEN_QUERY_PARAM ] ?? ''; return is_string( $token ) ? trim( $token ) : ''; } }