Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
61.38% covered (warning)
61.38%
89 / 145
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Render_Messages_Controller
61.54% covered (warning)
61.54%
88 / 143
40.00% covered (danger)
40.00%
2 / 5
20.19
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
1
 register_routes
100.00% covered (success)
100.00%
70 / 70
100.00% covered (success)
100.00%
1 / 1
1
 get_item_schema
0.00% covered (danger)
0.00%
0 / 29
0.00% covered (danger)
0.00%
0 / 1
2
 permissions_check
53.85% covered (warning)
53.85%
7 / 13
0.00% covered (danger)
0.00%
0 / 1
7.46
 render_messages
16.67% covered (danger)
16.67%
4 / 24
0.00% covered (danger)
0.00%
0 / 1
13.26
1<?php
2/**
3 * Publicize: Render Messages Controller
4 *
5 * @package automattic/jetpack-publicize
6 */
7
8namespace Automattic\Jetpack\Publicize\REST_API;
9
10use Automattic\Jetpack\Connection\Traits\WPCOM_REST_API_Proxy_Request;
11use Automattic\Jetpack\Publicize\Publicize_Utils as Utils;
12use WP_Error;
13use WP_REST_Request;
14use WP_REST_Server;
15
16if ( ! defined( 'ABSPATH' ) ) {
17    exit( 0 );
18}
19
20/**
21 * Publicize: Render Messages Controller class.
22 *
23 * Renders Publicize message templates for a given post and a batch of
24 * connection inputs in a single request, so the block-editor preview can
25 * fetch all enabled connections' previews in one round-trip when the
26 * `social-message-templates` feature is enabled.
27 *
28 * POST takes a JSON body of `{ post_id, items: [...], post_intent: {...} }`
29 * and returns one record per input item, in input order, keyed by the
30 * client-supplied `connection_id`. Body-based POST is used instead of a GET
31 * collection so multi-connection / long-message batches don't hit
32 * infrastructure URL caps.
33 *
34 * @phan-constructor-used-for-side-effects
35 */
36class Render_Messages_Controller extends Base_Controller {
37
38    use WPCOM_REST_API_Proxy_Request;
39
40    /**
41     * Constructor.
42     */
43    public function __construct() {
44        parent::__construct();
45
46        $this->base_api_path = 'wpcom';
47        $this->version       = 'v2';
48
49        $this->namespace = "{$this->base_api_path}/{$this->version}";
50        $this->rest_base = 'publicize/render-messages';
51
52        $this->allow_requests_as_blog = true;
53
54        add_action( 'rest_api_init', array( $this, 'register_routes' ) );
55    }
56
57    /**
58     * Register the routes.
59     */
60    public function register_routes() {
61        register_rest_route(
62            $this->namespace,
63            '/' . $this->rest_base,
64            array(
65                'methods'                        => WP_REST_Server::CREATABLE,
66                'callback'                       => array( $this, 'render_messages' ),
67                'permission_callback'            => array( $this, 'permissions_check' ),
68                'private_site_security_settings' => array(
69                    'allow_blog_token_access' => true,
70                ),
71                'args'                           => array(
72                    'post_id'     => array(
73                        'description' => __( 'The ID of the post to render the messages for.', 'jetpack-publicize-pkg' ),
74                        'type'        => 'integer',
75                        'required'    => true,
76                    ),
77                    'items'       => array(
78                        'description' => __( 'List of per-connection render inputs.', 'jetpack-publicize-pkg' ),
79                        'type'        => 'array',
80                        'required'    => true,
81                        'minItems'    => 1,
82                        'items'       => array(
83                            'type'                 => 'object',
84                            'additionalProperties' => false,
85                            'required'             => array( 'connection_id' ),
86                            'properties'           => array(
87                                'connection_id'  => array(
88                                    'description' => __( 'Publicize connection ID â€” used to dispatch the renderer and resolve the per-connection template.', 'jetpack-publicize-pkg' ),
89                                    'type'        => 'string',
90                                ),
91                                'message'        => array(
92                                    'description' => __( 'Optional message override. Empty walks the per-connection / site / network-default chain.', 'jetpack-publicize-pkg' ),
93                                    'type'        => 'string',
94                                    'default'     => '',
95                                ),
96                                'is_social_post' => array(
97                                    'description' => __( 'Whether the post will be shared as a social post (media attached) rather than a link share.', 'jetpack-publicize-pkg' ),
98                                    'type'        => 'boolean',
99                                    'default'     => false,
100                                ),
101                            ),
102                        ),
103                    ),
104                    'post_intent' => array(
105                        'description'          => __( 'Edited post fields to use when rendering unsaved preview changes.', 'jetpack-publicize-pkg' ),
106                        'type'                 => 'object',
107                        'default'              => array(),
108                        'additionalProperties' => false,
109                        'properties'           => array(
110                            'title'   => array(
111                                'description' => __( 'Edited post title.', 'jetpack-publicize-pkg' ),
112                                'type'        => 'string',
113                                'default'     => '',
114                            ),
115                            'excerpt' => array(
116                                'description' => __( 'Edited post excerpt.', 'jetpack-publicize-pkg' ),
117                                'type'        => 'string',
118                                'default'     => '',
119                            ),
120                            'content' => array(
121                                'description' => __( 'Edited post content.', 'jetpack-publicize-pkg' ),
122                                'type'        => 'string',
123                                'default'     => '',
124                            ),
125                        ),
126                    ),
127                ),
128                'schema'                         => array( $this, 'get_public_item_schema' ),
129            )
130        );
131    }
132
133    /**
134     * Retrieves the JSON schema for a single rendered-message item.
135     *
136     * @return array Schema data.
137     */
138    public function get_item_schema() {
139        return array(
140            '$schema'    => 'http://json-schema.org/draft-04/schema#',
141            'title'      => 'publicize-render-messages-item',
142            'type'       => 'object',
143            'properties' => array(
144                'connection_id'    => array(
145                    'description' => __( 'Connection identifier echoed back from the request.', 'jetpack-publicize-pkg' ),
146                    'type'        => 'string',
147                    'readonly'    => true,
148                    'context'     => array( 'view', 'edit' ),
149                ),
150                'rendered_message' => array(
151                    'description' => __( 'The rendered message for this item. Absent when the item failed to render.', 'jetpack-publicize-pkg' ),
152                    'type'        => 'string',
153                    'readonly'    => true,
154                    'context'     => array( 'view', 'edit' ),
155                ),
156                'error'            => array(
157                    'description' => __( 'Per-item error. Present only when this item failed to render.', 'jetpack-publicize-pkg' ),
158                    'type'        => 'object',
159                    'readonly'    => true,
160                    'context'     => array( 'view', 'edit' ),
161                    'properties'  => array(
162                        'code'    => array( 'type' => 'string' ),
163                        'message' => array( 'type' => 'string' ),
164                    ),
165                ),
166            ),
167        );
168    }
169
170    /**
171     * Permission check.
172     *
173     * Preserves the blog-token proxy path via Base_Controller::publicize_permissions_check()
174     * (which returns true for authorized blog requests when allow_requests_as_blog is set),
175     * and enforces post-level `edit_post` capability for regular user requests.
176     *
177     * @param WP_REST_Request $request Full details about the request.
178     * @return true|WP_Error True if authorized, WP_Error otherwise.
179     */
180    public function permissions_check( $request ) {
181        $base_check = $this->publicize_permissions_check();
182
183        if ( is_wp_error( $base_check ) ) {
184            return $base_check;
185        }
186
187        // Blog-token proxy requests don't have a user context; the publicize check
188        // above already returned true for those.
189        if ( self::is_authorized_blog_request() ) {
190            return true;
191        }
192
193        $post_id = (int) $request->get_param( 'post_id' );
194
195        if ( ! $post_id || ! current_user_can( 'edit_post', $post_id ) ) {
196            return new WP_Error(
197                'invalid_user_permission_publicize',
198                __( 'Sorry, you are not allowed to access Jetpack Social data for this post.', 'jetpack-publicize-pkg' ),
199                array( 'status' => rest_authorization_required_code() )
200            );
201        }
202
203        return true;
204    }
205
206    /**
207     * Render the messages for the given post and items.
208     *
209     * Top-level errors (feature off, post not found) short-circuit the whole batch.
210     * Per-item failures are returned as `{ id, error: { code, message } }` so a
211     * single bad item never fails the batch.
212     *
213     * @param WP_REST_Request $request The request object.
214     * @return mixed The rendered items array, or a WP_Error for top-level failures.
215     */
216    public function render_messages( $request ) {
217        if ( Utils::is_wpcom() ) {
218            require_lib( 'publicize/util/message-templates' );
219
220            if ( ! \Publicize\is_message_templates_enabled() ) {
221                return new WP_Error(
222                    'feature_not_enabled',
223                    __( 'Publicize message templates are not enabled for this site.', 'jetpack-publicize-pkg' ),
224                    array( 'status' => 403 )
225                );
226            }
227
228            $post_id = (int) $request->get_param( 'post_id' );
229            $items   = (array) $request->get_param( 'items' );
230            $intent  = (array) $request->get_param( 'post_intent' );
231
232            $post = get_post( $post_id );
233
234            if ( ! $post ) {
235                return new WP_Error(
236                    'post_not_found',
237                    __( 'Post not found.', 'jetpack-publicize-pkg' ),
238                    array( 'status' => 404 )
239                );
240            }
241
242            return rest_ensure_response(
243                \Publicize\render_messages( $post, $items, $intent )
244            );
245        }
246
247        // Self-hosted Jetpack: proxy the request body to WPCOM.
248        return rest_ensure_response(
249            $this->proxy_request_to_wpcom_as_blog( $request )
250        );
251    }
252}