PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 11.5
Jetpack – WP Security, Backup, Speed, & Growth v11.5
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
1,221 lines 38.0 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 $initial_state = array(
683 'available_blocks' => self::get_availability(),
684 'jetpack' => array(
685 'is_active' => Jetpack::is_connection_ready(),
686 'is_current_user_connected' => $is_current_user_connected,
687 /** This filter is documented in class.jetpack-gutenberg.php */
688 'enable_upgrade_nudge' => apply_filters( 'jetpack_block_editor_enable_upgrade_nudge', false ),
689 'is_private_site' => '-1' === get_option( 'blog_public' ),
690 'is_coming_soon' => ( function_exists( 'site_is_coming_soon' ) && site_is_coming_soon() ) || (bool) get_option( 'wpcom_public_coming_soon' ),
691 'is_offline_mode' => $status->is_offline_mode(),
692 /**
693 * Enable the RePublicize UI in the block editor context.
694 *
695 * @module publicize
696 *
697 * @since 10.3.0
698 * @deprecated $$next_version$$ This is a feature flag that is no longer used.
699 *
700 * @param bool true Enable the RePublicize UI in the block editor context. Defaults to true.
701 */
702 'republicize_enabled' => apply_filters( 'jetpack_block_editor_republicize_feature', true ),
703 ),
704 'siteFragment' => $status->get_site_suffix(),
705 'adminUrl' => esc_url( admin_url() ),
706 'tracksUserData' => $user_data,
707 'wpcomBlogId' => $blog_id,
708 'allowedMimeTypes' => wp_get_mime_types(),
709 'siteLocale' => str_replace( '_', '-', get_locale() ),
710 );
711
712 if ( Jetpack::is_module_active( 'publicize' ) && function_exists( 'publicize_init' ) ) {
713 $publicize = publicize_init();
714 $initial_state['social'] = array(
715 'sharesData' => $publicize->get_publicize_shares_info( $blog_id ),
716 'hasPaidPlan' => $publicize->has_paid_plan(),
717 );
718 }
719
720 wp_localize_script(
721 'jetpack-blocks-editor',
722 'Jetpack_Editor_Initial_State',
723 $initial_state
724 );
725 }
726
727 /**
728 * Add the Gutenberg editor stylesheet to iframed editors, such as the site editor,
729 * which don't have access to stylesheets added with `wp_enqueue_style`.
730 *
731 * This workaround is currently used by WordPress.com Simple and Atomic sites.
732 *
733 * @since 10.7
734 *
735 * @return void
736 */
737 public static function add_iframed_editor_style() {
738 if ( ! self::should_load() ) {
739 return;
740 }
741
742 global $pagenow;
743 if ( ! isset( $pagenow ) ) {
744 return;
745 }
746
747 // Pre 13.7 pages that still need to be supported if < 13.7 is
748 // still installed.
749 $allowed_old_pages = array( 'admin.php', 'themes.php' );
750 $is_old_site_editor_page = in_array( $pagenow, $allowed_old_pages, true ) && isset( $_GET['page'] ) && 'gutenberg-edit-site' === $_GET['page']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
751 // For Gutenberg > 13.7, the core `site-editor.php` route is used instead
752 $is_site_editor_page = 'site-editor.php' === $pagenow;
753
754 $should_skip_adding_styles = ! $is_site_editor_page && ! $is_old_site_editor_page;
755 if ( $should_skip_adding_styles ) {
756 return;
757 }
758
759 $blocks_dir = self::get_blocks_directory();
760 $blocks_variation = self::blocks_variation();
761
762 if ( 'production' !== $blocks_variation ) {
763 $blocks_env = '-' . esc_attr( $blocks_variation );
764 } else {
765 $blocks_env = '';
766 }
767
768 $path = "{$blocks_dir}editor{$blocks_env}.css";
769 $dir = dirname( JETPACK__PLUGIN_FILE );
770
771 if ( file_exists( "$dir/$path" ) ) {
772 if ( is_rtl() ) {
773 $rtlcsspath = substr( $path, 0, -4 ) . '.rtl.css';
774 if ( file_exists( "$dir/$rtlcsspath" ) ) {
775 $path = $rtlcsspath;
776 }
777 }
778
779 $url = Assets::normalize_path( plugins_url( $path, JETPACK__PLUGIN_FILE ) );
780 $url = add_query_arg( 'minify', 'false', $url );
781
782 add_editor_style( $url );
783 }
784 }
785
786 /**
787 * Some blocks do not depend on a specific module,
788 * and can consequently be loaded outside of the usual modules.
789 * We will look for such modules in the extensions/ directory.
790 *
791 * @since 7.1.0
792 */
793 public static function load_independent_blocks() {
794 if ( self::should_load() ) {
795 /**
796 * Look for files that match our list of available Jetpack Gutenberg extensions (blocks and plugins).
797 * If available, load them.
798 */
799 foreach ( self::$extensions as $extension ) {
800 $extension_file_glob = glob( JETPACK__PLUGIN_DIR . 'extensions/*/' . $extension . '/' . $extension . '.php' );
801 if ( ! empty( $extension_file_glob ) ) {
802 include_once $extension_file_glob[0];
803 }
804 }
805 }
806 }
807
808 /**
809 * Loads PHP components of block editor extensions.
810 *
811 * @since 8.9.0
812 */
813 public static function load_block_editor_extensions() {
814 if ( self::should_load() ) {
815 // Block editor extensions to load.
816 $extensions_to_load = array(
817 'extended-blocks',
818 'plugins',
819 );
820
821 // Collect the extension paths.
822 foreach ( $extensions_to_load as $extension_to_load ) {
823 $extensions_folder = glob( JETPACK__PLUGIN_DIR . 'extensions/' . $extension_to_load . '/*' );
824
825 // Require each of the extension files, in case it exists.
826 foreach ( $extensions_folder as $extension_folder ) {
827 $name = basename( $extension_folder );
828 $extension_file_path = JETPACK__PLUGIN_DIR . 'extensions/' . $extension_to_load . '/' . $name . '/' . $name . '.php';
829
830 if ( file_exists( $extension_file_path ) ) {
831 include_once $extension_file_path;
832 }
833 }
834 }
835 }
836 }
837
838 /**
839 * Get CSS classes for a block.
840 *
841 * @since 7.7.0
842 *
843 * @param string $slug Block slug.
844 * @param array $attr Block attributes.
845 * @param array $extra Potential extra classes you may want to provide.
846 *
847 * @return string $classes List of CSS classes for a block.
848 */
849 public static function block_classes( $slug, $attr, $extra = array() ) {
850 _deprecated_function( __METHOD__, '9.0.0', 'Automattic\\Jetpack\\Blocks::classes' );
851 return Blocks::classes( $slug, $attr, $extra );
852 }
853
854 /**
855 * Determine whether a site should use the default set of blocks, or a custom set.
856 * Possible variations are currently beta, experimental, and production.
857 *
858 * @since 8.1.0
859 *
860 * @return string $block_varation production|beta|experimental
861 */
862 public static function blocks_variation() {
863 // Default to production blocks.
864 $block_varation = 'production';
865
866 if ( Constants::is_true( 'JETPACK_BETA_BLOCKS' ) ) {
867 $block_varation = 'beta';
868 }
869
870 /*
871 * Switch to experimental blocks if you use the JETPACK_EXPERIMENTAL_BLOCKS constant.
872 */
873 if ( Constants::is_true( 'JETPACK_EXPERIMENTAL_BLOCKS' ) ) {
874 $block_varation = 'experimental';
875 }
876
877 /**
878 * Allow customizing the variation of blocks in use on a site.
879 *
880 * @since 8.1.0
881 *
882 * @param string $block_variation Can be beta, experimental, and production. Defaults to production.
883 */
884 return apply_filters( 'jetpack_blocks_variation', $block_varation );
885 }
886
887 /**
888 * Get a list of extensions available for the variation you chose.
889 *
890 * @since 8.1.0
891 *
892 * @param obj $preset_extensions_manifest List of extensions available in Jetpack.
893 * @param string $blocks_variation Subset of blocks. production|beta|experimental.
894 *
895 * @return array $preset_extensions Array of extensions for that variation
896 */
897 public static function get_extensions_preset_for_variation( $preset_extensions_manifest, $blocks_variation ) {
898 $preset_extensions = isset( $preset_extensions_manifest->{ $blocks_variation } )
899 ? (array) $preset_extensions_manifest->{ $blocks_variation }
900 : array();
901
902 /*
903 * Experimental and Beta blocks need the production blocks as well.
904 */
905 if (
906 'experimental' === $blocks_variation
907 || 'beta' === $blocks_variation
908 ) {
909 $production_extensions = isset( $preset_extensions_manifest->production )
910 ? (array) $preset_extensions_manifest->production
911 : array();
912
913 $preset_extensions = array_unique( array_merge( $preset_extensions, $production_extensions ) );
914 }
915
916 /*
917 * Beta blocks need the experimental blocks as well.
918 *
919 * If you've chosen to see Beta blocks,
920 * we want to make all blocks available to you:
921 * - Production
922 * - Experimental
923 * - Beta
924 */
925 if ( 'beta' === $blocks_variation ) {
926 $production_extensions = isset( $preset_extensions_manifest->experimental )
927 ? (array) $preset_extensions_manifest->experimental
928 : array();
929
930 $preset_extensions = array_unique( array_merge( $preset_extensions, $production_extensions ) );
931 }
932
933 return $preset_extensions;
934 }
935
936 /**
937 * Validate a URL used in a SSR block.
938 *
939 * @since 8.3.0
940 *
941 * @param string $url URL saved as an attribute in block.
942 * @param array $allowed Array of allowed hosts for that block, or regexes to check against.
943 * @param bool $is_regex Array of regexes matching the URL that could be used in block.
944 *
945 * @return bool|string
946 */
947 public static function validate_block_embed_url( $url, $allowed = array(), $is_regex = false ) {
948 if (
949 empty( $url )
950 || ! is_array( $allowed )
951 || empty( $allowed )
952 ) {
953 return false;
954 }
955
956 $url_components = wp_parse_url( $url );
957
958 // Bail early if we cannot find a host.
959 if ( empty( $url_components['host'] ) ) {
960 return false;
961 }
962
963 // Normalize URL.
964 $url = sprintf(
965 '%s://%s%s%s',
966 isset( $url_components['scheme'] ) ? $url_components['scheme'] : 'https',
967 $url_components['host'],
968 isset( $url_components['path'] ) ? $url_components['path'] : '/',
969 isset( $url_components['query'] ) ? '?' . $url_components['query'] : ''
970 );
971
972 if ( ! empty( $url_components['fragment'] ) ) {
973 $url = $url . '#' . rawurlencode( $url_components['fragment'] );
974 }
975
976 /*
977 * If we're using an allowed list of hosts,
978 * check if the URL belongs to one of the domains allowed for that block.
979 */
980 if (
981 false === $is_regex
982 && in_array( $url_components['host'], $allowed, true )
983 ) {
984 return $url;
985 }
986
987 /*
988 * If we are using an array of regexes to check against,
989 * loop through that.
990 */
991 if ( true === $is_regex ) {
992 foreach ( $allowed as $regex ) {
993 if ( 1 === preg_match( $regex, $url ) ) {
994 return $url;
995 }
996 }
997 }
998
999 return false;
1000 }
1001
1002 /**
1003 * Determines whether a preview of the block with an upgrade nudge should
1004 * be displayed for admins on the site frontend.
1005 *
1006 * @since 8.4.0
1007 *
1008 * @param array $availability_for_block The availability for the block.
1009 *
1010 * @return bool
1011 */
1012 public static function should_show_frontend_preview( $availability_for_block ) {
1013 return (
1014 isset( $availability_for_block['details']['required_plan'] )
1015 && current_user_can( 'manage_options' )
1016 && ! is_feed()
1017 );
1018 }
1019
1020 /**
1021 * Output an UpgradeNudge Component on the frontend of a site.
1022 *
1023 * @since 8.4.0
1024 *
1025 * @param string $plan The plan that users need to purchase to make the block work.
1026 *
1027 * @return string
1028 */
1029 public static function upgrade_nudge( $plan ) {
1030 require_once JETPACK__PLUGIN_DIR . '_inc/lib/components.php';
1031 return Jetpack_Components::render_upgrade_nudge(
1032 array(
1033 'plan' => $plan,
1034 )
1035 );
1036 }
1037
1038 /**
1039 * Output a notice within a block.
1040 *
1041 * @since 8.6.0
1042 *
1043 * @param string $message Notice we want to output.
1044 * @param string $status Status of the notice. Can be one of success, info, warning, error. info by default.
1045 * @param string $classes List of CSS classes.
1046 *
1047 * @return string
1048 */
1049 public static function notice( $message, $status = 'info', $classes = '' ) {
1050 if (
1051 empty( $message )
1052 || ! in_array( $status, array( 'success', 'info', 'warning', 'error' ), true )
1053 ) {
1054 return '';
1055 }
1056
1057 $color = '';
1058 switch ( $status ) {
1059 case 'success':
1060 $color = '#00a32a';
1061 break;
1062 case 'warning':
1063 $color = '#dba617';
1064 break;
1065 case 'error':
1066 $color = '#d63638';
1067 break;
1068 case 'info':
1069 default:
1070 $color = '#72aee6';
1071 break;
1072 }
1073
1074 return sprintf(
1075 '<div class="jetpack-block__notice %1$s %3$s" style="border-left:5px solid %4$s;padding:1em;background-color:#f8f9f9;">%2$s</div>',
1076 esc_attr( $status ),
1077 wp_kses(
1078 $message,
1079 array(
1080 'br' => array(),
1081 'p' => array(),
1082 )
1083 ),
1084 esc_attr( $classes ),
1085 sanitize_hex_color( $color )
1086 );
1087 }
1088
1089 /**
1090 * Retrieve site-specific features for Simple sites.
1091 *
1092 * We're caching the data for the lifetime of the request, because it can be slow to calculate,
1093 * and it can be called multiple times per single request.
1094 *
1095 * We intentionally don't use object caching or any other type of persistent caching,
1096 * in order to avoid complex cache invalidation on subscription addition or removal.
1097 *
1098 * @since 10.7
1099 *
1100 * @return array
1101 */
1102 private static function get_site_specific_features() {
1103 $current_blog_id = get_current_blog_id();
1104
1105 if ( isset( self::$site_specific_features[ $current_blog_id ] ) ) {
1106 return self::$site_specific_features[ $current_blog_id ];
1107 }
1108
1109 if ( ! class_exists( 'Store_Product_List' ) ) {
1110 require WP_CONTENT_DIR . '/admin-plugins/wpcom-billing/store-product-list.php';
1111 }
1112
1113 $site_specific_features = Store_Product_List::get_site_specific_features_data( $current_blog_id );
1114 self::$site_specific_features[ $current_blog_id ] = $site_specific_features;
1115
1116 return $site_specific_features;
1117 }
1118
1119 /**
1120 * Set the availability of the block as the editor
1121 * is loaded.
1122 *
1123 * @param string $slug Slug of the block.
1124 */
1125 public static function set_availability_for_plan( $slug ) {
1126 $slug = self::remove_extension_prefix( $slug );
1127
1128 if ( Jetpack_Plan::supports( $slug ) ) {
1129 self::set_extension_available( $slug );
1130 return;
1131 }
1132
1133 // Check what's the minimum plan where the feature is available.
1134 $plan = '';
1135 $features_data = array();
1136 $is_simple_site = defined( 'IS_WPCOM' ) && IS_WPCOM;
1137 $is_atomic_site = ( new Host() )->is_woa_site();
1138
1139 if ( $is_simple_site || $is_atomic_site ) {
1140 // Simple sites.
1141 if ( $is_simple_site ) {
1142 $features_data = self::get_site_specific_features();
1143 } else {
1144 // Atomic sites.
1145 $option = get_option( 'jetpack_active_plan' );
1146 if ( isset( $option['features'] ) ) {
1147 $features_data = $option['features'];
1148 }
1149 }
1150
1151 if ( ! empty( $features_data['available'][ $slug ] ) ) {
1152 $plan = $features_data['available'][ $slug ][0];
1153 }
1154 } else {
1155 // Jetpack sites.
1156 $plan = Jetpack_Plan::get_minimum_plan_for_feature( $slug );
1157 }
1158
1159 self::set_extension_unavailable(
1160 $slug,
1161 'missing_plan',
1162 array(
1163 'required_feature' => $slug,
1164 'required_plan' => $plan,
1165 )
1166 );
1167 }
1168
1169 /**
1170 * Wraps the suplied render_callback in a function to check
1171 * the availability of the block before rendering it.
1172 *
1173 * @param string $slug The block slug, used to check for availability.
1174 * @param callable $render_callback The render_callback that will be called if the block is available.
1175 */
1176 public static function get_render_callback_with_availability_check( $slug, $render_callback ) {
1177 return function ( $prepared_attributes, $block_content, $block ) use ( $render_callback, $slug ) {
1178 $availability = self::get_cached_availability();
1179 $bare_slug = self::remove_extension_prefix( $slug );
1180 if ( isset( $availability[ $bare_slug ] ) && $availability[ $bare_slug ]['available'] ) {
1181 return call_user_func( $render_callback, $prepared_attributes, $block_content );
1182 }
1183
1184 // A preview of the block is rendered for admins on the frontend with an upgrade nudge.
1185 if ( isset( $availability[ $bare_slug ] ) ) {
1186 if ( self::should_show_frontend_preview( $availability[ $bare_slug ] ) ) {
1187 $block_preview = call_user_func( $render_callback, $prepared_attributes, $block_content );
1188
1189 // If the upgrade nudge isn't already being displayed by a parent block, display the nudge.
1190 if ( isset( $block->attributes['shouldDisplayFrontendBanner'] ) && $block->attributes['shouldDisplayFrontendBanner'] ) {
1191 $upgrade_nudge = self::upgrade_nudge( $availability[ $bare_slug ]['details']['required_plan'] );
1192 return $upgrade_nudge . $block_preview;
1193 }
1194
1195 return $block_preview;
1196 }
1197 }
1198
1199 return null;
1200 };
1201 }
1202 }
1203
1204 if ( ( new Host() )->is_woa_site() ) {
1205 /**
1206 * Enable upgrade nudge for Atomic sites.
1207 * This feature is false as default,
1208 * so let's enable it through this filter.
1209 *
1210 * More doc: https://github.com/Automattic/jetpack/blob/trunk/projects/plugins/jetpack/extensions/README.md#upgrades-for-blocks
1211 */
1212 add_filter( 'jetpack_block_editor_enable_upgrade_nudge', '__return_true' );
1213
1214 /**
1215 * Load block editor styles inline for iframed editors.
1216 *
1217 * @see paYJgx-1Kl-p2
1218 */
1219 add_action( 'admin_init', array( 'Jetpack_Gutenberg', 'add_iframed_editor_style' ) );
1220 }
1221