PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3
Jetpack – WP Security, Backup, Speed, & Growth v16.3
16.3 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 All 508 releases
← All changes | jetpack_vendor/automattic/jetpack-blaze/src/class-dashboard-rest-controller.php +939 -42 13.0.2 → 16.3 View file →
@@ -10,14 +10,17 @@
10 10
11 11 use Automattic\Jetpack\Connection\Client;
12 12 use Automattic\Jetpack\Connection\Manager as Connection_Manager;
13 13 use Automattic\Jetpack\Status\Host;
14 +use Automattic\Jetpack\Sync\Health;
15 +use WC_Product;
14 16 use WP_Error;
17 +use WP_REST_Request;
15 18 use WP_REST_Server;
16 19
17 20 /**
18 21 * Registers the REST routes for Blaze Dashboard.
19 - * It bascially forwards the requests to the WordPress.com REST API.
22 + * It basically forwards the requests to the WordPress.com REST API.
20 23 */
21 24 class Dashboard_REST_Controller {
22 25 /**
23 26 * Namespace for the REST API.
@@ -26,8 +29,37 @@
26 29 */
27 30 public static $namespace = 'jetpack/v4/blaze-app';
28 31
29 32 /**
33 + * Connection manager object.
34 + *
35 + * @var \Automattic\Jetpack\Connection\Manager
36 + */
37 + private $connection;
38 +
39 + /**
40 + * Creates the Dashboard_REST_Controller object.
41 + *
42 + * @param \Automattic\Jetpack\Connection\Manager $connection The connection manager object.
43 + */
44 + public function __construct( $connection = null ) {
45 + $this->connection = $connection ?? new Connection_Manager();
46 + }
47 +
48 + /**
49 + * Registers the REST routes on the `rest_api_init` hook.
50 + *
51 + * Instantiated here, rather than eagerly, so the controller class only loads
52 + * on requests that reach `rest_api_init`. Static so the callback can be
53 + * unregistered.
54 + *
55 + * @access public
56 + */
57 + public static function register() {
58 + ( new self() )->register_rest_routes();
59 + }
60 +
61 + /**
30 62 * Registers the REST routes for Blaze Dashboard.
31 63 *
32 64 * Blaze Dashboard is built from `wp-calypso`, which leverages the `public-api.wordpress.com` API.
33 65 * The current Site ID is added as part of the route, so that the front end doesn't have to handle the differences.
@@ -95,8 +127,19 @@
95 127 'permission_callback' => array( $this, 'can_user_view_dsp_callback' ),
96 128 )
97 129 );
98 130
131 + // WordAds DSP API upload to WP Media Library routes
132 + register_rest_route(
133 + static::$namespace,
134 + sprintf( '/sites/%1$d/wordads/dsp/api/v1/wpcom/sites/%1$d/media', $site_id ),
135 + array(
136 + 'methods' => WP_REST_Server::CREATABLE,
137 + 'callback' => array( $this, 'upload_image_to_current_website' ),
138 + 'permission_callback' => array( $this, 'can_user_view_dsp_callback' ),
139 + )
140 + );
141 +
99 142 // WordAds DSP API media openverse query routes
100 143 register_rest_route(
101 144 static::$namespace,
102 145 sprintf( '/sites/%1$d/wordads/dsp/api/v1/wpcom/media(?P<sub_path>[a-zA-Z0-9-_\/]*)(\?.*)?', $site_id ),
@@ -120,9 +163,9 @@
120 163
121 164 // WordAds DSP API Campaigns routes
122 165 register_rest_route(
123 166 static::$namespace,
124 - sprintf( '/sites/%d/wordads/dsp/api/v1/campaigns(?P<sub_path>[a-zA-Z0-9-_\/]*)(\?.*)?', $site_id ),
167 + sprintf( '/sites/%d/wordads/dsp/api/(?P<api_version>v[0-9]+\.?[0-9]*)/campaigns(?P<sub_path>[a-zA-Z0-9-_\/]*)(\?.*)?', $site_id ),
125 168 array(
126 169 'methods' => WP_REST_Server::READABLE,
127 170 'callback' => array( $this, 'get_dsp_campaigns' ),
128 171 'permission_callback' => array( $this, 'can_user_view_dsp_callback' ),
@@ -127,12 +170,23 @@
127 170 'callback' => array( $this, 'get_dsp_campaigns' ),
128 171 'permission_callback' => array( $this, 'can_user_view_dsp_callback' ),
129 172 )
130 173 );
174 +
131 175 register_rest_route(
132 176 static::$namespace,
133 - sprintf( '/sites/%d/wordads/dsp/api/v1/campaigns(?P<sub_path>[a-zA-Z0-9-_\/]*)', $site_id ),
177 + sprintf( '/sites/%d/wordads/dsp/api/v1.1/campaigns', $site_id ),
134 178 array(
179 + 'methods' => WP_REST_Server::CREATABLE,
180 + 'callback' => array( $this, 'create_dsp_campaigns' ),
181 + 'permission_callback' => array( $this, 'can_user_view_dsp_callback' ),
182 + )
183 + );
184 +
185 + register_rest_route(
186 + static::$namespace,
187 + sprintf( '/sites/%d/wordads/dsp/api/(?P<api_version>v[0-9]+\.?[0-9]*)/campaigns(?P<sub_path>[a-zA-Z0-9-_\/]*)', $site_id ),
188 + array(
135 189 'methods' => WP_REST_Server::EDITABLE,
136 190 'callback' => array( $this, 'edit_dsp_campaigns' ),
137 191 'permission_callback' => array( $this, 'can_user_view_dsp_callback' ),
138 192 )
@@ -148,8 +202,29 @@
148 202 'permission_callback' => array( $this, 'can_user_view_dsp_callback' ),
149 203 )
150 204 );
151 205
206 + // WordAds DSP API Site Stats routes
207 + register_rest_route(
208 + static::$namespace,
209 + sprintf( '/sites/%d/wordads/dsp/api/(?P<api_version>v[0-9]+\.?[0-9]*)/stats(?P<sub_path>[a-zA-Z0-9-_\/]*)(\?.*)?', $site_id ),
210 + array(
211 + 'methods' => WP_REST_Server::READABLE,
212 + 'callback' => array( $this, 'get_dsp_stats' ),
213 + 'permission_callback' => array( $this, 'can_user_view_dsp_callback' ),
214 + )
215 + );
216 +
217 + register_rest_route(
218 + static::$namespace,
219 + sprintf( '/sites/%d/wordads/dsp/api/(?P<api_version>v[0-9]+\.?[0-9]*)/stats(?P<sub_path>[a-zA-Z0-9-_\/]*)', $site_id ),
220 + array(
221 + 'methods' => WP_REST_Server::EDITABLE,
222 + 'callback' => array( $this, 'edit_dsp_stats' ),
223 + 'permission_callback' => array( $this, 'can_user_view_dsp_callback' ),
224 + )
225 + );
226 +
152 227 // WordAds DSP API Search routes
153 228 register_rest_route(
154 229 static::$namespace,
155 230 sprintf( '/sites/%d/wordads/dsp/api/v1/search(?P<sub_path>[a-zA-Z0-9-_\/]*)(\?.*)?', $site_id ),
@@ -173,8 +248,27 @@
173 248
174 249 // WordAds DSP API Templates routes
175 250 register_rest_route(
176 251 static::$namespace,
252 + sprintf( '/sites/%d/wordads/dsp/api/v1/templates/article/(?P<urn>[a-zA-Z0-9-_:]*)(\?.*)?', $site_id ),
253 + array(
254 + 'methods' => WP_REST_Server::READABLE,
255 + 'callback' => array( $this, 'get_dsp_templates_article' ),
256 + 'permission_callback' => array( $this, 'can_user_view_dsp_callback' ),
257 + )
258 + );
259 + register_rest_route(
260 + static::$namespace,
261 + sprintf( '/sites/%d/wordads/dsp/api/v1/templates/advise/campaign/(?P<urn>[a-zA-Z0-9-_:]*)(\?.*)?', $site_id ),
262 + array(
263 + 'methods' => WP_REST_Server::READABLE,
264 + 'callback' => array( $this, 'get_dsp_templates_advise_campaign' ),
265 + 'permission_callback' => array( $this, 'can_user_view_dsp_callback' ),
266 + )
267 + );
268 +
269 + register_rest_route(
270 + static::$namespace,
177 271 sprintf( '/sites/%d/wordads/dsp/api/v1/templates(?P<sub_path>[a-zA-Z0-9-_\/:]*)(\?.*)?', $site_id ),
178 272 array(
179 273 'methods' => WP_REST_Server::READABLE,
180 274 'callback' => array( $this, 'get_dsp_templates' ),
@@ -181,8 +275,28 @@
181 275 'permission_callback' => array( $this, 'can_user_view_dsp_callback' ),
182 276 )
183 277 );
184 278
279 + // WordAds DSP API Advise routes
280 + register_rest_route(
281 + static::$namespace,
282 + sprintf( '/sites/%d/wordads/dsp/api/v1/advise/campaign/(?P<urn>[a-zA-Z0-9-_:]*)(\?.*)?', $site_id ),
283 + array(
284 + 'methods' => WP_REST_Server::READABLE,
285 + 'callback' => array( $this, 'get_dsp_advise_campaign' ),
286 + 'permission_callback' => array( $this, 'can_user_view_dsp_callback' ),
287 + )
288 + );
289 + register_rest_route(
290 + static::$namespace,
291 + sprintf( '/sites/%d/wordads/dsp/api/v1/advise(?P<sub_path>[a-zA-Z0-9-_\/:]*)(\?.*)?', $site_id ),
292 + array(
293 + 'methods' => WP_REST_Server::READABLE,
294 + 'callback' => array( $this, 'get_dsp_advise' ),
295 + 'permission_callback' => array( $this, 'can_user_view_dsp_callback' ),
296 + )
297 + );
298 +
185 299 // WordAds DSP API Subscriptions routes
186 300 register_rest_route(
187 301 static::$namespace,
188 302 sprintf( '/sites/%d/wordads/dsp/api/v1/subscriptions(?P<sub_path>[a-zA-Z0-9-_\/]*)(\?.*)?', $site_id ),
@@ -204,9 +318,9 @@
204 318
205 319 // WordAds DSP API Payments routes
206 320 register_rest_route(
207 321 static::$namespace,
208 - sprintf( '/sites/%d/wordads/dsp/api/v1/payments(?P<sub_path>[a-zA-Z0-9-_\/]*)(\?.*)?', $site_id ),
322 + sprintf( '/sites/%d/wordads/dsp/api/(?P<api_version>v[0-9]+\.?[0-9]*)/payments(?P<sub_path>[a-zA-Z0-9-_\/]*)(\?.*)?', $site_id ),
209 323 array(
210 324 'methods' => WP_REST_Server::READABLE,
211 325 'callback' => array( $this, 'get_dsp_payments' ),
212 326 'permission_callback' => array( $this, 'can_user_view_dsp_callback' ),
@@ -213,9 +327,9 @@
213 327 )
214 328 );
215 329 register_rest_route(
216 330 static::$namespace,
217 - sprintf( '/sites/%d/wordads/dsp/api/v1/payments(?P<sub_path>[a-zA-Z0-9-_\/]*)', $site_id ),
331 + sprintf( '/sites/%d/wordads/dsp/api/(?P<api_version>v[0-9]+\.?[0-9]*)/payments(?P<sub_path>[a-zA-Z0-9-_\/]*)', $site_id ),
218 332 array(
219 333 'methods' => WP_REST_Server::EDITABLE,
220 334 'callback' => array( $this, 'edit_dsp_payments' ),
221 335 'permission_callback' => array( $this, 'can_user_view_dsp_callback' ),
@@ -303,10 +417,15 @@
303 417 return $this->get_forbidden_error();
304 418 }
305 419
306 420 /**
307 - * Redirect GET requests to WordAds DSP for the site.
421 + * Get a list of posts that are eligible for Blaze campaigns.
308 422 *
423 + * Routes to WPCOM API or local database based on Jetpack Sync status:
424 + * - If sync is ready: Uses WPCOM API (has stats data like like_count, monthly_view_count).
425 + * - If sync is not ready: Uses local database query (stats show as -1, stats-based
426 + * sorting falls back to date).
427 + *
309 428 * @param WP_REST_Request $req The request object.
310 429 * @return array|WP_Error
311 430 */
312 431 public function get_blaze_posts( $req ) {
@@ -314,21 +433,269 @@
314 433 if ( is_wp_error( $site_id ) ) {
315 434 return array();
316 435 }
317 436
318 - // We don't use sub_path in the blaze posts, only query strings
437 + $sync_ready = $this->are_posts_ready();
438 +
439 + if ( $sync_ready ) {
440 + $response = $this->get_blaze_posts_from_wpcom( $req, $site_id );
441 + } else {
442 + $response = $this->get_blaze_posts_local( $req );
443 + }
444 +
445 + if ( is_wp_error( $response ) || $response instanceof \WP_REST_Response ) {
446 + return $response;
447 + }
448 +
449 + if ( is_array( $response ) ) {
450 + $response['sync_ready'] = $sync_ready;
451 + }
452 +
453 + return $response;
454 + }
455 +
456 + /**
457 + * Get Blaze posts from the WPCOM API.
458 + *
459 + * Used when Jetpack Sync is ready and posts are available on WPCOM.
460 + * Provides full functionality including stats data (like_count, monthly_view_count)
461 + * and stats-based sorting.
462 + *
463 + * @param WP_REST_Request $req The request object.
464 + * @param int $site_id The site ID.
465 + * @return array|WP_Error
466 + */
467 + private function get_blaze_posts_from_wpcom( $req, $site_id ) {
468 + // We don't use sub_path in the blaze posts, only query strings.
319 469 if ( isset( $req['sub_path'] ) ) {
320 470 unset( $req['sub_path'] );
321 471 }
322 472
323 - return $this->request_as_user(
473 + $response = $this->request_as_user(
324 474 sprintf( '/sites/%d/blaze/posts%s', $site_id, $this->build_subpath_with_query_strings( $req->get_params() ) ),
325 475 'v2',
326 476 array( 'method' => 'GET' )
327 477 );
478 +
479 + // Bail if we get an error (WP_ERROR or an already formatted WP_REST_Response error).
480 + if ( is_wp_error( $response ) || $response instanceof \WP_REST_Response ) {
481 + return $response;
482 + }
483 +
484 + if ( isset( $response['posts'] ) && count( $response['posts'] ) > 0 ) {
485 + $response['posts'] = $this->add_prices_in_posts( $response['posts'] );
486 + }
487 +
488 + return $response;
328 489 }
329 490
330 491 /**
492 + * Get Blaze posts from the local WordPress database.
493 + *
494 + * Used as fallback when Jetpack Sync is not ready. Stats fields (like_count,
495 + * monthly_view_count) are returned as -1 since they are only available on WPCOM.
496 + * If user requests sorting by stats fields, falls back to sorting by date.
497 + *
498 + * @param WP_REST_Request $req The request object.
499 + * @return array
500 + */
501 + private function get_blaze_posts_local( $req ) {
502 + // Default and maximum posts per page for this function.
503 + $default_posts_per_page = 20;
504 +
505 + // Parse request parameters.
506 + $page = absint( $req->get_param( 'page' ) ?? 1 );
507 + $posts_per_page = absint( $req->get_param( 'posts_per_page' ) ?? $default_posts_per_page );
508 + $order = $req->get_param( 'order' ) ?? 'DESC';
509 + $order_by = $req->get_param( 'order_by' ) ?? 'date';
510 + $post_types = $req->get_param( 'filter_post_type' ) ?? implode( ',', $this->get_blazable_post_types() );
511 + $title = strtolower( sanitize_text_field( $req->get_param( 'title' ) ?? '' ) );
512 +
513 + // Sanitize and validate post types.
514 + $post_type_list = $this->sanitize_post_type( $post_types );
515 +
516 + // Validate page parameter.
517 + if ( $page < 1 ) {
518 + $page = 1;
519 + }
520 +
521 + // Validate post per page parameter (use default value if invalid)
522 + if ( $posts_per_page <= 0 || $posts_per_page > $default_posts_per_page ) {
523 + $posts_per_page = $default_posts_per_page;
524 + }
525 +
526 + // Validate order.
527 + $order = in_array( strtoupper( $order ), array( 'ASC', 'DESC' ), true ) ? strtoupper( $order ) : 'DESC';
528 +
529 + // Validate order_by - stats-related fields fall back to date (handled by WPCOM).
530 + $valid_order_by = array( 'post_title', 'type', 'date', 'modified', 'comment_count' );
531 + if ( ! in_array( $order_by, $valid_order_by, true ) ) {
532 + $order_by = 'date';
533 + }
534 +
535 + $args = array(
536 + 'post_type' => $post_type_list,
537 + 'post_status' => 'publish',
538 + 'post_password' => '',
539 + 'posts_per_page' => $posts_per_page,
540 + 'paged' => $page,
541 + 'ignore_sticky_posts' => 1,
542 + 'orderby' => $order_by,
543 + 'order' => $order,
544 + );
545 +
546 + // Add title search filter if provided.
547 + $title_filter = null;
548 + if ( ! empty( $title ) ) {
549 + $title_filter = function ( $where ) use ( $title ) {
550 + global $wpdb;
551 + $title_like = '%' . $wpdb->esc_like( $title ) . '%';
552 + $where .= $wpdb->prepare( " AND {$wpdb->posts}.post_title LIKE %s", $title_like );
553 + return $where;
554 + };
555 + add_filter( 'posts_where', $title_filter );
556 + }
557 +
558 + $query = new \WP_Query( $args );
559 + $posts = $query->get_posts();
560 + $total_pages = $query->max_num_pages;
561 +
562 + // Remove the title filter after query.
563 + if ( $title_filter !== null ) {
564 + remove_filter( 'posts_where', $title_filter );
565 + }
566 +
567 + // Format posts for the response.
568 + $formatted_posts = array();
569 + if ( $page <= $total_pages ) {
570 + foreach ( $posts as $post ) {
571 + $formatted_posts[] = $this->format_post_for_blaze( $post );
572 + }
573 + }
574 +
575 + // Add prices for WooCommerce products.
576 + if ( count( $formatted_posts ) > 0 ) {
577 + $formatted_posts = $this->add_prices_in_posts( $formatted_posts );
578 + }
579 +
580 + return array(
581 + 'posts' => $formatted_posts,
582 + 'total_items' => $query->found_posts,
583 + 'post_title' => $title,
584 + 'page' => $page,
585 + 'total_pages' => $total_pages,
586 + 'stats_enabled' => $this->is_jetpack_module_active( 'stats' ),
587 + 'likes_enabled' => $this->is_jetpack_module_active( 'likes' ),
588 + 'tsp_eligible' => $this->count_tsp_eligible_posts(),
589 + );
590 + }
591 +
592 + /**
593 + * Format a post object for the Blaze API response.
594 + *
595 + * @param \WP_Post $post The post object.
596 + * @return array Formatted post data.
597 + */
598 + protected function format_post_for_blaze( $post ) {
599 + $featured_image_data = $this->get_post_featured_image( $post->ID );
600 + $featured_image = $featured_image_data['URL'] ?? null;
601 +
602 + // Get SKU for WooCommerce products.
603 + $sku = get_post_meta( $post->ID, '_sku', true );
604 +
605 + return array(
606 + 'ID' => $post->ID,
607 + 'title' => $post->post_title,
608 + 'type' => $post->post_type,
609 + 'date' => gmdate( 'c', strtotime( $post->post_date_gmt ) ),
610 + 'modified' => gmdate( 'c', strtotime( $post->post_modified_gmt ) ),
611 + 'comment_count' => (int) $post->comment_count,
612 + 'like_count' => -1, // Stats not available locally.
613 + 'featured_image' => $featured_image,
614 + 'author' => $post->post_author,
615 + 'sku' => $sku,
616 + 'post_url' => get_permalink( $post->ID ),
617 + 'monthly_view_count' => -1, // Stats not available locally.
618 + );
619 + }
620 +
621 + /**
622 + * Get the post types that are eligible for Blaze campaigns.
623 + *
624 + * @return array List of post type slugs.
625 + */
626 + private function get_blazable_post_types() {
627 + return array( 'post', 'page', 'product' );
628 + }
629 +
630 + /**
631 + * Sanitize and validate post types for Blaze.
632 + *
633 + * @param string $post_types Comma-separated list of post types.
634 + * @return array Valid post types, or all blazable types if none valid.
635 + */
636 + private function sanitize_post_type( $post_types ) {
637 + $blazable_post_types = $this->get_blazable_post_types();
638 + if ( ! is_string( $post_types ) ) {
639 + return $blazable_post_types;
640 + }
641 + $post_types = sanitize_text_field( $post_types );
642 + $post_type_list = explode( ',', $post_types );
643 +
644 + $allowed_types = array();
645 +
646 + foreach ( $post_type_list as $post_type ) {
647 + if ( in_array( $post_type, $blazable_post_types, true ) ) {
648 + $allowed_types[] = $post_type;
649 + }
650 + }
651 +
652 + return count( $allowed_types )
653 + ? $allowed_types
654 + : $blazable_post_types;
655 + }
656 +
657 + /**
658 + * Check if a Jetpack module is active.
659 + * Uses jetpack-status Modules class which handles WPCOM and self-hosted sites.
660 + *
661 + * @param string $module_name The module name (e.g., 'stats', 'likes').
662 + * @return bool Whether the module is active.
663 + */
664 + private function is_jetpack_module_active( $module_name ) {
665 + // Default to true if Modules class is unavailable (matches WPCOM behavior).
666 + if ( ! class_exists( '\Automattic\Jetpack\Modules' ) ) {
667 + return true;
668 + }
669 + $modules = new \Automattic\Jetpack\Modules();
670 + return $modules->is_active( $module_name );
671 + }
672 +
673 + /**
674 + * Count posts eligible for TSP (has Gutenberg blocks).
675 + * Matches WPCOM's count_tsp_eligible_posts implementation.
676 + *
677 + * @return bool Whether there are TSP eligible posts.
678 + */
679 + private function count_tsp_eligible_posts() {
680 + $query = array(
681 + 'posts_per_page' => 1,
682 + 'order' => 'DESC',
683 + 'orderby' => 'date',
684 + 'post_type' => 'post',
685 + 'post_status' => array( 'publish' ),
686 + 's' => '<!-- wp:',
687 + 'fields' => 'ids',
688 + 'ignore_sticky_posts' => 1,
689 + 'offset' => 0,
690 + );
691 +
692 + $wp_query = new \WP_Query( $query );
693 +
694 + return (int) $wp_query->found_posts > 0;
695 + }
696 +
697 + /**
331 698 * Builds the subpath including the query string to be used in the DSP call
332 699 *
333 700 * @param array $params The request object parameters.
334 701 * @return string
@@ -351,25 +718,49 @@
351 718 return $sub_path;
352 719 }
353 720
354 721 /**
355 - * Redirect GET requests to WordAds DSP Blaze Posts endpoint for the site.
722 + * Get Blaze posts for DSP
356 723 *
724 + * Maps DSP parameters to blaze/posts format and reuses get_blaze_posts
725 + * for consistent local/WPCOM routing logic.
726 + *
357 727 * @param WP_REST_Request $req The request object.
358 728 * @return array|WP_Error
359 729 */
360 730 public function get_dsp_blaze_posts( $req ) {
361 - $site_id = $this->get_site_id();
362 - if ( is_wp_error( $site_id ) ) {
363 - return array();
731 + // Map DSP params → blaze params.
732 + $param_map = array(
733 + 'title' => $req->get_param( 'search' ),
734 + 'filter_post_type' => $req->get_param( 'post_type' ),
735 + 'posts_per_page' => $req->get_param( 'limit' ),
736 + 'page' => $req->get_param( 'page' ),
737 + 'order' => $req->get_param( 'order' ),
738 + 'order_by' => $req->get_param( 'order_by' ),
739 + );
740 +
741 + // Create new request with transformed params (only non-null values).
742 + $blaze_req = new \WP_REST_Request( 'GET' );
743 + foreach ( $param_map as $key => $value ) {
744 + if ( $value !== null ) {
745 + $blaze_req->set_param( $key, $value );
746 + }
364 747 }
365 748
366 - // We don't use sub_path in the blaze posts, only query strings
367 - if ( isset( $req['sub_path'] ) ) {
368 - unset( $req['sub_path'] );
749 + // Reuse get_blaze_posts (handles local/WPCOM routing).
750 + $response = $this->get_blaze_posts( $blaze_req );
751 +
752 + // Bail if we get an error.
753 + if ( is_wp_error( $response ) || $response instanceof \WP_REST_Response ) {
754 + return $response;
369 755 }
370 756
371 - return $this->get_dsp_generic( sprintf( 'v1/wpcom/sites/%d/blaze/posts', $site_id ), $req );
757 + // Transform response to DSP format.
758 + return array(
759 + 'results' => $response['posts'] ?? array(),
760 + 'total' => $response['total_items'] ?? 0,
761 + 'sync_ready' => $response['sync_ready'] ?? false,
762 + );
372 763 }
373 764
374 765 /**
375 766 * Redirect GET requests to WordAds DSP Blaze media endpoint for the site.
@@ -385,8 +776,67 @@
385 776 return $this->get_dsp_generic( sprintf( 'v1/wpcom/sites/%d/media', $site_id ), $req );
386 777 }
387 778
388 779 /**
780 + * Redirect POST requests to WordAds DSP Blaze media endpoint for the site.
781 + *
782 + * @return array|WP_Error
783 + */
784 + public function upload_image_to_current_website() {
785 + $site_id = $this->get_site_id();
786 + if ( is_wp_error( $site_id ) ) {
787 + return array( 'error' => $site_id->get_error_message() );
788 + }
789 +
790 + if ( empty( $_FILES['image'] ) ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
791 + return array( 'error' => 'File is missed' );
792 + }
793 + $file = $_FILES['image']; // phpcs:ignore WordPress.Security.NonceVerification.Missing,WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
794 + $temp_name = $file['tmp_name'] ?? '';
795 + if ( ! $temp_name || ! is_uploaded_file( $temp_name ) ) {
796 + return array( 'error' => 'Specified file was not uploaded' );
797 + }
798 +
799 + // Getting the original file name.
800 + $filename = sanitize_file_name( basename( $file['name'] ) );
801 + // Upload contents to the Upload folder locally.
802 + $upload = wp_upload_bits(
803 + $filename,
804 + null,
805 + file_get_contents( $temp_name ) // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
806 + );
807 +
808 + if ( ! empty( $upload['error'] ) ) {
809 + return array( 'error' => $upload['error'] );
810 + }
811 +
812 + // Check the type of file. We'll use this as the 'post_mime_type'.
813 + $filetype = wp_check_filetype( $filename, null );
814 +
815 + // Prepare an array of post data for the attachment.
816 + $attachment = array(
817 + 'guid' => wp_upload_dir()['url'] . '/' . $filename,
818 + 'post_mime_type' => $filetype['type'],
819 + 'post_title' => preg_replace( '/\.[^.]+$/', '', $filename ),
820 + 'post_content' => '',
821 + 'post_status' => 'inherit',
822 + );
823 +
824 + // Insert the attachment.
825 + $attach_id = wp_insert_attachment( $attachment, $upload['file'] );
826 +
827 + // Make sure wp_generate_attachment_metadata() has all requirement dependencies.
828 + require_once ABSPATH . 'wp-admin/includes/image.php';
829 +
830 + // Generate the metadata for the attachment, and update the database record.
831 + $attach_data = wp_generate_attachment_metadata( $attach_id, $upload['file'] );
832 + // Store metadata in the local DB.
833 + wp_update_attachment_metadata( $attach_id, $attach_data );
834 +
835 + return array( 'url' => $upload['url'] );
836 + }
837 +
838 + /**
389 839 * Redirect GET requests to WordAds DSP Blaze openverse endpoint.
390 840 *
391 841 * @param WP_REST_Request $req The request object.
392 842 * @return array|WP_Error
@@ -421,9 +871,10 @@
421 871 * @param WP_REST_Request $req The request object.
422 872 * @return array|WP_Error
423 873 */
424 874 public function get_dsp_campaigns( $req ) {
425 - return $this->get_dsp_generic( 'v1/campaigns', $req );
875 + $version = $req->get_param( 'api_version' ) ?? 'v1';
876 + return $this->get_dsp_generic( "{$version}/campaigns", $req );
426 877 }
427 878
428 879 /**
429 880 * Redirect GET requests to WordAds DSP Site Campaigns endpoint for the site.
@@ -440,8 +891,32 @@
440 891 return $this->get_dsp_generic( sprintf( 'v1/sites/%d/campaigns', $site_id ), $req );
441 892 }
442 893
443 894 /**
895 + * Redirect GET requests to WordAds DSP Stats endpoint for the site.
896 + *
897 + * @param WP_REST_Request $req The request object.
898 + *
899 + * @return array|WP_Error
900 + */
901 + public function get_dsp_stats( $req ) {
902 + $version = $req->get_param( 'api_version' ) ?? 'v1';
903 + return $this->get_dsp_generic( "{$version}/stats", $req );
904 + }
905 +
906 + /**
907 + * Redirect POST requests to WordAds DSP Stats endpoint for the site.
908 + *
909 + * @param WP_REST_Request $req The request object.
910 + *
911 + * @return array|WP_Error
912 + */
913 + public function edit_dsp_stats( $req ) {
914 + $version = $req->get_param( 'api_version' ) ?? 'v1';
915 + return $this->edit_dsp_generic( "{$version}/stats", $req );
916 + }
917 +
918 + /**
444 919 * Redirect GET requests to WordAds DSP Search endpoint for the site.
445 920 *
446 921 * @param WP_REST_Request $req The request object.
447 922 * @return array|WP_Error
@@ -460,18 +935,209 @@
460 935 return $this->get_dsp_generic( 'v1/user', $req );
461 936 }
462 937
463 938 /**
464 - * Redirect GET requests to WordAds DSP Search endpoint for the site.
939 + * Redirect GET requests to the WordAds DSP Templates Article endpoint for the site.
465 940 *
466 941 * @param WP_REST_Request $req The request object.
467 942 * @return array|WP_Error
468 943 */
944 + public function get_dsp_templates_article( $req ) {
945 + $urn = $req->get_param( 'urn' ) ?? '';
946 +
947 + $sync_ready = $this->are_posts_ready();
948 +
949 + $response = $sync_ready ?
950 + $this->get_dsp_generic( 'v1/templates/article/' . $urn, $req ) :
951 + $this->get_dsp_templates_article_local( $urn, $req );
952 +
953 + if ( ! is_wp_error( $response ) && is_array( $response ) ) {
954 + $response['sync_ready'] = $sync_ready;
955 + }
956 +
957 + return $response;
958 + }
959 +
960 + /**
961 + * Get the article information to be used in the Blaze create campaign flow.
962 + *
963 + * If Jetpack Sync is not yet complete and posts are not fully synced, this endpoint will read local DB data and provide additional information to the WPCOM endpoint.
964 + *
965 + * @param string $urn The request urn.
966 + * @param WP_REST_Request $req The request object.
967 + * @return array|WP_Error
968 + */
969 + public function get_dsp_templates_article_local( $urn, $req ) {
970 + $parsed_urn = $this->get_data_from_urn( $urn );
971 + $site_id = $this->get_site_id();
972 +
973 + if ( is_wp_error( $site_id ) ) {
974 + return array();
975 + }
976 +
977 + if ( ! $parsed_urn['site_id'] || $parsed_urn['site_id'] !== $site_id ) {
978 + return $this->get_forbidden_error();
979 + }
980 +
981 + $post = get_post( $parsed_urn['post_id'] );
982 + if ( ! $post ) {
983 + return new WP_Error( 'post_not_found', esc_html__( 'Post not found', 'jetpack-blaze' ), array( 'status' => 404 ) );
984 + }
985 +
986 + // Generates the attachments object
987 + $post_attachments = get_attached_media( 'image', $post->ID );
988 + $attachments = array();
989 +
990 + foreach ( $post_attachments as $attachment ) {
991 + $attachment_url = wp_get_attachment_url( $attachment->ID );
992 + $metadata = wp_get_attachment_metadata( $attachment->ID );
993 +
994 + // Skip attachment if some of the required data is missing
995 + if ( ! $attachment_url || ! $metadata || ! isset( $metadata['width'] ) || ! isset( $metadata['height'] ) ) {
996 + continue;
997 + }
998 +
999 + $attachments[ $attachment->ID ] = array(
1000 + 'ID' => $attachment->ID,
1001 + 'URL' => $attachment_url,
1002 + 'mime_type' => $attachment->post_mime_type,
1003 + 'width' => $metadata['width'],
1004 + 'height' => $metadata['height'],
1005 + );
1006 + }
1007 +
1008 + $body = array(
1009 + 'widget_origin' => $req->get_param( 'widget_origin' ),
1010 + 'wp_post' => array(
1011 + 'ID' => $post->ID,
1012 + 'title' => $post->post_title,
1013 + 'excerpt' => $post->post_excerpt,
1014 + 'URL' => get_permalink( $post ),
1015 + 'type' => $post->post_type,
1016 + 'content' => $post->post_content,
1017 + 'post_thumbnail' => $this->get_post_featured_image( $post->ID ),
1018 + 'attachments' => (object) $attachments,
1019 + ),
1020 + );
1021 +
1022 + return $this->request_as_user(
1023 + sprintf( '/sites/%d/wordads/dsp/api/v1/templates/article/%s', $site_id, $urn ),
1024 + 'v2',
1025 + array( 'method' => 'POST' ),
1026 + $body
1027 + );
1028 + }
1029 +
1030 + /**
1031 + * Redirect GET requests to the WordAds DSP Templates Advise Campaign endpoint for the site.
1032 + *
1033 + * @param WP_REST_Request $req The request object.
1034 + * @return array|WP_Error
1035 + */
1036 + public function get_dsp_templates_advise_campaign( $req ) {
1037 + $urn = $req->get_param( 'urn' ) ?? '';
1038 +
1039 + $sync_ready = $this->are_posts_ready();
1040 +
1041 + $response = $sync_ready ?
1042 + $this->get_dsp_generic( 'v1/templates/advise/campaign/' . $urn, $req ) :
1043 + $this->get_dsp_advise_campaign_local( $urn );
1044 +
1045 + if ( ! is_wp_error( $response ) && is_array( $response ) ) {
1046 + $response['sync_ready'] = $sync_ready;
1047 + }
1048 +
1049 + return $response;
1050 + }
1051 +
1052 + /**
1053 + * Get the advise campaign information to be used in the Blaze create campaign flow.
1054 + *
1055 + * If Jetpack Sync still is running, this endpoint will read local DB data and provide additional information to the WPCOM endpoint.
1056 + *
1057 + * @param string $urn The request urn.
1058 + * @return array|WP_Error
1059 + */
1060 + public function get_dsp_advise_campaign_local( $urn ) {
1061 + $parsed_urn = $this->get_data_from_urn( $urn );
1062 + $site_id = $this->get_site_id();
1063 +
1064 + if ( is_wp_error( $site_id ) ) {
1065 + return array();
1066 + }
1067 +
1068 + if ( ! $parsed_urn['site_id'] || $parsed_urn['site_id'] !== $site_id ) {
1069 + return $this->get_forbidden_error();
1070 + }
1071 +
1072 + $post = get_post( $parsed_urn['post_id'] );
1073 + if ( ! $post ) {
1074 + return new WP_Error( 'post_not_found', esc_html__( 'Post not found', 'jetpack-blaze' ), array( 'status' => 404 ) );
1075 + }
1076 +
1077 + $rendered_content = apply_filters( 'the_content', $post->post_content );
1078 +
1079 + $body = array(
1080 + 'wp_post' => array(
1081 + 'ID' => $post->ID,
1082 + 'title' => $post->post_title,
1083 + 'URL' => get_permalink( $post ),
1084 + 'type' => $post->post_type,
1085 + 'content' => $rendered_content,
1086 + ),
1087 + );
1088 +
1089 + return $this->request_as_user(
1090 + sprintf( '/sites/%d/wordads/dsp/api/v1/advise/campaign/%s', $site_id, $urn ),
1091 + 'v2',
1092 + array( 'method' => 'POST' ),
1093 + $body
1094 + );
1095 + }
1096 +
1097 + /**
1098 + * Redirect GET requests to the WordAds DSP Templates endpoint for the site.
1099 + *
1100 + * @param WP_REST_Request $req The request object.
1101 + * @return array|WP_Error
1102 + */
469 1103 public function get_dsp_templates( $req ) {
470 1104 return $this->get_dsp_generic( 'v1/templates', $req );
471 1105 }
472 1106
473 1107 /**
1108 + * Redirect GET requests to the WordAds DSP Advise Campaign endpoint for the site.
1109 + *
1110 + * @param WP_REST_Request $req The request object.
1111 + * @return array|WP_Error
1112 + */
1113 + public function get_dsp_advise_campaign( $req ) {
1114 + $urn = $req->get_param( 'urn' ) ?? '';
1115 +
1116 + $sync_ready = $this->are_posts_ready();
1117 +
1118 + $response = $sync_ready ?
1119 + $this->get_dsp_generic( 'v1/advise/campaign/' . $urn, $req ) :
1120 + $this->get_dsp_advise_campaign_local( $urn );
1121 +
1122 + if ( ! is_wp_error( $response ) && is_array( $response ) ) {
1123 + $response['sync_ready'] = $sync_ready;
1124 + }
1125 +
1126 + return $response;
1127 + }
1128 +
1129 + /**
1130 + * Redirect GET requests to the WordAds DSP Advise endpoint for the site.
1131 + *
1132 + * @param WP_REST_Request $req The request object.
1133 + * @return array|WP_Error
1134 + */
1135 + public function get_dsp_advise( $req ) {
1136 + return $this->get_dsp_generic( 'v1/advise', $req );
1137 + }
1138 +
1139 + /**
474 1140 * Redirect GET requests to WordAds DSP Subscriptions endpoint for the site.
475 1141 *
476 1142 * @param WP_REST_Request $req The request object.
477 1143 * @return array|WP_Error
@@ -486,9 +1152,10 @@
486 1152 * @param WP_REST_Request $req The request object.
487 1153 * @return array|WP_Error
488 1154 */
489 1155 public function get_dsp_payments( $req ) {
490 - return $this->get_dsp_generic( 'v1/payments', $req );
1156 + $version = $req->get_param( 'api_version' ) ?? 'v1';
1157 + return $this->get_dsp_generic( "{$version}/payments", $req );
491 1158 }
492 1159
493 1160 /**
494 1161 * Redirect GET requests to WordAds DSP Subscriptions endpoint for the site.
@@ -560,12 +1227,96 @@
560 1227 * @param WP_REST_Request $req The request object.
561 1228 * @return array|WP_Error
562 1229 */
563 1230 public function edit_wpcom_checkout( $req ) {
564 - return $this->edit_dsp_generic( 'v1/wpcom/checkout', $req, array( 'timeout' => 20 ) );
1231 + return $this->edit_dsp_generic( 'v1/wpcom/checkout', $req, array( 'timeout' => 60 ) );
565 1232 }
566 1233
567 1234 /**
1235 + * Redirect POST request to WordAds DSP Create Campaign endpoint for the site.
1236 + *
1237 + * If Jetpack Sync is not yet complete and posts are not fully synced, this endpoint will read local DB data and provide additional information to the WPCOM endpoint.
1238 + *
1239 + * @param WP_REST_Request $req The request object.
1240 + * @return array|WP_Error
1241 + */
1242 + public function create_dsp_campaigns( $req ) {
1243 + $sync_ready = $this->are_posts_ready();
1244 +
1245 + $response = $sync_ready ?
1246 + $this->edit_dsp_generic( 'v1.1/campaigns', $req, array( 'timeout' => 60 ) ) :
1247 + $this->create_dsp_campaigns_local( $req );
1248 +
1249 + if ( ! is_wp_error( $response ) && is_array( $response ) ) {
1250 + $response['sync_ready'] = $sync_ready;
1251 + }
1252 +
1253 + return $response;
1254 + }
1255 +
1256 + /**
1257 + * Sends a create campaign request to the WordAds DSP Create Campaign endpoint.
1258 + * Includes additional Post information to the original request.
1259 + *
1260 + * @param WP_REST_Request $req The request object.
1261 + * @return array|WP_Error
1262 + */
1263 + public function create_dsp_campaigns_local( $req ) {
1264 + $site_id = $this->get_site_id();
1265 + if ( is_wp_error( $site_id ) ) {
1266 + return array();
1267 + }
1268 +
1269 + $request_body = $req->get_json_params();
1270 + if ( ! is_array( $request_body ) ) {
1271 + return new WP_Error( 'invalid_json', esc_html__( 'Invalid JSON Body', 'jetpack-blaze' ), array( 'status' => 400 ) );
1272 + }
1273 +
1274 + if ( ! isset( $request_body['target_urn'] ) ) {
1275 + return new WP_Error( 'missing_target_urn', esc_html__( 'Missing target_urn in request body', 'jetpack-blaze' ), array( 'status' => 400 ) );
1276 + }
1277 +
1278 + $urn = $request_body['target_urn'];
1279 + $parsed_urn = $this->get_data_from_urn( $urn );
1280 +
1281 + if ( ! $parsed_urn['site_id'] || $parsed_urn['site_id'] !== $site_id ) {
1282 + return $this->get_forbidden_error();
1283 + }
1284 +
1285 + $post = get_post( $parsed_urn['post_id'] );
1286 + if ( ! $post ) {
1287 + return new WP_Error( 'post_not_found', esc_html__( 'Post not found', 'jetpack-blaze' ), array( 'status' => 404 ) );
1288 + }
1289 +
1290 + $featured_image = $this->get_post_featured_image( $post->ID );
1291 +
1292 + $body = array_merge(
1293 + $request_body,
1294 + array(
1295 + 'wp_post' => array(
1296 + 'ID' => $post->ID,
1297 + 'title' => $post->post_title,
1298 + 'URL' => get_permalink( $post ),
1299 + 'type' => $post->post_type,
1300 + 'content' => $post->post_content,
1301 + 'featured_image' => $featured_image['URL'] ?? '',
1302 + 'modified' => $post->post_modified,
1303 + ),
1304 + )
1305 + );
1306 +
1307 + return $this->request_as_user(
1308 + sprintf( '/sites/%d/wordads/dsp/api/v1.1/campaigns', $site_id ),
1309 + 'v2',
1310 + array(
1311 + 'method' => 'POST',
1312 + 'timeout' => 60,
1313 + ),
1314 + $body
1315 + );
1316 + }
1317 +
1318 + /**
568 1319 * Redirect POST/PUT/PATCH requests to WordAds DSP Campaigns endpoint for the site.
569 1320 *
570 1321 * @param WP_REST_Request $req The request object.
571 1322 * @return array|WP_Error
@@ -570,9 +1321,10 @@
570 1321 * @param WP_REST_Request $req The request object.
571 1322 * @return array|WP_Error
572 1323 */
573 1324 public function edit_dsp_campaigns( $req ) {
574 - return $this->edit_dsp_generic( 'v1/campaigns', $req, array( 'timeout' => 20 ) );
1325 + $version = $req->get_param( 'api_version' ) ?? 'v1';
1326 + return $this->edit_dsp_generic( "{$version}/campaigns", $req, array( 'timeout' => 60 ) );
575 1327 }
576 1328
577 1329 /**
578 1330 * Redirect POST/PUT/PATCH requests to WordAds DSP Subscriptions endpoint for the site.
@@ -590,9 +1342,10 @@
590 1342 * @param WP_REST_Request $req The request object.
591 1343 * @return array|WP_Error
592 1344 */
593 1345 public function edit_dsp_payments( $req ) {
594 - return $this->edit_dsp_generic( 'v1/payments', $req, array( 'timeout' => 20 ) );
1346 + $version = $req->get_param( 'api_version' ) ?? 'v1';
1347 + return $this->edit_dsp_generic( "{$version}/payments", $req, array( 'timeout' => 20 ) );
595 1348 }
596 1349
597 1350 /**
598 1351 * Redirect POST/PUT/PATCH requests to WordAds DSP Logs endpoint for the site.
@@ -639,21 +1392,68 @@
639 1392 );
640 1393 }
641 1394
642 1395 /**
1396 + * Will check the posts for prices and add them to the posts array
1397 + *
1398 + * @param array $posts The posts object.
1399 + * @return array The list posts with the price on them (if they are woo products).
1400 + */
1401 + protected function add_prices_in_posts( $posts ) {
1402 +
1403 + if ( ! function_exists( 'wc_get_product' ) ||
1404 + ! function_exists( 'wc_get_price_decimal_separator' ) ||
1405 + ! function_exists( 'wc_get_price_thousand_separator' ) ||
1406 + ! function_exists( 'wc_get_price_decimals' ) ||
1407 + ! function_exists( 'get_woocommerce_price_format' ) ||
1408 + ! function_exists( 'get_woocommerce_currency_symbol' )
1409 + ) {
1410 + return $posts;
1411 + }
1412 +
1413 + foreach ( $posts as $key => $item ) {
1414 + if ( ! isset( $item['ID'] ) ) {
1415 + $posts[ $key ]['price'] = '';
1416 + continue;
1417 + }
1418 + $product = wc_get_product( $item['ID'] );
1419 + if ( ! $product || ! $product instanceof WC_Product ) {
1420 + $posts[ $key ]['price'] = '';
1421 + } else {
1422 + $price = $product->get_price();
1423 + $decimal_separator = wc_get_price_decimal_separator();
1424 + $thousand_separator = wc_get_price_thousand_separator();
1425 + $decimals = wc_get_price_decimals();
1426 + $price_format = get_woocommerce_price_format();
1427 + $currency_symbol = get_woocommerce_currency_symbol();
1428 +
1429 + // Convert to float to avoid issues on PHP 8.
1430 + $price = (float) $price;
1431 + $negative = $price < 0;
1432 + $price = $negative ? $price * -1 : $price;
1433 + $price = number_format( $price, $decimals, $decimal_separator, $thousand_separator );
1434 + $formatted_price = sprintf( $price_format, $currency_symbol, $price );
1435 +
1436 + $posts[ $key ]['price'] = html_entity_decode( $formatted_price, ENT_COMPAT );
1437 + }
1438 + }
1439 + return $posts;
1440 + }
1441 +
1442 + /**
643 1443 * Queries the WordPress.com REST API with a user token.
644 1444 *
645 - * @param String $path The API endpoint relative path.
646 - * @param String $version The API version.
647 - * @param array $args Request arguments.
648 - * @param String $body Request body.
649 - * @param String $base_api_path (optional) the API base path override, defaults to 'rest'.
650 - * @param bool $use_cache (optional) default to true.
651 - * @return array|WP_Error $response Data.
1445 + * @param String $path The API endpoint relative path.
1446 + * @param String $version The API version.
1447 + * @param array $args Request arguments.
1448 + * @param null|String|array $body Request body.
1449 + * @param String $base_api_path (optional) the API base path override, defaults to 'rest'.
1450 + * @param bool $use_cache (optional) default to true.
1451 + * @return array|string|WP_Error|\WP_REST_Response $response Data.
652 1452 */
653 1453 protected function request_as_user( $path, $version = '2', $args = array(), $body = null, $base_api_path = 'wpcom', $use_cache = false ) {
654 1454 // Arrays are serialized without considering the order of objects, but it's okay atm.
655 - $cache_key = 'BLAZE_REST_RESP_' . md5( implode( '|', array( $path, $version, wp_json_encode( $args ), wp_json_encode( $body ), $base_api_path ) ) );
1455 + $cache_key = 'BLAZE_REST_RESP_' . md5( implode( '|', array( $path, $version, wp_json_encode( $args, JSON_UNESCAPED_SLASHES ), wp_json_encode( $body, JSON_UNESCAPED_SLASHES ), $base_api_path ) ) );
656 1456
657 1457 if ( $use_cache ) {
658 1458 $response_body_content = get_transient( $cache_key );
659 1459 if ( false !== $response_body_content ) {
@@ -674,12 +1474,18 @@
674 1474 }
675 1475
676 1476 $response_code = wp_remote_retrieve_response_code( $response );
677 1477 $response_body_content = wp_remote_retrieve_body( $response );
678 - $response_body = json_decode( $response_body_content, true );
1478 + $content_type = $response['headers']['content-type'] ?? '';
679 1479
1480 + if ( str_starts_with( $content_type, 'text/csv' ) ) {
1481 + return $response_body_content;
1482 + }
1483 +
1484 + $response_body = json_decode( $response_body_content, true );
1485 +
680 1486 if ( 200 !== $response_code ) {
681 - return $this->get_wp_error( $response_body, $response_code );
1487 + return $this->get_blaze_error( $response_body, $response_code );
682 1488 }
683 1489
684 1490 // Cache the successful JSON response for 5 minutes.
685 1491 set_transient( $cache_key, $response_body_content, 5 * MINUTE_IN_SECONDS );
@@ -702,11 +1508,17 @@
702 1508 * Build error object from remote response body and status code.
703 1509 *
704 1510 * @param array $response_body Remote response body.
705 1511 * @param int $response_code Http response code.
706 - * @return WP_Error
1512 + * @return \WP_REST_Response
707 1513 */
708 - protected function get_wp_error( $response_body, $response_code = 500 ) {
1514 + protected function get_blaze_error( $response_body, $response_code = 500 ) {
1515 + if ( ! is_array( $response_body ) ) {
1516 + $response_body = array(
1517 + 'errorMessage' => $response_body,
1518 + );
1519 + }
1520 +
709 1521 $error_code = 'remote-error';
710 1522 foreach ( array( 'code', 'error' ) as $error_code_key ) {
711 1523 if ( isset( $response_body[ $error_code_key ] ) ) {
712 1524 $error_code = $response_body[ $error_code_key ];
@@ -713,15 +1525,13 @@
713 1525 break;
714 1526 }
715 1527 }
716 1528
717 - $error_message = isset( $response_body['message'] ) ? $response_body['message'] : 'unknown remote error';
1529 + $response_body['code'] = $error_code;
1530 + $response_body['status'] = $response_code;
1531 + $response_body['errorMessage'] ??= 'Unknown remote error';
718 1532
719 - return new WP_Error(
720 - $error_code,
721 - $error_message,
722 - array( 'status' => $response_code )
723 - );
1533 + return new \WP_REST_Response( $response_body, $response_code );
724 1534 }
725 1535
726 1536 /**
727 1537 * Check if the current user is connected.
@@ -733,10 +1543,9 @@
733 1543 if ( ( new Host() )->is_wpcom_simple() ) {
734 1544 return true;
735 1545 }
736 1546
737 - $connection = new Connection_Manager();
738 - return $connection->is_connected() && $connection->is_user_connected();
1547 + return $this->connection->is_connected() && $this->connection->is_user_connected();
739 1548 }
740 1549
741 1550 /**
742 1551 * Get the site ID.
@@ -744,6 +1553,94 @@
744 1553 * @return int|WP_Error
745 1554 */
746 1555 private function get_site_id() {
747 1556 return Connection_Manager::get_site_id();
1557 + }
1558 +
1559 + /**
1560 + * Check if the Health status code is sync.
1561 + *
1562 + * @return bool True if is sync, false otherwise.
1563 + */
1564 + private function are_posts_ready(): bool {
1565 + // On WordPress.com Simple, Sync is not present, so we consider always ready.
1566 + if ( ( new Host() )->is_wpcom_simple() ) {
1567 + return true;
1568 + }
1569 +
1570 + return Health::STATUS_IN_SYNC === Health::get_status();
1571 + }
1572 +
1573 + /**
1574 + * Get the featured image data for a post.
1575 + *
1576 + * @param int $post_id The post ID.
1577 + *
1578 + * @return null|array {
1579 + * Featured image data, or null if no featured image exists.
1580 + *
1581 + * @type int $ID The attachment ID.
1582 + * @type string $URL The image URL.
1583 + * @type int $width The image width in pixels.
1584 + * @type int $height The image height in pixels.
1585 + * @type string $mime_type The image mime type (e.g., 'image/jpeg').
1586 + * }
1587 + */
1588 + private function get_post_featured_image( $post_id ) {
1589 + $thumbnail_id = get_post_thumbnail_id( $post_id );
1590 + if ( ! $thumbnail_id ) {
1591 + return null;
1592 + }
1593 +
1594 + $image_src = wp_get_attachment_image_src( $thumbnail_id, 'full' );
1595 + if ( ! $image_src ) {
1596 + return null;
1597 + }
1598 +
1599 + return array(
1600 + 'ID' => $thumbnail_id,
1601 + 'URL' => $image_src[0],
1602 + 'width' => $image_src[1],
1603 + 'height' => $image_src[2],
1604 + 'mime_type' => get_post_mime_type( $thumbnail_id ),
1605 + );
1606 + }
1607 +
1608 + /**
1609 + * Extract site ID and post ID from a WordPress.com URN.
1610 + *
1611 + * Parses a URN in the format "urn:wpcom:post:SITE_ID:POST_ID" and returns
1612 + * an associative array containing the site ID and post ID components.
1613 + *
1614 + * @param string $urn The URN string to parse (e.g., "urn:wpcom:post:12345:67890").
1615 + * @return array {
1616 + * Associative array containing the parsed URN components.
1617 + *
1618 + * @type int $site_id The WordPress.com site ID.
1619 + * @type int $post_id The post ID.
1620 + * }
1621 + */
1622 + private function get_data_from_urn( $urn ) {
1623 + $default = array(
1624 + 'site_id' => 0,
1625 + 'post_id' => 0,
1626 + );
1627 +
1628 + if ( empty( $urn ) ) {
1629 + return $default;
1630 + }
1631 +
1632 + $urn_parts = explode( ':', $urn );
1633 +
1634 + if ( count( $urn_parts ) < 5 ) {
1635 + return $default;
1636 + }
1637 +
1638 + $site_id = (int) $urn_parts[3];
1639 + $post_id = (int) $urn_parts[4];
1640 +
1641 + return array(
1642 + 'site_id' => $site_id,
1643 + 'post_id' => $post_id,
1644 + );
748 1645 }
749 1646 }