PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.2
Jetpack – WP Security, Backup, Speed, & Growth v16.2
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 13.8.3 13.9.2 14.0.1 14.1.1 14.2.2 All 502 releases
jetpack / _inc / lib / core-api / wpcom-endpoints / class-wpcom-rest-api-v2-endpoint-email-editor-bootstrap.php

class-wpcom-rest-api-v2-endpoint-email-editor-bootstrap.php in Jetpack – WP Security, Backup, Speed, & Growth 16.2, at _inc/lib/core-api/wpcom-endpoints/class-wpcom-rest-api-v2-endpoint-email-editor-bootstrap.php

272 lines 10.0 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Email editor bootstrap endpoint for the WordPress.com REST API.
4 *
5 * @package automattic/jetpack
6 */
7
8 use Automattic\Jetpack\Connection\Manager;
9 use Automattic\Jetpack\Connection\Traits\WPCOM_REST_API_Proxy_Request;
10 use Automattic\Jetpack\Status\Host;
11
12 if ( ! defined( 'ABSPATH' ) ) {
13 exit( 0 );
14 }
15
16 /**
17 * Class WPCOM_REST_API_V2_Endpoint_Email_Editor_Bootstrap
18 *
19 * Serves the newsletter email design screen its data, and saves the design back.
20 *
21 * The screen ships in this plugin, but the design it edits lives on the WordPress.com shadow blog,
22 * so the record is not in the site's own database on Atomic or self-hosted. Declaring the route here
23 * means the browser calls one local `/wpcom/v2/` URL on all three platforms, and this class decides
24 * whether that is answered in-process (Simple) or proxied (everywhere else).
25 *
26 * Nothing about the email engine lives in this plugin: both callbacks apply a filter and return what
27 * comes back. WordPress.com implements those filters, exactly as it implements
28 * `jetpack_generate_email_preview_html` for the email preview endpoint next door.
29 */
30 class WPCOM_REST_API_V2_Endpoint_Email_Editor_Bootstrap extends WP_REST_Controller {
31
32 use WPCOM_REST_API_Proxy_Request;
33
34 /**
35 * Constructor.
36 */
37 public function __construct() {
38 $this->base_api_path = 'wpcom';
39 $this->version = 'v2';
40 $this->namespace = $this->base_api_path . '/' . $this->version;
41 $this->rest_base = '/email-editor-bootstrap';
42 $this->wpcom_is_wpcom_only_endpoint = true;
43 $this->wpcom_is_site_specific_endpoint = true;
44
45 add_action( 'rest_api_init', array( $this, 'register_routes' ) );
46 }
47
48 /**
49 * Registers the routes for the email editor's data layer.
50 *
51 * @see register_rest_route()
52 */
53 public function register_routes() {
54 // Off Simple the record lives on a database this site cannot reach, so every method proxies.
55 // The trait forwards the request's own method, query args and body, so one callback serves
56 // both the read and the write.
57 $is_simple = ( new Host() )->is_wpcom_simple();
58
59 register_rest_route(
60 $this->namespace,
61 $this->rest_base,
62 array(
63 array(
64 'show_in_index' => true,
65 'methods' => WP_REST_Server::READABLE,
66 'callback' => $is_simple
67 ? array( $this, 'get_bootstrap_data' )
68 : array( $this, 'proxy_request_to_wpcom_as_user' ),
69 'permission_callback' => array( $this, 'permissions_check' ),
70 'args' => array(
71 'template_slug' => array(
72 'description' => __( 'Slug of the email template to open in the editor.', 'jetpack' ),
73 'type' => 'string',
74 ),
75 ),
76 ),
77 array(
78 // The editor sends POST and the server has been observed receiving PUT, so this
79 // takes the constant covering both rather than betting on either.
80 'show_in_index' => true,
81 'methods' => WP_REST_Server::EDITABLE,
82 'callback' => $is_simple
83 ? array( $this, 'save_design' )
84 : array( $this, 'proxy_request_to_wpcom_as_user' ),
85 'permission_callback' => array( $this, 'permissions_check' ),
86 'args' => array(
87 'design' => array(
88 'description' => __( 'The email design document to store.', 'jetpack' ),
89 'type' => 'object',
90 'required' => true,
91 ),
92 ),
93 ),
94 )
95 );
96 }
97
98 /**
99 * Checks that the user may read and write the site's email design.
100 *
101 * `edit_theme_options` rather than `edit_posts`: this is the same shape of data core's
102 * `WP_REST_Global_Styles_Controller` guards with that capability, and it is what the design
103 * screen itself requires. It also matters more than a read-only route would — building the
104 * bundle creates a global styles scaffold row on WordPress.com the first time it is called for a
105 * blog, so a low bar here would let anyone make WordPress.com write rows on any blog they can name.
106 *
107 * @return true|WP_Error True if the request may proceed, WP_Error otherwise.
108 */
109 public function permissions_check() {
110 if ( ! ( new Host() )->is_wpcom_simple() && ! ( new Manager() )->is_user_connected() ) {
111 // The proxy refuses an unconnected user too, and returns this same error when it does, so
112 // this changes nothing observable today. It is here because the proxy refuses only while it
113 // is called with no blog-token fallback: without this, a change to that default inside the
114 // connection package would quietly start serving unconnected users on the site's token.
115 return new WP_Error(
116 'rest_unauthorized',
117 __( 'Please connect your user account to WordPress.com', 'jetpack' ),
118 array( 'status' => rest_authorization_required_code() )
119 );
120 }
121
122 if ( ! current_user_can( 'edit_theme_options' ) ) {
123 return new WP_Error(
124 'rest_forbidden',
125 __( 'Sorry, you are not allowed to edit this site&#8217;s email design.', 'jetpack' ),
126 array( 'status' => rest_authorization_required_code() )
127 );
128 }
129
130 return true;
131 }
132
133 /**
134 * Returns the data the email editor needs to start.
135 *
136 * @param WP_REST_Request $request Full data about the request.
137 *
138 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
139 */
140 public function get_bootstrap_data( $request ) {
141 try {
142 /**
143 * Filters the data the newsletter email editor needs to start.
144 *
145 * Returns the editor's settings, its theme, the resolved canvas template, the available
146 * personalization tags and the blog's saved design. Unfiltered on a site with nothing
147 * implementing it, which the endpoint reports as unavailable rather than as an empty design.
148 *
149 * Internal, and not settled: the editor this serves is still being built, so the shape of
150 * what comes back is expected to change. Do not depend on it from outside the plugin.
151 *
152 * @since 16.2
153 * @access private
154 *
155 * @param array|WP_Error|null $data The editor's bootstrap data. Null until something provides one.
156 * @param WP_REST_Request $request The REST request.
157 */
158 $data = apply_filters( 'jetpack_email_editor_bootstrap', null, $request );
159 } catch ( Throwable $e ) {
160 return $this->unexpected_error( $e );
161 }
162
163 return $this->respond( $data );
164 }
165
166 /**
167 * Saves the site's email design.
168 *
169 * @param WP_REST_Request $request Full data about the request.
170 *
171 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
172 */
173 public function save_design( $request ) {
174 try {
175 /**
176 * Filters the result of saving the newsletter email design.
177 *
178 * The editor sends the whole styles document rather than a patch, so implementations
179 * replace rather than merge. Implementations should report on a read-back of the stored
180 * design rather than echoing the submitted one: sanitizing drops properties outside the
181 * theme.json schema, so a save can succeed into invisibility, and the screen has to be
182 * able to tell a person their edit did not survive.
183 *
184 * Internal, and not settled, for the same reason as the read above: expect the shape of the
185 * design document and of what is returned to change while the editor is being built.
186 *
187 * @since 16.2
188 * @access private
189 *
190 * @param array|WP_Error|null $result The stored design. Null until something provides one.
191 * @param WP_REST_Request $request The REST request.
192 */
193 $result = apply_filters( 'jetpack_email_editor_save_design', null, $request );
194 } catch ( Throwable $e ) {
195 return $this->unexpected_error( $e );
196 }
197
198 return $this->respond( $result );
199 }
200
201 /**
202 * Turns a filtered value into a response.
203 *
204 * Anything that is not an array or a `WP_Error` is treated as no answer at all, because this data
205 * layer exists to stop a design silently reading or saving as nothing. An unfiltered `null` means
206 * nothing implements the filter on this site — the plugin is running somewhere its WordPress.com
207 * half has not shipped. `false` means an implementation failed and said so the way PHP usually
208 * does. Returning either bare would be indistinguishable from a site whose email design is
209 * genuinely empty, so both report as unavailable instead.
210 *
211 * @param array|WP_Error|null $value The filtered value.
212 *
213 * @return WP_REST_Response|WP_Error Response object on success, or WP_Error object on failure.
214 */
215 private function respond( $value ) {
216 if ( is_wp_error( $value ) ) {
217 return $value;
218 }
219
220 if ( ! is_array( $value ) ) {
221 return new WP_Error(
222 'email_editor_unavailable',
223 __( 'The email editor is not available on this site.', 'jetpack' ),
224 array( 'status' => 501 )
225 );
226 }
227
228 return rest_ensure_response( $value );
229 }
230
231 /**
232 * Turns a throwing filter into an error response.
233 *
234 * On Simple the filter runs in this process, so an implementation that raises would otherwise
235 * surface as a bare fatal with nothing for the screen to show. Off Simple the request is proxied
236 * and WordPress.com catches its own.
237 *
238 * @param Throwable $e The exception raised while filtering.
239 *
240 * @return WP_Error
241 */
242 private function unexpected_error( $e ) {
243 /**
244 * Fires when a filter serving the email editor raises.
245 *
246 * This plugin does not record the exception itself — the message can carry internals, so where
247 * it is safe to write it is the host's call rather than ours. Hook this to log it.
248 *
249 * @since 16.2
250 *
251 * @param Throwable $e The exception raised while filtering.
252 */
253 do_action( 'jetpack_email_editor_error', $e );
254
255 $data = array( 'status' => 500 );
256
257 if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) {
258 // Without this the raise is invisible: the message is the only thing separating a bug in
259 // the implementation from an outage. Debug builds only, since it can carry internals.
260 $data['exception'] = $e->getMessage();
261 }
262
263 return new WP_Error(
264 'email_editor_failed',
265 __( 'The email editor could not be reached. Please try again.', 'jetpack' ),
266 $data
267 );
268 }
269 }
270
271 wpcom_rest_api_v2_load_plugin( 'WPCOM_REST_API_V2_Endpoint_Email_Editor_Bootstrap' );
272