PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.3-a.7
Jetpack – WP Security, Backup, Speed, & Growth v16.3-a.7
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 13.8.3 All 506 releases
jetpack / modules / subscriptions / email-design-editor / class-jetpack-email-design-editor.php

class-jetpack-email-design-editor.php in Jetpack – WP Security, Backup, Speed, & Growth 16.3-a.7, at modules/subscriptions/email-design-editor/class-jetpack-email-design-editor.php

372 lines 11.5 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * The newsletter email design screen.
4 *
5 * @package automattic/jetpack
6 */
7
8 use Automattic\Jetpack\Feature_Flags\Feature_Flags;
9
10 if ( ! defined( 'ABSPATH' ) ) {
11 exit( 0 );
12 }
13
14 /**
15 * Registers an Appearance page that mounts the WooCommerce email editor against the
16 * newsletter template, so a creator sets their email design once for the whole site.
17 *
18 * The design itself lives on the WordPress.com shadow blog: the browser fetches it from
19 * `/wpcom/v2/email-editor-bootstrap` and this page supplies only what describes *this*
20 * installation. See NL-839.
21 */
22 class Jetpack_Email_Design_Editor {
23
24 /**
25 * The `page` query arg the screen answers to.
26 */
27 const PAGE_SLUG = 'jetpack-email-design';
28
29 /**
30 * The feature flag gating the screen. Registered off, forced on for testing with
31 * `wp companion feature-flag enable jetpack-email-design`.
32 */
33 const FEATURE_FLAG = 'jetpack-email-design';
34
35 /**
36 * The script handle, and the id of the element the editor mounts into.
37 */
38 const HANDLE = 'jetpack-email-design-editor';
39
40 /**
41 * Flags for JSON handed to a `<script>` tag.
42 */
43 const JSON_FLAGS = JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP;
44
45 /**
46 * The hook suffix `add_theme_page()` returned, or null when the page is not registered.
47 *
48 * @var string|null
49 */
50 private static $hook_suffix = null;
51
52 /**
53 * Wire the screen up.
54 */
55 public static function init() {
56 // Registered here rather than on `admin_menu` so the flag exists under WP-CLI, REST
57 // and cron too — `wp companion feature-flag list` reads it from one of those.
58 self::register_feature_flags();
59
60 add_action( 'admin_menu', array( __CLASS__, 'add_admin_page' ) );
61 }
62
63 /**
64 * Declare the screen's feature flag.
65 */
66 public static function register_feature_flags() {
67 Feature_Flags::register(
68 self::FEATURE_FLAG,
69 array(
70 'default' => false,
71 'description' => 'Edit the newsletter email design in wp-admin, under Appearance.',
72 'owner' => 'jetpack-newsletter',
73 )
74 );
75 }
76
77 /**
78 * Whether the screen should exist on this site.
79 *
80 * The WordPress.com `email-design-editor` sticker gates the bootstrap endpoint, not this
81 * page: a Jetpack site cannot read stickers without an API call, so the screen carries
82 * its own gate.
83 *
84 * @return bool
85 */
86 public static function is_enabled() {
87 return Feature_Flags::is_enabled( self::FEATURE_FLAG );
88 }
89
90 /**
91 * Add the screen under Appearance.
92 */
93 public static function add_admin_page() {
94 if ( ! self::is_enabled() ) {
95 return;
96 }
97
98 self::$hook_suffix = add_theme_page(
99 __( 'Email Design', 'jetpack' ),
100 __( 'Email Design', 'jetpack' ),
101 'edit_theme_options',
102 self::PAGE_SLUG,
103 array( __CLASS__, 'render' )
104 );
105
106 if ( self::$hook_suffix ) {
107 add_action( 'load-' . self::$hook_suffix, array( __CLASS__, 'on_load' ) );
108 }
109 }
110
111 /**
112 * Scope everything else to this one screen.
113 */
114 public static function on_load() {
115 add_action( 'admin_enqueue_scripts', array( __CLASS__, 'enqueue_assets' ) );
116 add_filter( 'admin_body_class', array( __CLASS__, 'add_fullscreen_body_class' ) );
117 }
118
119 /**
120 * Put the screen in fullscreen mode before the bundle has loaded.
121 *
122 * `wp-editor` hides the admin menu on this class, and the editor sets it from a React
123 * component — so it cannot apply until the bundle has loaded and the bootstrap has answered,
124 * and the page visibly reflows a few seconds in. Setting it here makes fullscreen the layout
125 * from first paint. The admin bar is untouched, which keeps a way out if the editor fails.
126 *
127 * @param string $classes Space-separated admin body classes.
128 * @return string
129 */
130 public static function add_fullscreen_body_class( $classes ) {
131 return trim( $classes . ' is-fullscreen-mode' );
132 }
133
134 /**
135 * Enqueue the editor bundle and the block-editor assets it expects to find.
136 */
137 public static function enqueue_assets() {
138 $asset_path = JETPACK__PLUGIN_DIR . '_inc/build/email-design-editor.asset.php';
139
140 if ( ! file_exists( $asset_path ) ) {
141 return;
142 }
143
144 $asset = include $asset_path;
145
146 self::enqueue_block_editor_assets();
147
148 // `@woocommerce/email-editor` opts into core's private APIs as `@wordpress/edit-site`,
149 // resolved against the site's `wp-private-apis`. Re-check on a package bump. NL-839 (j).
150 wp_enqueue_script(
151 self::HANDLE,
152 plugins_url( '_inc/build/email-design-editor.js', JETPACK__PLUGIN_FILE ),
153 $asset['dependencies'],
154 $asset['version'],
155 true
156 );
157 wp_set_script_translations( self::HANDLE, 'jetpack' );
158
159 // `wp-editor`, `wp-block-editor` and `wp-preferences` are what lay the editor's frame
160 // out; without them every region stacks into one narrow column. Keep the list to
161 // handles WordPress registers — an unregistered one drops this stylesheet silently,
162 // which is `wp-interface`'s trap.
163 wp_enqueue_style(
164 self::HANDLE,
165 plugins_url( '_inc/build/email-design-editor.css', JETPACK__PLUGIN_FILE ),
166 array(
167 'wp-components',
168 'wp-block-editor',
169 'wp-editor',
170 'wp-edit-blocks',
171 'wp-preferences',
172 'wp-format-library',
173 ),
174 $asset['version']
175 );
176 wp_style_add_data( self::HANDLE, 'rtl', 'replace' );
177 wp_add_inline_style( self::HANDLE, self::get_layout_css() );
178
179 wp_add_inline_script(
180 self::HANDLE,
181 'window.JetpackEmailDesignEditor = ' . wp_json_encode( self::get_screen_data(), self::JSON_FLAGS ) . ';',
182 'before'
183 );
184 }
185
186 /**
187 * Fill the screen with the editor.
188 *
189 * The editor's frame expects a viewport, not the flow of an admin page: inside the usual
190 * wp-admin content column it collapses to a fraction of the height and scrolls its own
191 * regions. Woo avoids this by running on `post.php`, which is already fullscreen.
192 *
193 * @return string
194 */
195 private static function get_layout_css() {
196 return '
197 #wpcontent { padding-inline-start: 0; }
198 #wpfooter { display: none; }
199 #' . self::HANDLE . ' {
200 position: fixed;
201 /* Below #wpadminbar (99999) so the editor\'s own popovers and snackbars, which ask
202 for 100000, cannot cover it — this caps everything inside. The admin menu is
203 hidden on this screen, so there is nothing else left to clear. */
204 z-index: 99990;
205 inset-block: var(--wp-admin--admin-bar--height, 32px) 0;
206 inset-inline: 0;
207 }
208 /* wp-edit-post pins this and the screen does not load it, so without this the
209 snackbar renders in flow beside the header instead of over the canvas. */
210 #' . self::HANDLE . ' .components-editor-notices__snackbar {
211 position: absolute;
212 inset-block-end: 20px;
213 inset-inline-start: 20px;
214 }
215 ';
216 }
217
218 /**
219 * Reproduce what the package's `Assets_Manager` does on WooCommerce's own screen.
220 *
221 * None of this happens automatically on a custom admin page: without it the editor
222 * mounts against no block library, no block categories and no server-side block
223 * definitions. See NL-839 (a).
224 *
225 * @todo Firing `enqueue_block_editor_assets` wholesale is the leading suspect for the
226 * second Styles button — it pulls in core's site-editing global styles UI. NL-839 (e).
227 */
228 private static function enqueue_block_editor_assets() {
229 // Named rather than built from a post: there is no post here, and `get_block_categories()`
230 // hands whatever it gets to filters that type-hint the context.
231 $context = new WP_Block_Editor_Context( array( 'name' => 'jetpack/email-design' ) );
232
233 wp_enqueue_media();
234
235 do_action( 'enqueue_block_assets' );
236 do_action( 'enqueue_block_editor_assets' );
237
238 wp_enqueue_style( 'wp-edit-blocks' );
239 wp_enqueue_style( 'wp-format-library' );
240
241 wp_add_inline_script(
242 'wp-blocks',
243 sprintf( 'wp.blocks.setCategories( %s );', wp_json_encode( get_block_categories( $context ), self::JSON_FLAGS ) ),
244 'after'
245 );
246 wp_add_inline_script(
247 'wp-blocks',
248 sprintf(
249 'wp.blocks.unstable__bootstrapServerSideBlockDefinitions( %s );',
250 wp_json_encode( get_block_editor_server_block_settings(), self::JSON_FLAGS )
251 ),
252 'after'
253 );
254 }
255
256 /**
257 * What the page hands the bundle, as `window.JetpackEmailDesignEditor`.
258 *
259 * Every WordPress.com id — the template's, the global-styles record's — comes from the
260 * bootstrap response instead, because they are namespaced to the shadow blog's theme and
261 * a locally computed one is right on Simple and wrong everywhere else. See NL-839 (c).
262 *
263 * @return array
264 */
265 private static function get_screen_data() {
266 return array(
267 'elementId' => self::HANDLE,
268 'editorSettings' => self::get_iframe_asset_settings(),
269
270 // The editor assigns these to `window.location.href` from its header buttons.
271 // Both point at Appearance until the screen has a real entry point (NL-844).
272 'urls' => array(
273 'back' => admin_url( 'themes.php' ),
274 'listings' => admin_url( 'themes.php' ),
275 ),
276 'userEmail' => wp_get_current_user()->user_email,
277 );
278 }
279
280 /**
281 * The two editor settings that describe this installation rather than the design.
282 *
283 * WordPress.com strips both from the bootstrap bundle, because there they would name
284 * WordPress.com's own asset URLs and push them into the site's canvas.
285 *
286 * @return array
287 */
288 private static function get_iframe_asset_settings() {
289 // Absent before WP 6.3, and private, but it is what core's own block editors call to
290 // resolve the assets an iframed canvas needs.
291 if ( ! function_exists( '_wp_get_iframed_editor_assets' ) ) {
292 return array();
293 }
294
295 $handles = self::get_allowed_iframe_style_handles();
296
297 return array(
298 '__unstableResolvedAssets' => self::get_resolved_assets( $handles ),
299 'allowedIframeStyleHandles' => $handles,
300 );
301 }
302
303 /**
304 * The stylesheet handles the canvas is allowed to keep.
305 *
306 * Mirrors the package's `Settings_Controller::get_allowed_iframe_style_handles()`. An empty
307 * list is not a no-op: the client strips every stylesheet not named here, so omitting this
308 * leaves the canvas painted in the site's own styles rather than the email's.
309 *
310 * @return string[]
311 */
312 private static function get_allowed_iframe_style_handles() {
313 $handles = array(
314 'wp-components-css',
315 'wp-reset-editor-styles-css',
316 'wp-block-library-css',
317 'wp-block-editor-content-css',
318 'wp-edit-blocks-css',
319 );
320
321 // Registration args reach the block type verbatim — `WP_Block_Type::set_props()` normalizes
322 // only `attributes`, and `register_block_type_args` can rewrite the rest — so a block
323 // declaring a bare string here would otherwise fatal the screen inside `array_merge()`.
324 foreach ( WP_Block_Type_Registry::get_instance()->get_all_registered() as $block ) {
325 if ( ! is_array( $block->supports ) || empty( $block->supports['email'] ) ) {
326 continue;
327 }
328
329 foreach ( array_merge( (array) $block->style_handles, (array) $block->editor_style_handles ) as $handle ) {
330 if ( is_string( $handle ) ) {
331 $handles[] = $handle . '-css';
332 }
333 }
334 }
335
336 return $handles;
337 }
338
339 /**
340 * The iframe assets, trimmed to the allowed handles.
341 *
342 * @param string[] $allowed Handles to keep.
343 * @return array The `_wp_get_iframed_editor_assets()` shape, with `styles` filtered.
344 */
345 private static function get_resolved_assets( array $allowed ) {
346 $assets = _wp_get_iframed_editor_assets();
347 $kept = array();
348
349 foreach ( explode( "\n", (string) $assets['styles'] ) as $asset ) {
350 foreach ( $allowed as $handle ) {
351 if ( str_contains( $asset, $handle ) ) {
352 $kept[] = $asset;
353 break;
354 }
355 }
356 }
357
358 $assets['styles'] = implode( "\n", $kept );
359
360 return $assets;
361 }
362
363 /**
364 * Render the container the editor mounts into.
365 */
366 public static function render() {
367 printf( '<div id="%s" class="jetpack-email-design-editor"></div>', esc_attr( self::HANDLE ) );
368 }
369 }
370
371 Jetpack_Email_Design_Editor::init();
372