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 / PushTokenRestController.php
PushTokenRestController.php
405 lines 12.1 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\Jetpack\Connection\Rest_Authentication;
10 use Automattic\WooCommerce\Internal\PushNotifications\DataStores\PushTokensDataStore;
11 use Automattic\WooCommerce\Internal\PushNotifications\Entities\PushToken;
12 use Automattic\WooCommerce\Internal\PushNotifications\Exceptions\PushTokenNotFoundException;
13 use Automattic\WooCommerce\Internal\PushNotifications\PushNotifications;
14 use Automattic\WooCommerce\Internal\PushNotifications\Traits\AuthorizesPushNotificationRequests;
15 use Automattic\WooCommerce\Internal\PushNotifications\Traits\ConvertsExceptionsToWpError;
16 use Automattic\WooCommerce\Internal\PushNotifications\Validators\PushTokenValidator;
17 use Automattic\WooCommerce\Internal\RestApiControllerBase;
18 use Exception;
19 use WC_Data_Exception;
20 use WP_REST_Server;
21 use WP_REST_Request;
22 use WP_REST_Response;
23 use WP_Error;
24 use WP_Http;
25
26 /**
27 * Controller for the REST endpoints associated with push notification device
28 * tokens.
29 *
30 * @since 10.6.0
31 */
32 class PushTokenRestController extends RestApiControllerBase {
33 use AuthorizesPushNotificationRequests;
34 use ConvertsExceptionsToWpError;
35
36 /**
37 * The root namespace for the JSON REST API endpoints.
38 *
39 * @var string
40 */
41 protected string $route_namespace = 'wc-push-notifications';
42
43 /**
44 * The REST base for the endpoints URL.
45 *
46 * @var string
47 */
48 protected string $rest_base = 'push-tokens';
49
50 /**
51 * Class identifier used by `woocommerce_rest_api_get_rest_namespaces`.
52 *
53 * Intentionally distinct from the URL `$route_namespace` — the filter keys
54 * one class per value here, so sharing the value with sibling controllers
55 * in the same module would overwrite them.
56 *
57 * @since 10.6.0
58 *
59 * @return string
60 */
61 protected function get_rest_api_namespace(): string {
62 return 'wc-push-notifications-push-tokens';
63 }
64
65 /**
66 * Register the REST API endpoints handled by this controller.
67 *
68 * @since 10.6.0
69 *
70 * @return void
71 */
72 public function register_routes(): void {
73 register_rest_route(
74 $this->route_namespace,
75 $this->rest_base,
76 array(
77 array(
78 'methods' => WP_REST_Server::READABLE,
79 'callback' => fn ( WP_REST_Request $request ) => $this->run( $request, 'index' ),
80 'permission_callback' => array( $this, 'authorize_as_from_wpcom' ),
81 'args' => array(
82 'page' => array(
83 'description' => __( 'Current page of the collection.', 'woocommerce' ),
84 'type' => 'integer',
85 'default' => 1,
86 'minimum' => 1,
87 'sanitize_callback' => 'absint',
88 'validate_callback' => 'rest_validate_request_arg',
89 ),
90 'per_page' => array(
91 'description' => __( 'Maximum number of items to be returned in result set.', 'woocommerce' ),
92 'type' => 'integer',
93 'default' => 10,
94 'minimum' => 1,
95 'maximum' => 100,
96 'sanitize_callback' => 'absint',
97 'validate_callback' => 'rest_validate_request_arg',
98 ),
99 ),
100 ),
101 array(
102 'methods' => WP_REST_Server::CREATABLE,
103 'callback' => fn ( WP_REST_Request $request ) => $this->run( $request, 'create' ),
104 'args' => $this->get_args( 'create' ),
105 'permission_callback' => array( $this, 'authorize_as_authenticated' ),
106 'schema' => array( $this, 'get_schema' ),
107 ),
108 )
109 );
110
111 register_rest_route(
112 $this->route_namespace,
113 $this->rest_base . '/(?P<id>[\d]+)',
114 array(
115 array(
116 'methods' => WP_REST_Server::DELETABLE,
117 'callback' => fn ( WP_REST_Request $request ) => $this->run( $request, 'delete' ),
118 'args' => $this->get_args( 'delete' ),
119 'permission_callback' => array( $this, 'authorize_as_authenticated' ),
120 'schema' => array( $this, 'get_schema' ),
121 ),
122 )
123 );
124 }
125
126 /**
127 * Returns all push tokens for roles that can receive push notifications,
128 * formatted for the WPCOM push notifications endpoint.
129 *
130 * @since 10.8.0
131 *
132 * @param WP_REST_Request $request The request object.
133 * @phpstan-param WP_REST_Request<array<string, mixed>> $request
134 * @return WP_REST_Response|WP_Error
135 */
136 public function index( WP_REST_Request $request ) {
137 $page = (int) $request->get_param( 'page' );
138 $per_page = (int) $request->get_param( 'per_page' );
139
140 try {
141 /**
142 * Paginated result from get_tokens_for_roles.
143 *
144 * @var array{tokens: PushToken[], total: int, total_pages: int} $result
145 */
146 $result = wc_get_container()
147 ->get( PushTokensDataStore::class )
148 ->get_tokens_for_roles(
149 PushNotifications::ROLES_WITH_PUSH_NOTIFICATIONS_ENABLED,
150 $page,
151 $per_page
152 );
153 } catch ( Exception $e ) {
154 return $this->convert_exception_to_wp_error( $e );
155 }
156
157 $response = new WP_REST_Response(
158 array(
159 'tokens' => array_map(
160 fn ( $token ) => $token->to_wpcom_format(),
161 $result['tokens']
162 ),
163 ),
164 WP_Http::OK
165 );
166
167 $response->header( 'X-WP-Total', (string) $result['total'] );
168 $response->header( 'X-WP-TotalPages', (string) $result['total_pages'] );
169
170 return $response;
171 }
172
173 /**
174 * Creates a push token record.
175 *
176 * @since 10.6.0
177 *
178 * @param WP_REST_Request $request The request object.
179 * @phpstan-param WP_REST_Request<array<string, mixed>> $request
180 * @return WP_REST_Response|WP_Error
181 */
182 public function create( WP_REST_Request $request ) {
183 try {
184 $data = array(
185 'user_id' => get_current_user_id(),
186 'token' => $request->get_param( 'token' ),
187 'platform' => $request->get_param( 'platform' ),
188 'device_uuid' => $request->get_param( 'device_uuid' ),
189 'origin' => $request->get_param( 'origin' ),
190 'device_locale' => $request->get_param( 'device_locale' ),
191 'metadata' => $request->get_param( 'metadata' ) ?? array(),
192 );
193
194 $data_store = wc_get_container()->get( PushTokensDataStore::class );
195 $push_token = $data_store->get_by_token_or_device_id( $data );
196
197 if ( $push_token ) {
198 $push_token->set_token( $data['token'] );
199 $push_token->set_device_uuid( $data['device_uuid'] );
200 $push_token->set_device_locale( $data['device_locale'] );
201 $push_token->set_metadata( $data['metadata'] );
202 $data_store->update( $push_token );
203 } else {
204 $push_token = $data_store->create( $data );
205 }
206 } catch ( Exception $e ) {
207 return $this->convert_exception_to_wp_error( $e );
208 }
209
210 return new WP_REST_Response(
211 array( 'id' => $push_token->get_id() ),
212 WP_Http::CREATED
213 );
214 }
215
216 /**
217 * Deletes a push token record.
218 *
219 * @since 10.6.0
220 *
221 * @param WP_REST_Request $request The request object.
222 * @phpstan-param WP_REST_Request<array<string, mixed>> $request
223 * @throws PushTokenNotFoundException If token does not belong to authenticated user.
224 * @throws WC_Data_Exception If token wasn't deleted.
225 * @return WP_REST_Response|WP_Error
226 */
227 public function delete( WP_REST_Request $request ) {
228 try {
229 $id = (int) $request->get_param( 'id' );
230 $data_store = wc_get_container()->get( PushTokensDataStore::class );
231 $push_token = $data_store->read( $id );
232
233 if ( $push_token->get_user_id() !== get_current_user_id() ) {
234 throw new PushTokenNotFoundException();
235 }
236
237 $deleted = $data_store->delete( $id );
238
239 if ( ! $deleted ) {
240 throw new WC_Data_Exception(
241 'woocommerce_push_token_not_deleted',
242 'The push token could not be deleted.',
243 WP_Http::INTERNAL_SERVER_ERROR
244 );
245 }
246 } catch ( Exception $e ) {
247 return $this->convert_exception_to_wp_error( $e );
248 }
249
250 return new WP_REST_Response( null, WP_Http::NO_CONTENT );
251 }
252
253 /**
254 * Validates the arguments from the request via PushTokenValidator.
255 *
256 * @since 10.6.0
257 *
258 * @param mixed $value The value being validated.
259 * @param WP_REST_Request $request The request object.
260 * @phpstan-param WP_REST_Request<array<string, mixed>> $request
261 * @param string $param The name of the parameter being validated.
262 * @return bool|WP_Error
263 */
264 public function validate_argument( $value, WP_REST_Request $request, string $param ) {
265 return PushTokenValidator::validate( $request->get_params(), array( $param ) );
266 }
267
268 /**
269 * Get the schema for the POST endpoint.
270 *
271 * @since 10.6.0
272 *
273 * @return array[]
274 */
275 public function get_schema(): array {
276 return array_merge(
277 $this->get_base_schema(),
278 array(
279 'title' => PushToken::POST_TYPE,
280 'properties' => array_map(
281 fn ( $item ) => array_intersect_key(
282 $item,
283 array(
284 'description' => null,
285 'type' => null,
286 'enum' => null,
287 'minimum' => null,
288 'default' => null,
289 'required' => null,
290 )
291 ),
292 $this->get_args()
293 ),
294 )
295 );
296 }
297
298 /**
299 * Validates that the request is signed with a Jetpack blog token,
300 * ensuring only WPCOM can access this endpoint.
301 *
302 * @since 10.8.0
303 *
304 * @param WP_REST_Request $request The request object.
305 * @phpstan-param WP_REST_Request<array<string, mixed>> $request
306 * @return bool|WP_Error
307 */
308 public function authorize_as_from_wpcom( WP_REST_Request $request ) {
309 if ( ! wc_get_container()->get( PushNotifications::class )->should_be_enabled() ) {
310 return false;
311 }
312
313 if (
314 class_exists( Rest_Authentication::class )
315 && Rest_Authentication::is_signed_with_blog_token()
316 ) {
317 return true;
318 }
319
320 return new WP_Error(
321 'woocommerce_rest_cannot_view',
322 __( 'Sorry, you are not allowed to do that.', 'woocommerce' ),
323 array( 'status' => rest_authorization_required_code() )
324 );
325 }
326
327 /**
328 * Get the accepted arguments for the POST request.
329 *
330 * @since 10.6.0
331 *
332 * @param string $context The context to return args for.
333 * @return array
334 */
335 private function get_args( ?string $context = null ): array {
336 $args = array(
337 'id' => array(
338 'description' => __( 'Push Token ID', 'woocommerce' ),
339 'type' => 'integer',
340 'required' => true,
341 'context' => array( 'delete' ),
342 'minimum' => 1,
343 'sanitize_callback' => 'absint',
344 'validate_callback' => array( $this, 'validate_argument' ),
345 ),
346 'origin' => array(
347 'description' => __( 'Origin', 'woocommerce' ),
348 'type' => 'string',
349 'required' => true,
350 'context' => array( 'create' ),
351 'enum' => PushToken::ORIGINS,
352 'validate_callback' => array( $this, 'validate_argument' ),
353 ),
354 'device_uuid' => array(
355 'description' => __( 'Device UUID', 'woocommerce' ),
356 'default' => '',
357 'type' => 'string',
358 'context' => array( 'create' ),
359 'validate_callback' => array( $this, 'validate_argument' ),
360 'sanitize_callback' => 'sanitize_text_field',
361 ),
362 'device_locale' => array(
363 'description' => __( 'Device Locale', 'woocommerce' ),
364 'type' => 'string',
365 'required' => true,
366 'context' => array( 'create' ),
367 'validate_callback' => array( $this, 'validate_argument' ),
368 'sanitize_callback' => 'sanitize_text_field',
369 ),
370 'platform' => array(
371 'description' => __( 'Platform', 'woocommerce' ),
372 'type' => 'string',
373 'required' => true,
374 'context' => array( 'create' ),
375 'enum' => PushToken::PLATFORMS,
376 'validate_callback' => array( $this, 'validate_argument' ),
377 ),
378 'token' => array(
379 'description' => __( 'Push Token', 'woocommerce' ),
380 'type' => 'string',
381 'required' => true,
382 'context' => array( 'create' ),
383 'validate_callback' => array( $this, 'validate_argument' ),
384 'sanitize_callback' => 'wp_unslash',
385 ),
386 'metadata' => array(
387 'description' => __( 'Metadata', 'woocommerce' ),
388 'type' => 'object',
389 'context' => array( 'create' ),
390 'validate_callback' => array( $this, 'validate_argument' ),
391 'sanitize_callback' => 'wp_unslash',
392 ),
393 );
394
395 if ( $context ) {
396 $args = array_filter(
397 $args,
398 fn ( $arg ) => in_array( $context, $arg['context'], true )
399 );
400 }
401
402 return $args;
403 }
404 }
405