PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 11.0
Jetpack – WP Security, Backup, Speed, & Growth v11.0
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 14.3.1 All 501 releases
jetpack / class.jetpack-gutenberg.php

class.jetpack-gutenberg.php in Jetpack – WP Security, Backup, Speed, & Growth 11.0, at class.jetpack-gutenberg.php

1,207 lines 37.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php //phpcs:ignore WordPress.Files.FileName.InvalidClassFileName
2 /**
3 * Handles server-side registration and use of all blocks and plugins available in Jetpack for the block editor, aka Gutenberg.
4 * Works in tandem with client-side block registration via `index.json`
5 *
6 * @package automattic/jetpack
7 */
8
9 use Automattic\Jetpack\Assets;
10 use Automattic\Jetpack\Blocks;
11 use Automattic\Jetpack\Connection\Manager as Connection_Manager;
12 use Automattic\Jetpack\Constants;
13 use Automattic\Jetpack\Status;
14 use Automattic\Jetpack\Status\Host;
15
16 /**
17 * Wrapper function to safely register a gutenberg block type
18 *
19 * @deprecated 9.1.0 Use Automattic\\Jetpack\\Blocks::jetpack_register_block instead
20 *
21 * @see register_block_type
22 *
23 * @since 6.7.0
24 *
25 * @param string $slug Slug of the block.
26 * @param array $args Arguments that are passed into register_block_type.
27 *
28 * @return WP_Block_Type|false The registered block type on success, or false on failure.
29 */
30 function jetpack_register_block( $slug, $args = array() ) {
31 _deprecated_function( __METHOD__, '9.1.0', 'Automattic\\Jetpack\\Blocks::jetpack_register_block' );
32 return Blocks::jetpack_register_block( $slug, $args );
33 }
34
35 /**
36 * General Gutenberg editor specific functionality
37 */
38 class Jetpack_Gutenberg {
39
40 /**
41 * Only these extensions can be registered. Used to control availability of beta blocks.
42 *
43 * @var array Extensions allowed list.
44 */
45 private static $extensions = array();
46
47 /**
48 * Keeps track of the reasons why a given extension is unavailable.
49 *
50 * @var array Extensions availability information
51 */
52 private static $availability = array();
53
54 /**
55 * A cached array of the fully processed availability data. Keeps track of
56 * reasons why an extension is unavailable or missing.
57 *
58 * @var array Extensions availability information.
59 */
60 private static $cached_availability = null;
61
62 /**
63 * Site-specific features available.
64 * Their calculation can be expensive and slow, so we're caching it for the request.
65 *
66 * @var array Site-specific features
67 */
68 private static $site_specific_features = array();
69
70 /**
71 * Check to see if a minimum version of Gutenberg is available. Because a Gutenberg version is not available in
72 * php if the Gutenberg plugin is not installed, if we know which minimum WP release has the required version we can
73 * optionally fall back to that.
74 *
75 * @param array $version_requirements An array containing the required Gutenberg version and, if known, the WordPress version that was released with this minimum version.
76 * @param string $slug The slug of the block or plugin that has the gutenberg version requirement.
77 *
78 * @since 8.3.0
79 *
80 * @return boolean True if the version of gutenberg required by the block or plugin is available.
81 */
82 public static function is_gutenberg_version_available( $version_requirements, $slug ) {
83 global $wp_version;
84
85 // Bail if we don't at least have the gutenberg version requirement, the WP version is optional.
86 if ( empty( $version_requirements['gutenberg'] ) ) {
87 return false;
88 }
89
90 // If running a local dev build of gutenberg plugin GUTENBERG_DEVELOPMENT_MODE is set so assume correct version.
91 if ( defined( 'GUTENBERG_DEVELOPMENT_MODE' ) && GUTENBERG_DEVELOPMENT_MODE ) {
92 return true;
93 }
94
95 $version_available = false;
96
97 // If running a production build of the gutenberg plugin then GUTENBERG_VERSION is set, otherwise if WP version
98 // with required version of Gutenberg is known check that.
99 if ( defined( 'GUTENBERG_VERSION' ) ) {
100 $version_available = version_compare( GUTENBERG_VERSION, $version_requirements['gutenberg'], '>=' );
101 } elseif ( ! empty( $version_requirements['wp'] ) ) {
102 $version_available = version_compare( $wp_version, $version_requirements['wp'], '>=' );
103 }
104
105 if ( ! $version_available ) {
106 self::set_extension_unavailable(
107 $slug,
108 'incorrect_gutenberg_version',
109 array(
110 'required_feature' => $slug,
111 'required_version' => $version_requirements,
112 'current_version' => array(
113 'wp' => $wp_version,
114 'gutenberg' => defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : null,
115 ),
116 )
117 );
118 }
119
120 return $version_available;
121 }
122
123 /**
124 * Prepend the 'jetpack/' prefix to a block name
125 *
126 * @param string $block_name The block name.
127 *
128 * @return string The prefixed block name.
129 */
130 private static function prepend_block_prefix( $block_name ) {
131 return 'jetpack/' . $block_name;
132 }
133
134 /**
135 * Remove the 'jetpack/' or jetpack-' prefix from an extension name
136 *
137 * @param string $extension_name The extension name.
138 *
139 * @return string The unprefixed extension name.
140 */
141 public static function remove_extension_prefix( $extension_name ) {
142 if ( 0 === strpos( $extension_name, 'jetpack/' ) || 0 === strpos( $extension_name, 'jetpack-' ) ) {
143 return substr( $extension_name, strlen( 'jetpack/' ) );
144 }
145 return $extension_name;
146 }
147
148 /**
149 * Whether two arrays share at least one item
150 *
151 * @param array $a An array.
152 * @param array $b Another array.
153 *
154 * @return boolean True if $a and $b share at least one item
155 */
156 protected static function share_items( $a, $b ) {
157 return count( array_intersect( $a, $b ) ) > 0;
158 }
159
160 /**
161 * Set a (non-block) extension as available
162 *
163 * @param string $slug Slug of the extension.
164 */
165 public static function set_extension_available( $slug ) {
166 self::$availability[ self::remove_extension_prefix( $slug ) ] = true;
167 }
168
169 /**
170 * Set the reason why an extension (block or plugin) is unavailable
171 *
172 * @param string $slug Slug of the extension.
173 * @param string $reason A string representation of why the extension is unavailable.
174 * @param array $details A free-form array containing more information on why the extension is unavailable.
175 */
176 public static function set_extension_unavailable( $slug, $reason, $details = array() ) {
177 if (
178 // Extensions that require a plan may be eligible for upgrades.
179 'missing_plan' === $reason
180 && (
181 /**
182 * Filter 'jetpack_block_editor_enable_upgrade_nudge' with `true` to enable or `false`
183 * to disable paid feature upgrade nudges in the block editor.
184 *
185 * When this is changed to default to `true`, you should also update `modules/memberships/class-jetpack-memberships.php`
186 * See https://github.com/Automattic/jetpack/pull/13394#pullrequestreview-293063378
187 *
188 * @since 7.7.0
189 *
190 * @param boolean
191 */
192 ! apply_filters( 'jetpack_block_editor_enable_upgrade_nudge', false )
193 /** This filter is documented in _inc/lib/admin-pages/class.jetpack-react-page.php */
194 || ! apply_filters( 'jetpack_show_promotions', true )
195 )
196 ) {
197 // The block editor may apply an upgrade nudge if `missing_plan` is the reason.
198 // Add a descriptive suffix to disable behavior but provide informative reason.
199 $reason .= '__nudge_disabled';
200 }
201
202 self::$availability[ self::remove_extension_prefix( $slug ) ] = array(
203 'reason' => $reason,
204 'details' => $details,
205 );
206 }
207
208 /**
209 * Set up a list of allowed block editor extensions
210 *
211 * @return void
212 */
213 public static function init() {
214 if ( ! self::should_load() ) {
215 return;
216 }
217
218 /**
219 * Alternative to `JETPACK_BETA_BLOCKS`, set to `true` to load Beta Blocks.
220 *
221 * @since 6.9.0
222 *
223 * @param boolean
224 */
225 if ( apply_filters( 'jetpack_load_beta_blocks', false ) ) {
226 Constants::set_constant( 'JETPACK_BETA_BLOCKS', true );
227 }
228
229 /**
230 * Alternative to `JETPACK_EXPERIMENTAL_BLOCKS`, set to `true` to load Experimental Blocks.
231 *
232 * @since 8.4.0
233 *
234 * @param boolean
235 */
236 if ( apply_filters( 'jetpack_load_experimental_blocks', false ) ) {
237 Constants::set_constant( 'JETPACK_EXPERIMENTAL_BLOCKS', true );
238 }
239
240 /**
241 * Filter the list of block editor extensions that are available through Jetpack.
242 *
243 * @since 7.0.0
244 *
245 * @param array
246 */
247 self::$extensions = apply_filters( 'jetpack_set_available_extensions', self::get_available_extensions() );
248
249 /**
250 * Filter the list of block editor plugins that are available through Jetpack.
251 *
252 * @deprecated 7.0.0 Use jetpack_set_available_extensions instead
253 *
254 * @since 6.8.0
255 *
256 * @param array
257 */
258 self::$extensions = apply_filters( 'jetpack_set_available_blocks', self::$extensions );
259
260 /**
261 * Filter the list of block editor plugins that are available through Jetpack.
262 *
263 * @deprecated 7.0.0 Use jetpack_set_available_extensions instead
264 *
265 * @since 6.9.0
266 *
267 * @param array
268 */
269 self::$extensions = apply_filters( 'jetpack_set_available_plugins', self::$extensions );
270 }
271
272 /**
273 * Resets the class to its original state
274 *
275 * Used in unit tests
276 *
277 * @return void
278 */
279 public static function reset() {
280 self::$extensions = array();
281 self::$availability = array();
282 self::$cached_availability = null;
283 }
284
285 /**
286 * Return the Gutenberg extensions (blocks and plugins) directory
287 *
288 * @return string The Gutenberg extensions directory
289 */
290 public static function get_blocks_directory() {
291 /**
292 * Filter to select Gutenberg blocks directory
293 *
294 * @since 6.9.0
295 *
296 * @param string default: '_inc/blocks/'
297 */
298 return apply_filters( 'jetpack_blocks_directory', '_inc/blocks/' );
299 }
300
301 /**
302 * Checks for a given .json file in the blocks folder.
303 *
304 * @param string $preset The name of the .json file to look for.
305 *
306 * @return bool True if the file is found.
307 */
308 public static function preset_exists( $preset ) {
309 return file_exists( JETPACK__PLUGIN_DIR . self::get_blocks_directory() . $preset . '.json' );
310 }
311
312 /**
313 * Decodes JSON loaded from a preset file in the blocks folder
314 *
315 * @param string $preset The name of the .json file to load.
316 *
317 * @return mixed Returns an object if the file is present, or false if a valid .json file is not present.
318 */
319 public static function get_preset( $preset ) {
320 return json_decode(
321 // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
322 file_get_contents( JETPACK__PLUGIN_DIR . self::get_blocks_directory() . $preset . '.json' )
323 );
324 }
325
326 /**
327 * Returns a list of Jetpack Gutenberg extensions (blocks and plugins), based on index.json
328 *
329 * @return array A list of blocks: eg [ 'publicize', 'markdown' ]
330 */
331 public static function get_jetpack_gutenberg_extensions_allowed_list() {
332 $preset_extensions_manifest = self::preset_exists( 'index' )
333 ? self::get_preset( 'index' )
334 : (object) array();
335 $blocks_variation = self::blocks_variation();
336
337 return self::get_extensions_preset_for_variation( $preset_extensions_manifest, $blocks_variation );
338 }
339
340 /**
341 * Returns a diff from a combined list of allowed extensions and extensions determined to be excluded
342 *
343 * @param array $allowed_extensions An array of allowed extensions.
344 *
345 * @return array A list of blocks: eg array( 'publicize', 'markdown' )
346 */
347 public static function get_available_extensions( $allowed_extensions = null ) {
348 $exclusions = get_option( 'jetpack_excluded_extensions', array() );
349 $allowed_extensions = $allowed_extensions === null ? self::get_jetpack_gutenberg_extensions_allowed_list() : $allowed_extensions;
350
351 return array_diff( $allowed_extensions, $exclusions );
352 }
353
354 /**
355 * Return true if the extension has been registered and there's nothing in the availablilty array.
356 *
357 * @param string $extension The name of the extension.
358 *
359 * @return bool whether the extension has been registered and there's nothing in the availablilty array.
360 */
361 public static function is_registered_and_no_entry_in_availability( $extension ) {
362 return self::is_registered( 'jetpack/' . $extension ) && ! isset( self::$availability[ $extension ] );
363 }
364
365 /**
366 * Return true if the extension has a true entry in the availablilty array.
367 *
368 * @param string $extension The name of the extension.
369 *
370 * @return bool whether the extension has a true entry in the availablilty array.
371 */
372 public static function is_available( $extension ) {
373 return isset( self::$availability[ $extension ] ) && true === self::$availability[ $extension ];
374 }
375
376 /**
377 * Get the availability of each block / plugin, or return the cached availability
378 * if it has already been calculated. Avoids re-registering extensions when not
379 * necessary.
380 *
381 * @return array A list of block and plugins and their availability status.
382 */
383 public static function get_cached_availability() {
384 if ( null === self::$cached_availability ) {
385 self::$cached_availability = self::get_availability();
386 }
387 return self::$cached_availability;
388 }
389
390 /**
391 * Get availability of each block / plugin.
392 *
393 * @return array A list of block and plugins and their availablity status
394 */
395 public static function get_availability() {
396 /**
397 * Fires before Gutenberg extensions availability is computed.
398 *
399 * In the function call you supply, use `Blocks::jetpack_register_block()` to set a block as available.
400 * Alternatively, use `Jetpack_Gutenberg::set_extension_available()` (for a non-block plugin), and
401 * `Jetpack_Gutenberg::set_extension_unavailable()` (if the block or plugin should not be registered
402 * but marked as unavailable).
403 *
404 * @since 7.0.0
405 */
406 do_action( 'jetpack_register_gutenberg_extensions' );
407
408 $available_extensions = array();
409
410 foreach ( self::$extensions as $extension ) {
411 $is_available = self::is_registered_and_no_entry_in_availability( $extension ) || self::is_available( $extension );
412 $available_extensions[ $extension ] = array(
413 'available' => $is_available,
414 );
415
416 if ( ! $is_available ) {
417 $reason = isset( self::$availability[ $extension ] ) ? self::$availability[ $extension ]['reason'] : 'missing_module';
418 $details = isset( self::$availability[ $extension ] ) ? self::$availability[ $extension ]['details'] : array();
419 $available_extensions[ $extension ]['unavailable_reason'] = $reason;
420 $available_extensions[ $extension ]['details'] = $details;
421 }
422 }
423
424 return $available_extensions;
425 }
426
427 /**
428 * Check if an extension/block is already registered
429 *
430 * @since 7.2
431 *
432 * @param string $slug Name of extension/block to check.
433 *
434 * @return bool
435 */
436 public static function is_registered( $slug ) {
437 return WP_Block_Type_Registry::get_instance()->is_registered( $slug );
438 }
439
440 /**
441 * Check if Gutenberg editor is available
442 *
443 * @since 6.7.0
444 *
445 * @return bool
446 */
447 public static function is_gutenberg_available() {
448 return true;
449 }
450
451 /**
452 * Check whether conditions indicate Gutenberg Extensions (blocks and plugins) should be loaded
453 *
454 * Loading blocks and plugins is enabled by default and may be disabled via filter:
455 * add_filter( 'jetpack_gutenberg', '__return_false' );
456 *
457 * @since 6.9.0
458 *
459 * @return bool
460 */
461 public static function should_load() {
462 if ( ! Jetpack::is_connection_ready() && ! ( new Status() )->is_offline_mode() ) {
463 return false;
464 }
465
466 if ( get_option( 'jetpack_blocks_disabled', false ) ) {
467 return false;
468 }
469
470 /**
471 * Filter to disable Gutenberg blocks
472 *
473 * @since 6.5.0
474 *
475 * @param bool true Whether to load Gutenberg blocks
476 */
477 return (bool) apply_filters( 'jetpack_gutenberg', true );
478 }
479
480 /**
481 * Only enqueue block assets when needed.
482 *
483 * @param string $type Slug of the block.
484 * @param array $script_dependencies Script dependencies. Will be merged with automatically
485 * detected script dependencies from the webpack build.
486 *
487 * @return void
488 */
489 public static function load_assets_as_required( $type, $script_dependencies = array() ) {
490 if ( is_admin() ) {
491 // A block's view assets will not be required in wp-admin.
492 return;
493 }
494
495 $type = sanitize_title_with_dashes( $type );
496 self::load_styles_as_required( $type );
497 self::load_scripts_as_required( $type, $script_dependencies );
498 }
499
500 /**
501 * Only enqueue block sytles when needed.
502 *
503 * @param string $type Slug of the block.
504 *
505 * @since 7.2.0
506 *
507 * @return void
508 */
509 public static function load_styles_as_required( $type ) {
510 if ( is_admin() ) {
511 // A block's view assets will not be required in wp-admin.
512 return;
513 }
514
515 // Enqueue styles.
516 $style_relative_path = self::get_blocks_directory() . $type . '/view' . ( is_rtl() ? '.rtl' : '' ) . '.css';
517 if ( self::block_has_asset( $style_relative_path ) ) {
518 $style_version = self::get_asset_version( $style_relative_path );
519 $view_style = plugins_url( $style_relative_path, JETPACK__PLUGIN_FILE );
520 $view_style = add_query_arg( 'minify', 'false', $view_style );
521
522 // If this is a customizer preview, render the style directly to the preview after autosave.
523 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
524 if ( is_customize_preview() && ! empty( $_GET['customize_autosaved'] ) ) {
525 // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet
526 echo '<link rel="stylesheet" id="jetpack-block-' . esc_attr( $type ) . '" href="' . esc_attr( $view_style ) . '&amp;ver=' . esc_attr( $style_version ) . '" media="all">';
527 } else {
528 wp_enqueue_style( 'jetpack-block-' . $type, $view_style, array(), $style_version );
529 }
530 }
531
532 }
533
534 /**
535 * Only enqueue block scripts when needed.
536 *
537 * @param string $type Slug of the block.
538 * @param array $script_dependencies Script dependencies. Will be merged with automatically
539 * detected script dependencies from the webpack build.
540 *
541 * @since 7.2.0
542 *
543 * @return void
544 */
545 public static function load_scripts_as_required( $type, $script_dependencies = array() ) {
546 if ( is_admin() ) {
547 // A block's view assets will not be required in wp-admin.
548 return;
549 }
550
551 // Enqueue script.
552 $script_relative_path = self::get_blocks_directory() . $type . '/view.js';
553 $script_deps_path = JETPACK__PLUGIN_DIR . self::get_blocks_directory() . $type . '/view.asset.php';
554 $script_dependencies[] = 'wp-polyfill';
555 if ( file_exists( $script_deps_path ) ) {
556 $asset_manifest = include $script_deps_path;
557 $script_dependencies = array_unique( array_merge( $script_dependencies, $asset_manifest['dependencies'] ) );
558 }
559
560 if ( ! Blocks::is_amp_request() && self::block_has_asset( $script_relative_path ) ) {
561 $script_version = self::get_asset_version( $script_relative_path );
562 $view_script = plugins_url( $script_relative_path, JETPACK__PLUGIN_FILE );
563 $view_script = add_query_arg( 'minify', 'false', $view_script );
564
565 // Enqueue dependencies.
566 wp_enqueue_script( 'jetpack-block-' . $type, $view_script, $script_dependencies, $script_version, false );
567
568 // If this is a customizer preview, enqueue the dependencies and render the script directly to the preview after autosave.
569 // phpcs:ignore WordPress.Security.NonceVerification.Recommended
570 if ( is_customize_preview() && ! empty( $_GET['customize_autosaved'] ) ) {
571 // The Map block is dependent on wp-element, and it doesn't appear to to be possible to load
572 // this dynamically into the customizer iframe currently.
573 if ( 'map' === $type ) {
574 echo '<div>' . esc_html_e( 'No map preview available. Publish and refresh to see this widget.', 'jetpack' ) . '</div>';
575 echo '<script>';
576 echo 'Array.from(document.getElementsByClassName(\'wp-block-jetpack-map\')).forEach(function(element){element.style.display = \'none\';})';
577 echo '</script>';
578 } else {
579 echo '<script id="jetpack-block-' . esc_attr( $type ) . '" src="' . esc_attr( $view_script ) . '&amp;ver=' . esc_attr( $script_version ) . '"></script>';
580 }
581 }
582 }
583
584 wp_localize_script(
585 'jetpack-block-' . $type,
586 'Jetpack_Block_Assets_Base_Url',
587 array(
588 'url' => plugins_url( self::get_blocks_directory(), JETPACK__PLUGIN_FILE ),
589 )
590 );
591 }
592
593 /**
594 * Check if an asset exists for a block.
595 *
596 * @param string $file Path of the file we are looking for.
597 *
598 * @return bool $block_has_asset Does the file exist.
599 */
600 public static function block_has_asset( $file ) {
601 return file_exists( JETPACK__PLUGIN_DIR . $file );
602 }
603
604 /**
605 * Get the version number to use when loading the file. Allows us to bypass cache when developing.
606 *
607 * @param string $file Path of the file we are looking for.
608 *
609 * @return string $script_version Version number.
610 */
611 public static function get_asset_version( $file ) {
612 return Jetpack::is_development_version() && self::block_has_asset( $file )
613 ? filemtime( JETPACK__PLUGIN_DIR . $file )
614 : JETPACK__VERSION;
615 }
616
617 /**
618 * Load Gutenberg editor assets
619 *
620 * @since 6.7.0
621 *
622 * @return void
623 */
624 public static function enqueue_block_editor_assets() {
625 if ( ! self::should_load() ) {
626 return;
627 }
628
629 $status = new Status();
630
631 // Required for Analytics. See _inc/lib/admin-pages/class.jetpack-admin-page.php.
632 if ( ! $status->is_offline_mode() && Jetpack::is_connection_ready() ) {
633 wp_enqueue_script( 'jp-tracks', '//stats.wp.com/w.js', array(), gmdate( 'YW' ), true );
634 }
635
636 $blocks_dir = self::get_blocks_directory();
637 $blocks_variation = self::blocks_variation();
638
639 if ( 'production' !== $blocks_variation ) {
640 $blocks_env = '-' . esc_attr( $blocks_variation );
641 } else {
642 $blocks_env = '';
643 }
644
645 Assets::register_script(
646 'jetpack-blocks-editor',
647 "{$blocks_dir}editor{$blocks_env}.js",
648 JETPACK__PLUGIN_FILE,
649 array( 'textdomain' => 'jetpack' )
650 );
651
652 // Hack around #20357 (specifically, that the editor bundle depends on
653 // wp-edit-post but wp-edit-post's styles break the Widget Editor and
654 // Site Editor) until a real fix gets unblocked.
655 // @todo Remove this once #20357 is properly fixed.
656 wp_styles()->query( 'jetpack-blocks-editor', 'registered' )->deps = array();
657
658 Assets::enqueue_script( 'jetpack-blocks-editor' );
659
660 wp_localize_script(
661 'jetpack-blocks-editor',
662 'Jetpack_Block_Assets_Base_Url',
663 array(
664 'url' => plugins_url( $blocks_dir . '/', JETPACK__PLUGIN_FILE ),
665 )
666 );
667
668 if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
669 $user = wp_get_current_user();
670 $user_data = array(
671 'userid' => $user->ID,
672 'username' => $user->user_login,
673 );
674 $blog_id = get_current_blog_id();
675 $is_current_user_connected = true;
676 } else {
677 $user_data = Jetpack_Tracks_Client::get_connected_user_tracks_identity();
678 $blog_id = Jetpack_Options::get_option( 'id', 0 );
679 $is_current_user_connected = ( new Connection_Manager( 'jetpack' ) )->is_user_connected();
680 }
681
682 wp_localize_script(
683 'jetpack-blocks-editor',
684 'Jetpack_Editor_Initial_State',
685 array(
686 'available_blocks' => self::get_availability(),
687 'jetpack' => array(
688 'is_active' => Jetpack::is_connection_ready(),
689 'is_current_user_connected' => $is_current_user_connected,
690 /** This filter is documented in class.jetpack-gutenberg.php */
691 'enable_upgrade_nudge' => apply_filters( 'jetpack_block_editor_enable_upgrade_nudge', false ),
692 'is_private_site' => '-1' === get_option( 'blog_public' ),
693 'is_coming_soon' => ( function_exists( 'site_is_coming_soon' ) && site_is_coming_soon() ) || (bool) get_option( 'wpcom_public_coming_soon' ),
694 'is_offline_mode' => $status->is_offline_mode(),
695 /**
696 * Enable the RePublicize UI in the block editor context.
697 *
698 * @module publicize
699 *
700 * @since 10.3.0
701 *
702 * @param bool true Enable the RePublicize UI in the block editor context. Defaults to true.
703 */
704 'republicize_enabled' => apply_filters( 'jetpack_block_editor_republicize_feature', true ),
705 ),
706 'siteFragment' => $status->get_site_suffix(),
707 'adminUrl' => esc_url( admin_url() ),
708 'tracksUserData' => $user_data,
709 'wpcomBlogId' => $blog_id,
710 'allowedMimeTypes' => wp_get_mime_types(),
711 'siteLocale' => str_replace( '_', '-', get_locale() ),
712 )
713 );
714 }
715
716 /**
717 * Add the Gutenberg editor stylesheet to iframed editors, such as the site editor,
718 * which don't have access to stylesheets added with `wp_enqueue_style`.
719 *
720 * This workaround is currently used by WordPress.com Simple and Atomic sites.
721 *
722 * @since 10.7
723 *
724 * @return void
725 */
726 public static function add_iframed_editor_style() {
727 if ( ! self::should_load() ) {
728 return;
729 }
730
731 global $pagenow;
732 if ( ! isset( $pagenow ) ) {
733 return;
734 }
735
736 $allowed_pages = array( 'admin.php', 'themes.php' );
737 $is_site_editor_page = in_array( $pagenow, $allowed_pages, true ) &&
738 isset( $_GET['page'] ) && 'gutenberg-edit-site' === $_GET['page']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
739
740 // WP 5.9 puts the site editor in `site-editor.php` when Gutenberg is not active.
741 if ( 'site-editor.php' !== $pagenow && ! $is_site_editor_page ) {
742 return;
743 }
744
745 $blocks_dir = self::get_blocks_directory();
746 $blocks_variation = self::blocks_variation();
747
748 if ( 'production' !== $blocks_variation ) {
749 $blocks_env = '-' . esc_attr( $blocks_variation );
750 } else {
751 $blocks_env = '';
752 }
753
754 $path = "{$blocks_dir}editor{$blocks_env}.css";
755 $dir = dirname( JETPACK__PLUGIN_FILE );
756
757 if ( file_exists( "$dir/$path" ) ) {
758 if ( is_rtl() ) {
759 $rtlcsspath = substr( $path, 0, -4 ) . '.rtl.css';
760 if ( file_exists( "$dir/$rtlcsspath" ) ) {
761 $path = $rtlcsspath;
762 }
763 }
764
765 $url = Assets::normalize_path( plugins_url( $path, JETPACK__PLUGIN_FILE ) );
766 $url = add_query_arg( 'minify', 'false', $url );
767
768 add_editor_style( $url );
769 }
770 }
771
772 /**
773 * Some blocks do not depend on a specific module,
774 * and can consequently be loaded outside of the usual modules.
775 * We will look for such modules in the extensions/ directory.
776 *
777 * @since 7.1.0
778 */
779 public static function load_independent_blocks() {
780 if ( self::should_load() ) {
781 /**
782 * Look for files that match our list of available Jetpack Gutenberg extensions (blocks and plugins).
783 * If available, load them.
784 */
785 foreach ( self::$extensions as $extension ) {
786 $extension_file_glob = glob( JETPACK__PLUGIN_DIR . 'extensions/*/' . $extension . '/' . $extension . '.php' );
787 if ( ! empty( $extension_file_glob ) ) {
788 include_once $extension_file_glob[0];
789 }
790 }
791 }
792 }
793
794 /**
795 * Loads PHP components of block editor extensions.
796 *
797 * @since 8.9.0
798 */
799 public static function load_block_editor_extensions() {
800 if ( self::should_load() ) {
801 // Block editor extensions to load.
802 $extensions_to_load = array(
803 'extended-blocks',
804 'plugins',
805 );
806
807 // Collect the extension paths.
808 foreach ( $extensions_to_load as $extension_to_load ) {
809 $extensions_folder = glob( JETPACK__PLUGIN_DIR . 'extensions/' . $extension_to_load . '/*' );
810
811 // Require each of the extension files, in case it exists.
812 foreach ( $extensions_folder as $extension_folder ) {
813 $name = basename( $extension_folder );
814 $extension_file_path = JETPACK__PLUGIN_DIR . 'extensions/' . $extension_to_load . '/' . $name . '/' . $name . '.php';
815
816 if ( file_exists( $extension_file_path ) ) {
817 include_once $extension_file_path;
818 }
819 }
820 }
821 }
822 }
823
824 /**
825 * Get CSS classes for a block.
826 *
827 * @since 7.7.0
828 *
829 * @param string $slug Block slug.
830 * @param array $attr Block attributes.
831 * @param array $extra Potential extra classes you may want to provide.
832 *
833 * @return string $classes List of CSS classes for a block.
834 */
835 public static function block_classes( $slug, $attr, $extra = array() ) {
836 _deprecated_function( __METHOD__, '9.0.0', 'Automattic\\Jetpack\\Blocks::classes' );
837 return Blocks::classes( $slug, $attr, $extra );
838 }
839
840 /**
841 * Determine whether a site should use the default set of blocks, or a custom set.
842 * Possible variations are currently beta, experimental, and production.
843 *
844 * @since 8.1.0
845 *
846 * @return string $block_varation production|beta|experimental
847 */
848 public static function blocks_variation() {
849 // Default to production blocks.
850 $block_varation = 'production';
851
852 if ( Constants::is_true( 'JETPACK_BETA_BLOCKS' ) ) {
853 $block_varation = 'beta';
854 }
855
856 /*
857 * Switch to experimental blocks if you use the JETPACK_EXPERIMENTAL_BLOCKS constant.
858 */
859 if ( Constants::is_true( 'JETPACK_EXPERIMENTAL_BLOCKS' ) ) {
860 $block_varation = 'experimental';
861 }
862
863 /**
864 * Allow customizing the variation of blocks in use on a site.
865 *
866 * @since 8.1.0
867 *
868 * @param string $block_variation Can be beta, experimental, and production. Defaults to production.
869 */
870 return apply_filters( 'jetpack_blocks_variation', $block_varation );
871 }
872
873 /**
874 * Get a list of extensions available for the variation you chose.
875 *
876 * @since 8.1.0
877 *
878 * @param obj $preset_extensions_manifest List of extensions available in Jetpack.
879 * @param string $blocks_variation Subset of blocks. production|beta|experimental.
880 *
881 * @return array $preset_extensions Array of extensions for that variation
882 */
883 public static function get_extensions_preset_for_variation( $preset_extensions_manifest, $blocks_variation ) {
884 $preset_extensions = isset( $preset_extensions_manifest->{ $blocks_variation } )
885 ? (array) $preset_extensions_manifest->{ $blocks_variation }
886 : array();
887
888 /*
889 * Experimental and Beta blocks need the production blocks as well.
890 */
891 if (
892 'experimental' === $blocks_variation
893 || 'beta' === $blocks_variation
894 ) {
895 $production_extensions = isset( $preset_extensions_manifest->production )
896 ? (array) $preset_extensions_manifest->production
897 : array();
898
899 $preset_extensions = array_unique( array_merge( $preset_extensions, $production_extensions ) );
900 }
901
902 /*
903 * Beta blocks need the experimental blocks as well.
904 *
905 * If you've chosen to see Beta blocks,
906 * we want to make all blocks available to you:
907 * - Production
908 * - Experimental
909 * - Beta
910 */
911 if ( 'beta' === $blocks_variation ) {
912 $production_extensions = isset( $preset_extensions_manifest->experimental )
913 ? (array) $preset_extensions_manifest->experimental
914 : array();
915
916 $preset_extensions = array_unique( array_merge( $preset_extensions, $production_extensions ) );
917 }
918
919 return $preset_extensions;
920 }
921
922 /**
923 * Validate a URL used in a SSR block.
924 *
925 * @since 8.3.0
926 *
927 * @param string $url URL saved as an attribute in block.
928 * @param array $allowed Array of allowed hosts for that block, or regexes to check against.
929 * @param bool $is_regex Array of regexes matching the URL that could be used in block.
930 *
931 * @return bool|string
932 */
933 public static function validate_block_embed_url( $url, $allowed = array(), $is_regex = false ) {
934 if (
935 empty( $url )
936 || ! is_array( $allowed )
937 || empty( $allowed )
938 ) {
939 return false;
940 }
941
942 $url_components = wp_parse_url( $url );
943
944 // Bail early if we cannot find a host.
945 if ( empty( $url_components['host'] ) ) {
946 return false;
947 }
948
949 // Normalize URL.
950 $url = sprintf(
951 '%s://%s%s%s',
952 isset( $url_components['scheme'] ) ? $url_components['scheme'] : 'https',
953 $url_components['host'],
954 isset( $url_components['path'] ) ? $url_components['path'] : '/',
955 isset( $url_components['query'] ) ? '?' . $url_components['query'] : ''
956 );
957
958 if ( ! empty( $url_components['fragment'] ) ) {
959 $url = $url . '#' . rawurlencode( $url_components['fragment'] );
960 }
961
962 /*
963 * If we're using an allowed list of hosts,
964 * check if the URL belongs to one of the domains allowed for that block.
965 */
966 if (
967 false === $is_regex
968 && in_array( $url_components['host'], $allowed, true )
969 ) {
970 return $url;
971 }
972
973 /*
974 * If we are using an array of regexes to check against,
975 * loop through that.
976 */
977 if ( true === $is_regex ) {
978 foreach ( $allowed as $regex ) {
979 if ( 1 === preg_match( $regex, $url ) ) {
980 return $url;
981 }
982 }
983 }
984
985 return false;
986 }
987
988 /**
989 * Determines whether a preview of the block with an upgrade nudge should
990 * be displayed for admins on the site frontend.
991 *
992 * @since 8.4.0
993 *
994 * @param array $availability_for_block The availability for the block.
995 *
996 * @return bool
997 */
998 public static function should_show_frontend_preview( $availability_for_block ) {
999 return (
1000 isset( $availability_for_block['details']['required_plan'] )
1001 && current_user_can( 'manage_options' )
1002 && ! is_feed()
1003 );
1004 }
1005
1006 /**
1007 * Output an UpgradeNudge Component on the frontend of a site.
1008 *
1009 * @since 8.4.0
1010 *
1011 * @param string $plan The plan that users need to purchase to make the block work.
1012 *
1013 * @return string
1014 */
1015 public static function upgrade_nudge( $plan ) {
1016 jetpack_require_lib( 'components' );
1017 return Jetpack_Components::render_upgrade_nudge(
1018 array(
1019 'plan' => $plan,
1020 )
1021 );
1022 }
1023
1024 /**
1025 * Output a notice within a block.
1026 *
1027 * @since 8.6.0
1028 *
1029 * @param string $message Notice we want to output.
1030 * @param string $status Status of the notice. Can be one of success, info, warning, error. info by default.
1031 * @param string $classes List of CSS classes.
1032 *
1033 * @return string
1034 */
1035 public static function notice( $message, $status = 'info', $classes = '' ) {
1036 if (
1037 empty( $message )
1038 || ! in_array( $status, array( 'success', 'info', 'warning', 'error' ), true )
1039 ) {
1040 return '';
1041 }
1042
1043 $color = '';
1044 switch ( $status ) {
1045 case 'success':
1046 $color = '#00a32a';
1047 break;
1048 case 'warning':
1049 $color = '#dba617';
1050 break;
1051 case 'error':
1052 $color = '#d63638';
1053 break;
1054 case 'info':
1055 default:
1056 $color = '#72aee6';
1057 break;
1058 }
1059
1060 return sprintf(
1061 '<div class="jetpack-block__notice %1$s %3$s" style="border-left:5px solid %4$s;padding:1em;background-color:#f8f9f9;">%2$s</div>',
1062 esc_attr( $status ),
1063 wp_kses(
1064 $message,
1065 array(
1066 'br' => array(),
1067 'p' => array(),
1068 )
1069 ),
1070 esc_attr( $classes ),
1071 sanitize_hex_color( $color )
1072 );
1073 }
1074
1075 /**
1076 * Retrieve site-specific features for Simple sites.
1077 *
1078 * We're caching the data for the lifetime of the request, because it can be slow to calculate,
1079 * and it can be called multiple times per single request.
1080 *
1081 * We intentionally don't use object caching or any other type of persistent caching,
1082 * in order to avoid complex cache invalidation on subscription addition or removal.
1083 *
1084 * @since 10.7
1085 *
1086 * @return array
1087 */
1088 private static function get_site_specific_features() {
1089 $current_blog_id = get_current_blog_id();
1090
1091 if ( isset( self::$site_specific_features[ $current_blog_id ] ) ) {
1092 return self::$site_specific_features[ $current_blog_id ];
1093 }
1094
1095 if ( ! class_exists( 'Store_Product_List' ) ) {
1096 require WP_CONTENT_DIR . '/admin-plugins/wpcom-billing/store-product-list.php';
1097 }
1098
1099 $site_specific_features = Store_Product_List::get_site_specific_features_data( $current_blog_id );
1100 self::$site_specific_features[ $current_blog_id ] = $site_specific_features;
1101
1102 return $site_specific_features;
1103 }
1104
1105 /**
1106 * Set the availability of the block as the editor
1107 * is loaded.
1108 *
1109 * @param string $slug Slug of the block.
1110 */
1111 public static function set_availability_for_plan( $slug ) {
1112 $slug = self::remove_extension_prefix( $slug );
1113
1114 if ( Jetpack_Plan::supports( $slug ) ) {
1115 self::set_extension_available( $slug );
1116 return;
1117 }
1118
1119 // Check what's the minimum plan where the feature is available.
1120 $plan = '';
1121 $features_data = array();
1122 $is_simple_site = defined( 'IS_WPCOM' ) && IS_WPCOM;
1123 $is_atomic_site = ( new Host() )->is_woa_site();
1124
1125 if ( $is_simple_site || $is_atomic_site ) {
1126 // Simple sites.
1127 if ( $is_simple_site ) {
1128 $features_data = self::get_site_specific_features();
1129 } else {
1130 // Atomic sites.
1131 $option = get_option( 'jetpack_active_plan' );
1132 if ( isset( $option['features'] ) ) {
1133 $features_data = $option['features'];
1134 }
1135 }
1136
1137 if ( ! empty( $features_data['available'][ $slug ] ) ) {
1138 $plan = $features_data['available'][ $slug ][0];
1139 }
1140 } else {
1141 // Jetpack sites.
1142 $plan = Jetpack_Plan::get_minimum_plan_for_feature( $slug );
1143 }
1144
1145 self::set_extension_unavailable(
1146 $slug,
1147 'missing_plan',
1148 array(
1149 'required_feature' => $slug,
1150 'required_plan' => $plan,
1151 )
1152 );
1153 }
1154
1155 /**
1156 * Wraps the suplied render_callback in a function to check
1157 * the availability of the block before rendering it.
1158 *
1159 * @param string $slug The block slug, used to check for availability.
1160 * @param callable $render_callback The render_callback that will be called if the block is available.
1161 */
1162 public static function get_render_callback_with_availability_check( $slug, $render_callback ) {
1163 return function ( $prepared_attributes, $block_content, $block ) use ( $render_callback, $slug ) {
1164 $availability = self::get_cached_availability();
1165 $bare_slug = self::remove_extension_prefix( $slug );
1166 if ( isset( $availability[ $bare_slug ] ) && $availability[ $bare_slug ]['available'] ) {
1167 return call_user_func( $render_callback, $prepared_attributes, $block_content );
1168 }
1169
1170 // A preview of the block is rendered for admins on the frontend with an upgrade nudge.
1171 if ( isset( $availability[ $bare_slug ] ) ) {
1172 if ( self::should_show_frontend_preview( $availability[ $bare_slug ] ) ) {
1173 $block_preview = call_user_func( $render_callback, $prepared_attributes, $block_content );
1174
1175 // If the upgrade nudge isn't already being displayed by a parent block, display the nudge.
1176 if ( isset( $block->attributes['shouldDisplayFrontendBanner'] ) && $block->attributes['shouldDisplayFrontendBanner'] ) {
1177 $upgrade_nudge = self::upgrade_nudge( $availability[ $bare_slug ]['details']['required_plan'] );
1178 return $upgrade_nudge . $block_preview;
1179 }
1180
1181 return $block_preview;
1182 }
1183 }
1184
1185 return null;
1186 };
1187 }
1188 }
1189
1190 if ( ( new Host() )->is_woa_site() ) {
1191 /**
1192 * Enable upgrade nudge for Atomic sites.
1193 * This feature is false as default,
1194 * so let's enable it through this filter.
1195 *
1196 * More doc: https://github.com/Automattic/jetpack/tree/master/projects/plugins/jetpack/extensions#upgrades-for-blocks
1197 */
1198 add_filter( 'jetpack_block_editor_enable_upgrade_nudge', '__return_true' );
1199
1200 /**
1201 * Load block editor styles inline for iframed editors.
1202 *
1203 * @see paYJgx-1Kl-p2
1204 */
1205 add_action( 'admin_init', array( 'Jetpack_Gutenberg', 'add_iframed_editor_style' ) );
1206 }
1207