PluginProbe
WooCommerce / 11.1.0-rc.2
WooCommerce v11.1.0-rc.2
11.1.0 11.1.0-rc.2 11.1.0-rc.1 11.1.0-beta.2 11.1.0-beta.1 11.0.1 11.0.0 11.0.0-rc.3 11.0.0-rc.2 11.0.0-rc.1 11.0.0-beta.2 11.0.0-beta.1 10.9.4 10.9.3 10.9.2 10.9.1 10.9.0 10.9.0-rc.1 10.9.0-beta.2 10.9.0-beta.1 10.8.1 10.8.0 10.8.0-rc.1 10.8.0-beta.2 10.8.0-beta.1 All 648 releases
woocommerce / src / Internal / PushNotifications / Controllers / PushNotificationRestController.php
PushNotificationRestController.php
161 lines 4.2 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 declare( strict_types = 1 );
4
5 namespace Automattic\WooCommerce\Internal\PushNotifications\Controllers;
6
7 defined( 'ABSPATH' ) || exit;
8
9 use Automattic\WooCommerce\Internal\PushNotifications\Notifications\Notification;
10 use Automattic\WooCommerce\Internal\PushNotifications\PushNotifications;
11 use Automattic\WooCommerce\Internal\PushNotifications\Services\NotificationProcessor;
12 use Automattic\WooCommerce\StoreApi\Utilities\JsonWebToken;
13 use Exception;
14 use WP_Error;
15 use WP_Http;
16 use WP_REST_Request;
17 use WP_REST_Response;
18 use WP_REST_Server;
19
20 /**
21 * REST controller for the internal loopback send endpoint.
22 *
23 * Receives JWT-signed notification data from InternalNotificationDispatcher
24 * and delegates each notification to NotificationProcessor.
25 *
26 * @since 10.7.0
27 */
28 class PushNotificationRestController {
29
30 /**
31 * The route namespace, shared with PushTokenRestController.
32 */
33 const ROUTE_NAMESPACE = 'wc-push-notifications';
34
35 /**
36 * Registers the REST API route on the rest_api_init hook.
37 *
38 * @return void
39 *
40 * @since 10.7.0
41 */
42 public function register(): void {
43 add_action( 'rest_api_init', array( $this, 'register_routes' ) );
44 }
45
46 /**
47 * Registers the send route.
48 *
49 * @return void
50 *
51 * @since 10.7.0
52 */
53 public function register_routes(): void {
54 register_rest_route(
55 self::ROUTE_NAMESPACE,
56 'send',
57 array(
58 'methods' => WP_REST_Server::CREATABLE,
59 'callback' => array( $this, 'create' ),
60 'permission_callback' => array( $this, 'authorize' ),
61 )
62 );
63 }
64
65 /**
66 * Processes the send request by delegating each notification to the
67 * processor.
68 *
69 * @param WP_REST_Request $request The request object.
70 * @phpstan-param WP_REST_Request<array<string, mixed>> $request
71 * @return WP_REST_Response|WP_Error
72 *
73 * @since 10.7.0
74 */
75 public function create( WP_REST_Request $request ) {
76 wc_set_time_limit( 30 );
77
78 $body = json_decode( $request->get_body(), true );
79 $notifications = is_array( $body ) ? ( $body['notifications'] ?? array() ) : array();
80 $success_response = new WP_REST_Response( array( 'success' => true ), WP_Http::OK );
81
82 if ( empty( $notifications ) || ! is_array( $notifications ) ) {
83 wc_get_logger()->warning(
84 'Loopback endpoint received empty or missing notifications array.',
85 array( 'source' => PushNotifications::FEATURE_NAME )
86 );
87
88 return $success_response;
89 }
90
91 $processor = wc_get_container()->get( NotificationProcessor::class );
92
93 foreach ( $notifications as $data ) {
94 try {
95 $notification = Notification::from_array( $data );
96 $processor->process( $notification );
97 } catch ( Exception $e ) {
98 wc_get_logger()->error(
99 sprintf( 'Failed to process notification: %s', $e->getMessage() ),
100 array( 'source' => PushNotifications::FEATURE_NAME )
101 );
102 }
103 }
104
105 return $success_response;
106 }
107
108 /**
109 * Validates the JWT from the Authorization header.
110 *
111 * @param WP_REST_Request $request The request object.
112 * @phpstan-param WP_REST_Request<array<string, mixed>> $request
113 * @return true|WP_Error
114 *
115 * @since 10.7.0
116 */
117 public function authorize( WP_REST_Request $request ) {
118 $header = trim( (string) $request->get_header( 'authorization' ) );
119
120 if ( empty( $header ) ) {
121 return new WP_Error(
122 'woocommerce_rest_unauthorized',
123 'Missing authorization header.',
124 array( 'status' => WP_Http::UNAUTHORIZED )
125 );
126 }
127
128 $token = strncasecmp( $header, 'Bearer ', 7 ) === 0 ? substr( $header, 7 ) : $header;
129
130 if ( ! JsonWebToken::validate( $token, wp_salt( 'auth' ) ) ) {
131 return new WP_Error(
132 'woocommerce_rest_unauthorized',
133 'Invalid or expired token.',
134 array( 'status' => WP_Http::UNAUTHORIZED )
135 );
136 }
137
138 $parts = JsonWebToken::get_parts( $token );
139
140 if ( ! isset( $parts->payload->iss ) || get_site_url() !== $parts->payload->iss ) {
141 return new WP_Error(
142 'woocommerce_rest_unauthorized',
143 'Invalid token issuer.',
144 array( 'status' => WP_Http::UNAUTHORIZED )
145 );
146 }
147
148 $body_hash = hash( 'sha256', $request->get_body() );
149
150 if ( ! isset( $parts->payload->body_hash ) || ! hash_equals( (string) $parts->payload->body_hash, $body_hash ) ) {
151 return new WP_Error(
152 'woocommerce_rest_unauthorized',
153 'Body hash mismatch.',
154 array( 'status' => WP_Http::UNAUTHORIZED )
155 );
156 }
157
158 return true;
159 }
160 }
161