Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
55.97% covered (warning)
55.97%
89 / 159
40.00% covered (danger)
40.00%
2 / 5
CRAP
0.00% covered (danger)
0.00%
0 / 1
Render_Messages_Controller
56.05% covered (warning)
56.05%
88 / 157
40.00% covered (danger)
40.00%
2 / 5
24.22
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 / 43
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 on sites with
26 * social paid features.
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                'hyperlinks'       => array(
157                    'description' => __( 'Editor hyperlinks preserved from content or excerpt placeholders.', 'jetpack-publicize-pkg' ),
158                    'type'        => 'array',
159                    'readonly'    => true,
160                    'context'     => array( 'view', 'edit' ),
161                    'items'       => array(
162                        'type'       => 'object',
163                        'properties' => array(
164                            'text'       => array( 'type' => 'string' ),
165                            'href'       => array( 'type' => 'string' ),
166                            'occurrence' => array( 'type' => 'integer' ),
167                        ),
168                    ),
169                ),
170                'error'            => array(
171                    'description' => __( 'Per-item error. Present only when this item failed to render.', 'jetpack-publicize-pkg' ),
172                    'type'        => 'object',
173                    'readonly'    => true,
174                    'context'     => array( 'view', 'edit' ),
175                    'properties'  => array(
176                        'code'    => array( 'type' => 'string' ),
177                        'message' => array( 'type' => 'string' ),
178                    ),
179                ),
180            ),
181        );
182    }
183
184    /**
185     * Permission check.
186     *
187     * Preserves the blog-token proxy path via Base_Controller::publicize_permissions_check()
188     * (which returns true for authorized blog requests when allow_requests_as_blog is set),
189     * and enforces post-level `edit_post` capability for regular user requests.
190     *
191     * @param WP_REST_Request $request Full details about the request.
192     * @return true|WP_Error True if authorized, WP_Error otherwise.
193     */
194    public function permissions_check( $request ) {
195        $base_check = $this->publicize_permissions_check();
196
197        if ( is_wp_error( $base_check ) ) {
198            return $base_check;
199        }
200
201        // Blog-token proxy requests don't have a user context; the publicize check
202        // above already returned true for those.
203        if ( self::is_authorized_blog_request() ) {
204            return true;
205        }
206
207        $post_id = (int) $request->get_param( 'post_id' );
208
209        if ( ! $post_id || ! current_user_can( 'edit_post', $post_id ) ) {
210            return new WP_Error(
211                'invalid_user_permission_publicize',
212                __( 'Sorry, you are not allowed to access Jetpack Social data for this post.', 'jetpack-publicize-pkg' ),
213                array( 'status' => rest_authorization_required_code() )
214            );
215        }
216
217        return true;
218    }
219
220    /**
221     * Render the messages for the given post and items.
222     *
223     * Top-level errors (feature off, post not found) short-circuit the whole batch.
224     * Per-item failures are returned as `{ id, error: { code, message } }` so a
225     * single bad item never fails the batch.
226     *
227     * @param WP_REST_Request $request The request object.
228     * @return mixed The rendered items array, or a WP_Error for top-level failures.
229     */
230    public function render_messages( $request ) {
231        if ( Utils::is_wpcom() ) {
232            if ( ! wpcom_site_has_feature( \WPCOM_Features::SOCIAL_ENHANCED_PUBLISHING ) ) {
233                return new WP_Error(
234                    'feature_not_enabled',
235                    __( 'Publicize message templates are not enabled for this site.', 'jetpack-publicize-pkg' ),
236                    array( 'status' => 403 )
237                );
238            }
239
240            $post_id = (int) $request->get_param( 'post_id' );
241            $items   = (array) $request->get_param( 'items' );
242            $intent  = (array) $request->get_param( 'post_intent' );
243
244            $post = get_post( $post_id );
245
246            if ( ! $post ) {
247                return new WP_Error(
248                    'post_not_found',
249                    __( 'Post not found.', 'jetpack-publicize-pkg' ),
250                    array( 'status' => 404 )
251                );
252            }
253
254            require_lib( 'publicize/util/message-templates' );
255
256            return rest_ensure_response(
257                \Publicize\render_messages( $post, $items, $intent )
258            );
259        }
260
261        // Self-hosted Jetpack: proxy the request body to WPCOM.
262        return rest_ensure_response(
263            $this->proxy_request_to_wpcom_as_blog( $request )
264        );
265    }
266}