PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-beta
Jetpack – WP Security, Backup, Speed, & Growth v16.3-beta
16.3-beta 16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 All 507 releases
jetpack / jetpack_vendor / automattic / jetpack-publicize / src / rest-api / class-scheduled-actions-controller.php

class-scheduled-actions-controller.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-beta, at jetpack_vendor/automattic/jetpack-publicize/src/rest-api/class-scheduled-actions-controller.php

679 lines 19.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * The Publicize Scheduled Actions Controller class.
4 *
5 * @package automattic/jetpack-publicize
6 */
7
8 namespace Automattic\Jetpack\Publicize\REST_API;
9
10 use Automattic\Jetpack\Connection\Traits\WPCOM_REST_API_Proxy_Request;
11 use Automattic\Jetpack\Publicize\Connections;
12 use Automattic\Jetpack\Publicize\Publicize_Utils as Utils;
13 use WP_Error;
14 use WP_REST_Request;
15 use WP_REST_Response;
16 use WP_REST_Server;
17
18 if ( ! defined( 'ABSPATH' ) ) {
19 exit( 0 );
20 }
21
22 /**
23 * Scheduled Actions Controller class.
24 *
25 * @phan-constructor-used-for-side-effects
26 */
27 class Scheduled_Actions_Controller extends Base_Controller {
28
29 use WPCOM_REST_API_Proxy_Request;
30
31 /**
32 * Constructor.
33 */
34 public function __construct() {
35 parent::__construct();
36
37 $this->base_api_path = 'wpcom';
38 $this->version = 'v2';
39
40 $this->namespace = "{$this->base_api_path}/{$this->version}";
41 $this->rest_base = 'publicize/scheduled-actions';
42
43 add_action( 'rest_api_init', array( $this, 'register_routes' ) );
44 }
45
46 /**
47 * Register the routes.
48 */
49 public function register_routes() {
50 register_rest_route(
51 $this->namespace,
52 '/' . $this->rest_base,
53 array(
54 array(
55 'methods' => WP_REST_Server::READABLE,
56 'callback' => array( $this, 'get_items' ),
57 'permission_callback' => array( $this, 'get_items_permissions_check' ),
58 'args' => array(
59 'post_id' => array(
60 'type' => 'integer',
61 'description' => __( 'The post ID to filter the items by.', 'jetpack-publicize-pkg' ),
62 ),
63 ),
64 ),
65 array(
66 'methods' => WP_REST_Server::CREATABLE,
67 'callback' => array( $this, 'create_item' ),
68 'permission_callback' => array( $this, 'create_item_permissions_check' ),
69 'args' => array(
70 'post_id' => array(
71 'type' => 'integer',
72 'required' => true,
73 ),
74 'connection_id' => array(
75 'type' => 'integer',
76 'required' => true,
77 ),
78 'message' => array(
79 'type' => 'string',
80 ),
81 'share_date' => array(
82 'type' => 'integer',
83 'description' => sprintf(
84 /* translators: %s is the new field name */
85 __( 'Deprecated in favor of %s.', 'jetpack-publicize-pkg' ),
86 'timestamp'
87 ),
88 ),
89 'timestamp' => array(
90 'type' => 'integer',
91 'description' => __( 'GMT/UTC Unix timestamp in seconds for the action.', 'jetpack-publicize-pkg' ),
92 ),
93 ),
94 ),
95 'schema' => array( $this, 'get_public_item_schema' ),
96 )
97 );
98
99 register_rest_route(
100 $this->namespace,
101 '/' . $this->rest_base . '/(?P<action_id>\d+)',
102 array(
103 array(
104 'methods' => WP_REST_Server::READABLE,
105 'callback' => array( $this, 'get_item' ),
106 'permission_callback' => array( $this, 'get_item_permissions_check' ),
107 ),
108 array(
109 'methods' => WP_REST_Server::EDITABLE,
110 'callback' => array( $this, 'update_item' ),
111 'permission_callback' => array( $this, 'update_item_permissions_check' ),
112 'args' => array(
113 'message' => array( 'type' => 'string' ),
114 'share_date' => array(
115 'type' => 'integer',
116 'description' => sprintf(
117 /* translators: %s is the new field name */
118 __( 'Deprecated in favor of %s.', 'jetpack-publicize-pkg' ),
119 'timestamp'
120 ),
121 ),
122 'timestamp' => array(
123 'type' => 'integer',
124 'description' => __( 'GMT/UTC Unix timestamp in seconds for the action.', 'jetpack-publicize-pkg' ),
125 ),
126 ),
127 ),
128 array(
129 'methods' => WP_REST_Server::DELETABLE,
130 'callback' => array( $this, 'delete_item' ),
131 'permission_callback' => array( $this, 'delete_item_permissions_check' ),
132 ),
133 'schema' => array( $this, 'get_public_item_schema' ),
134 )
135 );
136 }
137
138 /**
139 * Schema for the endpoint.
140 *
141 * @return array
142 */
143 public function get_item_schema() {
144 $schema = array(
145 '$schema' => 'http://json-schema.org/draft-04/schema#',
146 'title' => 'publicize-scheduled-action',
147 'type' => 'object',
148 'properties' => array(
149 'blog_id' => array(
150 'type' => 'integer',
151 'description' => __( 'The blog ID that the action belongs to.', 'jetpack-publicize-pkg' ),
152 ),
153 'connection_id' => array(
154 'type' => 'integer',
155 'description' => __( 'The publicize connection ID that the action belongs to.', 'jetpack-publicize-pkg' ),
156 ),
157 'id' => array(
158 'type' => 'integer',
159 'description' => __( 'Action identifier.', 'jetpack-publicize-pkg' ),
160 ),
161 'ID' => array(
162 'type' => 'integer',
163 'description' => __( 'Action identifier.', 'jetpack-publicize-pkg' ) . ' ' . sprintf(
164 /* translators: %s is the new field name */
165 __( 'Deprecated in favor of %s.', 'jetpack-publicize-pkg' ),
166 'id'
167 ),
168 ),
169 'message' => array(
170 'type' => 'string',
171 'description' => __( 'The result of the action.', 'jetpack-publicize-pkg' ),
172 ),
173 'post_id' => array(
174 'type' => 'integer',
175 'description' => __( 'The post ID that the action belongs to.', 'jetpack-publicize-pkg' ),
176 ),
177 'share_date' => array(
178 'type' => 'string',
179 'description' => __( 'ISO 8601 formatted date for the action.', 'jetpack-publicize-pkg' ) . ' ' . sprintf(
180 /* translators: %s is the new field name */
181 __( 'Deprecated in favor of %s.', 'jetpack-publicize-pkg' ),
182 'timestamp'
183 ),
184 ),
185 'timestamp' => array(
186 'type' => 'integer',
187 'description' => __( 'GMT/UTC Unix timestamp in seconds for the action.', 'jetpack-publicize-pkg' ),
188 ),
189 'wpcom_user_id' => array(
190 'type' => 'integer',
191 'description' => __( 'wordpress.com ID of the user who created the action.', 'jetpack-publicize-pkg' ),
192 ),
193 ),
194 );
195
196 return $this->add_additional_fields_schema( $schema );
197 }
198
199 /**
200 * Check if the user has the basic permissions to access the Publicize scheduled actions.
201 *
202 * @return bool|WP_Error
203 */
204 public function basic_permissions_check() {
205 if ( ! current_user_can( 'edit_posts' ) ) {
206 return false;
207 }
208 return $this->publicize_permissions_check();
209 }
210
211 /**
212 * Check if the user has the basic permissions
213 * required to perform CRUD operations on an item related to a post
214 *
215 * @param int $post_id The post ID.
216 * @return bool|WP_Error
217 */
218 public function basic_post_permissions_check( $post_id ) {
219
220 if ( ! get_post( $post_id ) ) {
221 return new WP_Error(
222 'post_not_found',
223 __( 'Post not found.', 'jetpack-publicize-pkg' ),
224 array( 'status' => 400 )
225 );
226 }
227
228 // Ensure that the user can edit the post.
229 if ( ! current_user_can( 'edit_post', $post_id ) ) {
230 return new WP_Error(
231 'rest_forbidden',
232 __( 'Sorry, you are not allowed to view or scheduled shares for that post.', 'jetpack-publicize-pkg' ),
233 array( 'status' => 403 )
234 );
235 }
236
237 return true;
238 }
239
240 /**
241 * Verify that the request has access to connectoins list.
242 *
243 * @param WP_REST_Request $request Full details about the request.
244 * @return bool|WP_Error
245 */
246 public function get_items_permissions_check( $request ) {
247 $basic_permissions = $this->basic_permissions_check();
248
249 if ( is_wp_error( $basic_permissions ) || ! $basic_permissions ) {
250 return $basic_permissions;
251 }
252
253 $post_id = $request->get_param( 'post_id' );
254
255 /**
256 * The post_id is optional only for editors and above.
257 * It means that authors can view the scheduled shares
258 * only for the post they can edit but
259 * cannot view all the scheduled shares for the site.
260 */
261 if ( ! $post_id && ! current_user_can( 'edit_others_posts' ) ) {
262 return new WP_Error(
263 'rest_forbidden',
264 __( 'You must pass a post ID to list scheduled shares.', 'jetpack-publicize-pkg' ),
265 array( 'status' => rest_authorization_required_code() )
266 );
267 }
268
269 if ( $post_id ) {
270 return $this->basic_post_permissions_check( $post_id );
271 }
272
273 return true;
274 }
275
276 /**
277 * Get list of Publicize scheduled actions
278 *
279 * @param WP_REST_Request $request Full details about the request.
280 *
281 * @return WP_REST_Response|WP_Error The response
282 */
283 public function get_items( $request ) {
284
285 if ( Utils::is_wpcom() ) {
286 $post_id = $request->get_param( 'post_id' );
287
288 require_lib( 'publicize/class.publicize-actions' );
289
290 if ( $post_id ) {
291 $scheduled_actions = \Publicize_Actions::get_scheduled_actions_by_blog_and_post_id(
292 get_current_blog_id(),
293 $post_id
294 );
295 } else {
296 $scheduled_actions = \Publicize_Actions::get_scheduled_actions_by_blog_id(
297 get_current_blog_id()
298 );
299 }
300
301 if ( is_wp_error( $scheduled_actions ) ) {
302 return $scheduled_actions;
303 }
304
305 return rest_ensure_response(
306 $this->prepare_items_for_response( $scheduled_actions, $request )
307 );
308 }
309
310 return rest_ensure_response(
311 $this->proxy_request_to_wpcom_as_user( $request )
312 );
313 }
314
315 /**
316 * Checks if a given request has access to create a connection.
317 *
318 * @param WP_REST_Request $request Full details about the request.
319 * @return bool|WP_Error True if the request has access to create items, WP_Error object otherwise.
320 */
321 public function create_item_permissions_check( $request ) {
322 $basic_permissions = $this->basic_permissions_check();
323
324 if ( is_wp_error( $basic_permissions ) || ! $basic_permissions ) {
325 return $basic_permissions;
326 }
327
328 $post_id = $request->get_param( 'post_id' );
329
330 $basic_post_permissions = $this->basic_post_permissions_check( $post_id );
331
332 if ( is_wp_error( $basic_post_permissions ) || ! $basic_post_permissions ) {
333 return $basic_post_permissions;
334 }
335
336 $post = get_post( $post_id );
337
338 // Ensure that the post is published.
339 if ( 'publish' !== $post->post_status ) {
340 return new WP_Error(
341 'post_not_published',
342 __( 'The post must be published to schedule it for sharing.', 'jetpack-publicize-pkg' ),
343 array( 'status' => 400 )
344 );
345 }
346
347 /**
348 * We need to validate the passed connection_id
349 * to ensure that it's valid and the user has access to the connection.
350 */
351 $connection = Connections::get_by_id( (string) $request->get_param( 'connection_id' ) );
352
353 if ( ! $connection ) {
354 return new WP_Error(
355 'connection_not_found',
356 __( 'That connection does not exist.', 'jetpack-publicize-pkg' ),
357 array( 'status' => 400 )
358 );
359 }
360
361 if ( current_user_can( 'edit_others_posts' ) ) {
362 return true;
363 }
364
365 /**
366 * If the user is not an editor or above, they can create
367 * actions only for the connections they have access to.
368 * So, we need to check if the user has access to the connection
369 * that they are trying to use to create the action.
370 */
371 if ( ! Connections::is_shared( $connection ) && ! Connections::user_owns_connection( $connection ) ) {
372 return new WP_Error(
373 'rest_forbidden',
374 __( 'Sorry, you cannot schedule shares to that connection.', 'jetpack-publicize-pkg' ),
375 array( 'status' => 403 )
376 );
377 }
378
379 return true;
380 }
381
382 /**
383 * Creates a new action.
384 *
385 * @param WP_REST_Request $request Full details about the request.
386 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
387 */
388 public function create_item( $request ) {
389
390 if ( Utils::is_wpcom() ) {
391 require_lib( 'publicize/class.publicize-actions' );
392
393 $post_id = $request['post_id'];
394 $blog_id = get_current_blog_id();
395 $user_id = get_current_user_id();
396 $connection_id = (int) $request['connection_id'];
397 $message = sanitize_textarea_field( $request['message'] ?? '' );
398
399 $timestamp = time();
400 if ( ! empty( $request['timestamp'] ) ) {
401 $timestamp = (int) $request['timestamp'];
402 } elseif ( ! empty( $request['share_date'] ) ) { // Fallback for deprecated field.
403 $timestamp = $request['share_date']; // Calypso sends this as timestamp.
404 }
405
406 $action = array(
407 'post_id' => $post_id,
408 'blog_id' => $blog_id,
409 'user_id' => $user_id,
410 'connection_id' => $connection_id,
411 'message' => $message,
412 'scheduled_datetime' => $this->format_date_for_db( $timestamp ),
413 );
414
415 $action_id = \Publicize_Actions::add_scheduled_action( $action );
416 if ( is_wp_error( $action_id ) ) {
417 return $action_id;
418 }
419 $action['publicize_scheduled_action_id'] = $action_id;
420
421 $response = rest_ensure_response(
422 $this->prepare_action_for_response( $action )
423 );
424
425 $response->set_status( 201 );
426
427 return $response;
428 }
429
430 return rest_ensure_response(
431 $this->proxy_request_to_wpcom_as_user( $request )
432 );
433 }
434
435 /**
436 * Checks if a given request has access to read an action.
437 *
438 * @param WP_REST_Request $request Full details about the request.
439 * @return bool|WP_Error True if the request has read access for the item, WP_Error object or false otherwise.
440 */
441 public function get_item_permissions_check( $request ) {
442 $basic_permissions = $this->basic_permissions_check();
443
444 if ( is_wp_error( $basic_permissions ) || ! $basic_permissions ) {
445 return $basic_permissions;
446 }
447
448 if ( ! Utils::is_wpcom() ) {
449 // On Jetpack sites, we need to just check for basic permissions.
450 return true;
451 }
452
453 $action = $this->wpcom_get_action( $request['action_id'] );
454
455 if ( is_wp_error( $action ) ) {
456 return $action;
457 }
458
459 // Ensure that the action is for the current blog.
460 if ( get_current_blog_id() !== $action['blog_id'] ) {
461 return false;
462 }
463
464 return $this->basic_post_permissions_check( $action['post_id'] );
465 }
466
467 /**
468 * Retrieves a single action.
469 *
470 * @param WP_REST_Request $request Full details about the request.
471 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
472 */
473 public function get_item( $request ) {
474 $action_id = $request['action_id'];
475
476 if ( Utils::is_wpcom() ) {
477
478 return rest_ensure_response(
479 $this->wpcom_get_action( $action_id )
480 );
481 }
482
483 return rest_ensure_response(
484 $this->proxy_request_to_wpcom_as_user( $request, $action_id )
485 );
486 }
487
488 /**
489 * Checks if a given request has access to update an action.
490 *
491 * @param WP_REST_Request $request Full details about the request.
492 * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
493 */
494 public function update_item_permissions_check( $request ) {
495 // If a user can view an item, they can update it.
496 return $this->get_item_permissions_check( $request );
497 }
498
499 /**
500 * Update an action.
501 *
502 * @param WP_REST_Request $request Full details about the request.
503 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
504 */
505 public function update_item( $request ) {
506
507 $action_id = $request['action_id'];
508
509 if ( Utils::is_wpcom() ) {
510 require_lib( 'publicize/class.publicize-actions' );
511
512 $action = $this->wpcom_get_action( $action_id );
513
514 if ( is_wp_error( $action ) ) {
515 return $action;
516 }
517 $action['message'] = ! empty( $request['message'] ) ? sanitize_textarea_field( $request['message'] ) : $action['message'];
518
519 // Retain the original timestamp by default.
520 $timestamp = $action['timestamp'];
521 if ( ! empty( $request['timestamp'] ) ) {
522 $timestamp = (int) $request['timestamp'];
523 } elseif ( ! empty( $request['share_date'] ) ) { // Fallback for deprecated field.
524 $timestamp = strtotime( $request['share_date'] );
525 }
526 $action['scheduled_datetime'] = $this->format_date_for_db( $timestamp );
527
528 $action['publicize_scheduled_action_id'] = $action['id'];
529
530 $save_result = \Publicize_Actions::edit_scheduled_action( $action['id'], $action );
531 if ( is_wp_error( $save_result ) ) {
532 return $save_result;
533 }
534 return rest_ensure_response(
535 $this->prepare_action_for_response( $action )
536 );
537 }
538
539 return rest_ensure_response(
540 $this->proxy_request_to_wpcom_as_user( $request, $action_id )
541 );
542 }
543
544 /**
545 * Checks if a given request has access to delete an action.
546 *
547 * @param WP_REST_Request $request Full details about the request.
548 * @return true|WP_Error True if the request has access to create items, WP_Error object otherwise.
549 */
550 public function delete_item_permissions_check( $request ) {
551 // If a user can update an item, they can delete it.
552 return $this->update_item_permissions_check( $request );
553 }
554
555 /**
556 * Delete an action.
557 *
558 * @param WP_REST_Request $request Full details about the request.
559 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
560 */
561 public function delete_item( $request ) {
562
563 $action_id = $request['action_id'];
564
565 if ( Utils::is_wpcom() ) {
566 require_lib( 'publicize/class.publicize-actions' );
567
568 $action = $this->wpcom_get_action( $action_id );
569 if ( is_wp_error( $action ) ) {
570 return $action;
571 }
572 $delete_result = \Publicize_Actions::delete_scheduled_action(
573 $action['id'],
574 $action['blog_id']
575 );
576 if ( is_wp_error( $delete_result ) ) {
577 return $delete_result;
578 }
579 return rest_ensure_response( true );
580 }
581
582 return rest_ensure_response(
583 $this->proxy_request_to_wpcom_as_user( $request, $action_id )
584 );
585 }
586
587 /**
588 * Filters out data based on ?_fields= request parameter
589 *
590 * @param array $items Items to prepare.
591 * @param WP_REST_Request $request Full details about the request.
592 *
593 * @return array Items.
594 */
595 public function prepare_items_for_response( $items, $request ) {
596
597 $output = array();
598
599 foreach ( $items as $raw_item ) {
600
601 $item = $this->prepare_action_for_response( $raw_item );
602
603 $data = $this->prepare_item_for_response( $item, $request );
604
605 $output[] = $this->prepare_response_for_collection( $data );
606 }
607
608 return $output;
609 }
610
611 /**
612 * Prepare a single action for response, setting the correct field names.
613 *
614 * @param array $raw_action Raw action.
615 *
616 * @return array Items.
617 */
618 public function prepare_action_for_response( $raw_action ) {
619
620 return array(
621 'blog_id' => (int) $raw_action['blog_id'],
622 'connection_id' => (int) $raw_action['connection_id'],
623 'id' => (int) $raw_action['publicize_scheduled_action_id'],
624 'ID' => (int) $raw_action['publicize_scheduled_action_id'],
625 'message' => (string) $raw_action['message'],
626 'post_id' => (int) $raw_action['post_id'],
627 'share_date' => (string) $this->format_date_for_output( $raw_action['scheduled_datetime'] ),
628 'timestamp' => strtotime( $raw_action['scheduled_datetime'] ),
629 'wpcom_user_id' => (int) $raw_action['user_id'],
630 );
631 }
632
633 /**
634 * Return a formatted action by action_id
635 *
636 * @param int $action_id The action ID.
637 * @return WP_Error|array The action
638 */
639 private function wpcom_get_action( $action_id ) {
640 // Ensure that we are on WPCOM.
641 Utils::assert_is_wpcom( __METHOD__ );
642
643 require_lib( 'publicize/class.publicize-actions' );
644 $action = \Publicize_Actions::get_scheduled_action( $action_id );
645 if ( is_wp_error( $action ) ) {
646 return $action;
647 }
648 if ( ! isset( $action['publicize_scheduled_action_id'] ) ) {
649 return new WP_Error( 'not_found', __( 'Could not find that scheduled action.', 'jetpack-publicize-pkg' ), array( 'status' => 404 ) );
650 }
651
652 return $this->prepare_action_for_response( $action );
653 }
654
655 /**
656 * Returns ISO 8601 formatted datetime: 2011-12-08T01:15:36-08:00
657 *
658 * @param string $date_gmt GMT datetime string.
659 * @return string
660 */
661 private function format_date_for_output( $date_gmt ) {
662 // phpcs:ignore WordPress.DateTime.RestrictedFunctions.date_date
663 return date( 'c', strtotime( $date_gmt ) );
664 }
665
666 /**
667 * Returns SQL formatted datetime from unix timestamp
668 *
669 * @param int $timestamp The timestamp.
670 *
671 * @return string
672 */
673 private function format_date_for_db( $timestamp ) {
674 // Round down to the nearest minute.
675 $floored_timestamp = $timestamp - $timestamp % 60;
676 return gmdate( 'Y-m-d H:i:s', $floored_timestamp );
677 }
678 }
679