Completed
Push — try/jetpack-stories-block-mobi... ( 645fa9...5e255c )
by
unknown
93:41 queued 85:08
created

class.jetpack-gutenberg.php (2 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

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 Jetpack
7
 */
8
9
use Automattic\Jetpack\Constants;
10
use Automattic\Jetpack\Status;
11
12
/**
13
 * Wrapper function to safely register a gutenberg block type
14
 *
15
 * @param string $slug Slug of the block.
16
 * @param array  $args Arguments that are passed into register_block_type.
17
 *
18
 * @see register_block_type
19
 *
20
 * @since 6.7.0
21
 *
22
 * @return WP_Block_Type|false The registered block type on success, or false on failure.
23
 */
24
function jetpack_register_block( $slug, $args = array() ) {
25
	if ( 0 !== strpos( $slug, 'jetpack/' ) && ! strpos( $slug, '/' ) ) {
26
		_doing_it_wrong( 'jetpack_register_block', 'Prefix the block with jetpack/ ', '7.1.0' );
27
		$slug = 'jetpack/' . $slug;
28
	}
29
30
	if ( isset( $args['version_requirements'] )
31
		&& ! Jetpack_Gutenberg::is_gutenberg_version_available( $args['version_requirements'], $slug ) ) {
32
		return false;
33
	}
34
35
	// Checking whether block is registered to ensure it isn't registered twice.
36
	if ( Jetpack_Gutenberg::is_registered( $slug ) ) {
37
		return false;
38
	}
39
40
	$feature_name = Jetpack_Gutenberg::remove_extension_prefix( $slug );
41
	// If the block is dynamic, and a Jetpack block, wrap the render_callback to check availability.
42
	if (
43
		isset( $args['plan_check'] )
44
		&& true === $args['plan_check']
45
	) {
46
		if ( isset( $args['render_callback'] ) ) {
47
			$args['render_callback'] = Jetpack_Gutenberg::get_render_callback_with_availability_check( $feature_name, $args['render_callback'] );
48
		}
49
		$method_name = 'set_availability_for_plan';
50
	} else {
51
		$method_name = 'set_extension_available';
52
	}
53
54
	add_action(
55
		'jetpack_register_gutenberg_extensions',
56
		function() use ( $feature_name, $method_name ) {
57
			call_user_func( array( 'Jetpack_Gutenberg', $method_name ), $feature_name );
58
		}
59
	);
60
61
	return register_block_type( $slug, $args );
62
}
63
64
/**
65
 * Helper function to register a Jetpack Gutenberg plugin
66
 *
67
 * @deprecated 7.1.0 Use Jetpack_Gutenberg::set_extension_available() instead
68
 *
69
 * @param string $slug Slug of the plugin.
70
 *
71
 * @since 6.9.0
72
 *
73
 * @return void
74
 */
75
function jetpack_register_plugin( $slug ) {
76
	_deprecated_function( __FUNCTION__, '7.1', 'Jetpack_Gutenberg::set_extension_available' );
77
78
	Jetpack_Gutenberg::register_plugin( $slug );
79
}
80
81
/**
82
 * Set the reason why an extension (block or plugin) is unavailable
83
 *
84
 * @deprecated 7.1.0 Use Jetpack_Gutenberg::set_extension_unavailable() instead
85
 *
86
 * @param string $slug Slug of the block.
87
 * @param string $reason A string representation of why the extension is unavailable.
88
 *
89
 * @since 7.0.0
90
 *
91
 * @return void
92
 */
93
function jetpack_set_extension_unavailability_reason( $slug, $reason ) {
94
	_deprecated_function( __FUNCTION__, '7.1', 'Jetpack_Gutenberg::set_extension_unavailable' );
95
96
	Jetpack_Gutenberg::set_extension_unavailability_reason( $slug, $reason );
97
}
98
99
/**
100
 * General Gutenberg editor specific functionality
101
 */
102
class Jetpack_Gutenberg {
103
104
	/**
105
	 * Only these extensions can be registered. Used to control availability of beta blocks.
106
	 *
107
	 * @var array Extensions allowed list.
108
	 */
109
	private static $extensions = array();
110
111
	/**
112
	 * Keeps track of the reasons why a given extension is unavailable.
113
	 *
114
	 * @var array Extensions availability information
115
	 */
116
	private static $availability = array();
117
118
	/**
119
	 * Check to see if a minimum version of Gutenberg is available. Because a Gutenberg version is not available in
120
	 * php if the Gutenberg plugin is not installed, if we know which minimum WP release has the required version we can
121
	 * optionally fall back to that.
122
	 *
123
	 * @param array  $version_requirements An array containing the required Gutenberg version and, if known, the WordPress version that was released with this minimum version.
124
	 * @param string $slug The slug of the block or plugin that has the gutenberg version requirement.
125
	 *
126
	 * @since 8.3.0
127
	 *
128
	 * @return boolean True if the version of gutenberg required by the block or plugin is available.
129
	 */
130
	public static function is_gutenberg_version_available( $version_requirements, $slug ) {
131
		global $wp_version;
132
133
		// Bail if we don't at least have the gutenberg version requirement, the WP version is optional.
134
		if ( empty( $version_requirements['gutenberg'] ) ) {
135
			return false;
136
		}
137
138
		// If running a local dev build of gutenberg plugin GUTENBERG_DEVELOPMENT_MODE is set so assume correct version.
139
		if ( defined( 'GUTENBERG_DEVELOPMENT_MODE' ) && GUTENBERG_DEVELOPMENT_MODE ) {
140
			return true;
141
		}
142
143
		$version_available = false;
144
145
		// If running a production build of the gutenberg plugin then GUTENBERG_VERSION is set, otherwise if WP version
146
		// with required version of Gutenberg is known check that.
147
		if ( defined( 'GUTENBERG_VERSION' ) ) {
148
			$version_available = version_compare( GUTENBERG_VERSION, $version_requirements['gutenberg'], '>=' );
149
		} elseif ( ! empty( $version_requirements['wp'] ) ) {
150
			$version_available = version_compare( $wp_version, $version_requirements['wp'], '>=' );
151
		}
152
153
		if ( ! $version_available ) {
154
			self::set_extension_unavailable(
155
				$slug,
156
				'incorrect_gutenberg_version',
157
				array(
158
					'required_feature' => $slug,
159
					'required_version' => $version_requirements,
160
					'current_version'  => array(
161
						'wp'        => $wp_version,
162
						'gutenberg' => defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : null,
163
					),
164
				)
165
			);
166
		}
167
168
		return $version_available;
169
	}
170
171
	/**
172
	 * Prepend the 'jetpack/' prefix to a block name
173
	 *
174
	 * @param string $block_name The block name.
175
	 *
176
	 * @return string The prefixed block name.
177
	 */
178
	private static function prepend_block_prefix( $block_name ) {
179
		return 'jetpack/' . $block_name;
180
	}
181
182
	/**
183
	 * Remove the 'jetpack/' or jetpack-' prefix from an extension name
184
	 *
185
	 * @param string $extension_name The extension name.
186
	 *
187
	 * @return string The unprefixed extension name.
188
	 */
189
	public static function remove_extension_prefix( $extension_name ) {
190
		if ( 0 === strpos( $extension_name, 'jetpack/' ) || 0 === strpos( $extension_name, 'jetpack-' ) ) {
191
			return substr( $extension_name, strlen( 'jetpack/' ) );
192
		}
193
		return $extension_name;
194
	}
195
196
	/**
197
	 * Whether two arrays share at least one item
198
	 *
199
	 * @param array $a An array.
200
	 * @param array $b Another array.
201
	 *
202
	 * @return boolean True if $a and $b share at least one item
203
	 */
204
	protected static function share_items( $a, $b ) {
205
		return count( array_intersect( $a, $b ) ) > 0;
206
	}
207
208
	/**
209
	 * Register a block
210
	 *
211
	 * @deprecated 7.1.0 Use jetpack_register_block() instead
212
	 *
213
	 * @param string $slug Slug of the block.
214
	 * @param array  $args Arguments that are passed into register_block_type().
215
	 */
216
	public static function register_block( $slug, $args ) {
217
		_deprecated_function( __METHOD__, '7.1', 'jetpack_register_block' );
218
219
		jetpack_register_block( 'jetpack/' . $slug, $args );
220
	}
221
222
	/**
223
	 * Register a plugin
224
	 *
225
	 * @deprecated 7.1.0 Use Jetpack_Gutenberg::set_extension_available() instead
226
	 *
227
	 * @param string $slug Slug of the plugin.
228
	 */
229
	public static function register_plugin( $slug ) {
230
		_deprecated_function( __METHOD__, '7.1', 'Jetpack_Gutenberg::set_extension_available' );
231
232
		self::set_extension_available( $slug );
233
	}
234
235
	/**
236
	 * Register a block
237
	 *
238
	 * @deprecated 7.0.0 Use jetpack_register_block() instead
239
	 *
240
	 * @param string $slug Slug of the block.
241
	 * @param array  $args Arguments that are passed into the register_block_type.
242
	 * @param array  $availability array containing if a block is available and the reason when it is not.
243
	 */
244
	public static function register( $slug, $args, $availability ) {
245
		_deprecated_function( __METHOD__, '7.0', 'jetpack_register_block' );
246
247
		if ( isset( $availability['available'] ) && ! $availability['available'] ) {
248
			self::set_extension_unavailability_reason( $slug, $availability['unavailable_reason'] );
0 ignored issues
show
Deprecated Code introduced by
The method Jetpack_Gutenberg::set_e...unavailability_reason() has been deprecated with message: 7.1.0 Use set_extension_unavailable() instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
249
		} else {
250
			self::register_block( $slug, $args );
0 ignored issues
show
Deprecated Code introduced by
The method Jetpack_Gutenberg::register_block() has been deprecated with message: 7.1.0 Use jetpack_register_block() instead

This method has been deprecated. The supplier of the class has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the method will be removed from the class and what other method or class to use instead.

Loading history...
251
		}
252
	}
253
254
	/**
255
	 * Set a (non-block) extension as available
256
	 *
257
	 * @param string $slug Slug of the extension.
258
	 */
259
	public static function set_extension_available( $slug ) {
260
		self::$availability[ self::remove_extension_prefix( $slug ) ] = true;
261
	}
262
263
	/**
264
	 * Set the reason why an extension (block or plugin) is unavailable
265
	 *
266
	 * @param string $slug Slug of the extension.
267
	 * @param string $reason A string representation of why the extension is unavailable.
268
	 * @param array  $details A free-form array containing more information on why the extension is unavailable.
269
	 */
270
	public static function set_extension_unavailable( $slug, $reason, $details = array() ) {
271
		if (
272
			// Extensions that require a plan may be eligible for upgrades.
273
			'missing_plan' === $reason
274
			&& (
275
				/**
276
				 * Filter 'jetpack_block_editor_enable_upgrade_nudge' with `true` to enable or `false`
277
				 * to disable paid feature upgrade nudges in the block editor.
278
				 *
279
				 * When this is changed to default to `true`, you should also update `modules/memberships/class-jetpack-memberships.php`
280
				 * See https://github.com/Automattic/jetpack/pull/13394#pullrequestreview-293063378
281
				 *
282
				 * @since 7.7.0
283
				 *
284
				 * @param boolean
285
				 */
286
				! apply_filters( 'jetpack_block_editor_enable_upgrade_nudge', false )
287
				/** This filter is documented in _inc/lib/admin-pages/class.jetpack-react-page.php */
288
				|| ! apply_filters( 'jetpack_show_promotions', true )
289
			)
290
		) {
291
			// The block editor may apply an upgrade nudge if `missing_plan` is the reason.
292
			// Add a descriptive suffix to disable behavior but provide informative reason.
293
			$reason .= '__nudge_disabled';
294
		}
295
296
		self::$availability[ self::remove_extension_prefix( $slug ) ] = array(
297
			'reason'  => $reason,
298
			'details' => $details,
299
		);
300
	}
301
302
	/**
303
	 * Set the reason why an extension (block or plugin) is unavailable
304
	 *
305
	 * @deprecated 7.1.0 Use set_extension_unavailable() instead
306
	 *
307
	 * @param string $slug Slug of the extension.
308
	 * @param string $reason A string representation of why the extension is unavailable.
309
	 */
310
	public static function set_extension_unavailability_reason( $slug, $reason ) {
311
		_deprecated_function( __METHOD__, '7.1', 'Jetpack_Gutenberg::set_extension_unavailable' );
312
313
		self::set_extension_unavailable( $slug, $reason );
314
	}
315
316
	/**
317
	 * Set up a list of allowed block editor extensions
318
	 *
319
	 * @return void
320
	 */
321
	public static function init() {
322
		if ( ! self::should_load() ) {
323
			return;
324
		}
325
326
		/**
327
		 * Alternative to `JETPACK_BETA_BLOCKS`, set to `true` to load Beta Blocks.
328
		 *
329
		 * @since 6.9.0
330
		 *
331
		 * @param boolean
332
		 */
333
		if ( apply_filters( 'jetpack_load_beta_blocks', false ) ) {
334
			Constants::set_constant( 'JETPACK_BETA_BLOCKS', true );
335
		}
336
337
		/**
338
		 * Alternative to `JETPACK_EXPERIMENTAL_BLOCKS`, set to `true` to load Experimental Blocks.
339
		 *
340
		 * @since 8.4.0
341
		 *
342
		 * @param boolean
343
		 */
344
		if ( apply_filters( 'jetpack_load_experimental_blocks', false ) ) {
345
			Constants::set_constant( 'JETPACK_EXPERIMENTAL_BLOCKS', true );
346
		}
347
348
		/**
349
		 * Filter the list of block editor extensions that are available through Jetpack.
350
		 *
351
		 * @since 7.0.0
352
		 *
353
		 * @param array
354
		 */
355
		self::$extensions = apply_filters( 'jetpack_set_available_extensions', self::get_available_extensions() );
356
357
		/**
358
		 * Filter the list of block editor plugins that are available through Jetpack.
359
		 *
360
		 * @deprecated 7.0.0 Use jetpack_set_available_extensions instead
361
		 *
362
		 * @since 6.8.0
363
		 *
364
		 * @param array
365
		 */
366
		self::$extensions = apply_filters( 'jetpack_set_available_blocks', self::$extensions );
367
368
		/**
369
		 * Filter the list of block editor plugins that are available through Jetpack.
370
		 *
371
		 * @deprecated 7.0.0 Use jetpack_set_available_extensions instead
372
		 *
373
		 * @since 6.9.0
374
		 *
375
		 * @param array
376
		 */
377
		self::$extensions = apply_filters( 'jetpack_set_available_plugins', self::$extensions );
378
	}
379
380
	/**
381
	 * Resets the class to its original state
382
	 *
383
	 * Used in unit tests
384
	 *
385
	 * @return void
386
	 */
387
	public static function reset() {
388
		self::$extensions   = array();
389
		self::$availability = array();
390
	}
391
392
	/**
393
	 * Return the Gutenberg extensions (blocks and plugins) directory
394
	 *
395
	 * @return string The Gutenberg extensions directory
396
	 */
397
	public static function get_blocks_directory() {
398
		/**
399
		 * Filter to select Gutenberg blocks directory
400
		 *
401
		 * @since 6.9.0
402
		 *
403
		 * @param string default: '_inc/blocks/'
404
		 */
405
		return apply_filters( 'jetpack_blocks_directory', '_inc/blocks/' );
406
	}
407
408
	/**
409
	 * Checks for a given .json file in the blocks folder.
410
	 *
411
	 * @param string $preset The name of the .json file to look for.
412
	 *
413
	 * @return bool True if the file is found.
414
	 */
415
	public static function preset_exists( $preset ) {
416
		return file_exists( JETPACK__PLUGIN_DIR . self::get_blocks_directory() . $preset . '.json' );
417
	}
418
419
	/**
420
	 * Decodes JSON loaded from a preset file in the blocks folder
421
	 *
422
	 * @param string $preset The name of the .json file to load.
423
	 *
424
	 * @return mixed Returns an object if the file is present, or false if a valid .json file is not present.
425
	 */
426
	public static function get_preset( $preset ) {
427
		return json_decode(
428
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
429
			file_get_contents( JETPACK__PLUGIN_DIR . self::get_blocks_directory() . $preset . '.json' )
430
		);
431
	}
432
433
	/**
434
	 * Returns a list of Jetpack Gutenberg extensions (blocks and plugins), based on index.json
435
	 *
436
	 * @return array A list of blocks: eg [ 'publicize', 'markdown' ]
437
	 */
438
	public static function get_jetpack_gutenberg_extensions_allowed_list() {
439
		$preset_extensions_manifest = self::preset_exists( 'index' )
440
			? self::get_preset( 'index' )
441
			: (object) array();
442
		$blocks_variation           = self::blocks_variation();
443
444
		return self::get_extensions_preset_for_variation( $preset_extensions_manifest, $blocks_variation );
445
	}
446
447
	/**
448
	 * Returns a list of Jetpack Gutenberg extensions (blocks and plugins), based on index.json
449
	 *
450
	 * @deprecated 8.7.0 Use get_jetpack_gutenberg_extensions_allowed_list()
451
	 *
452
	 * @return array A list of blocks: eg [ 'publicize', 'markdown' ]
453
	 */
454
	public static function get_jetpack_gutenberg_extensions_whitelist() {
455
		_deprecated_function( __FUNCTION__, 'jetpack-8.7.0', 'Jetpack_Gutenberg::get_jetpack_gutenberg_extensions_allowed_list' );
456
		return self::get_jetpack_gutenberg_extensions_allowed_list();
457
	}
458
459
	/**
460
	 * Returns a diff from a combined list of allowed extensions and extensions determined to be excluded
461
	 *
462
	 * @param  array $allowed_extensions An array of allowed extensions.
463
	 *
464
	 * @return array A list of blocks: eg array( 'publicize', 'markdown' )
465
	 */
466
	public static function get_available_extensions( $allowed_extensions = null ) {
467
		$exclusions         = get_option( 'jetpack_excluded_extensions', array() );
468
		$allowed_extensions = is_null( $allowed_extensions ) ? self::get_jetpack_gutenberg_extensions_allowed_list() : $allowed_extensions;
469
470
		return array_diff( $allowed_extensions, $exclusions );
471
	}
472
473
	/**
474
	 * Return true if the extension has been registered and there's nothing in the availablilty array.
475
	 *
476
	 * @param string $extension The name of the extension.
477
	 *
478
	 * @return bool whether the extension has been registered and there's nothing in the availablilty array.
479
	 */
480
	public static function is_registered_and_no_entry_in_availability( $extension ) {
481
		return self::is_registered( 'jetpack/' . $extension ) && ! isset( self::$availability[ $extension ] );
482
	}
483
484
	/**
485
	 * Return true if the extension has a true entry in the availablilty array.
486
	 *
487
	 * @param string $extension The name of the extension.
488
	 *
489
	 * @return bool whether the extension has a true entry in the availablilty array.
490
	 */
491
	public static function is_available( $extension ) {
492
		return isset( self::$availability[ $extension ] ) && true === self::$availability[ $extension ];
493
	}
494
495
	/**
496
	 * Get availability of each block / plugin.
497
	 *
498
	 * @return array A list of block and plugins and their availablity status
499
	 */
500
	public static function get_availability() {
501
		/**
502
		 * Fires before Gutenberg extensions availability is computed.
503
		 *
504
		 * In the function call you supply, use `jetpack_register_block()` to set a block as available.
505
		 * Alternatively, use `Jetpack_Gutenberg::set_extension_available()` (for a non-block plugin), and
506
		 * `Jetpack_Gutenberg::set_extension_unavailable()` (if the block or plugin should not be registered
507
		 * but marked as unavailable).
508
		 *
509
		 * @since 7.0.0
510
		 */
511
		do_action( 'jetpack_register_gutenberg_extensions' );
512
513
		$available_extensions = array();
514
515
		foreach ( self::$extensions as $extension ) {
516
			$is_available                       = self::is_registered_and_no_entry_in_availability( $extension ) || self::is_available( $extension );
517
			$available_extensions[ $extension ] = array(
518
				'available' => $is_available,
519
			);
520
521
			if ( ! $is_available ) {
522
				$reason  = isset( self::$availability[ $extension ] ) ? self::$availability[ $extension ]['reason'] : 'missing_module';
523
				$details = isset( self::$availability[ $extension ] ) ? self::$availability[ $extension ]['details'] : array();
524
				$available_extensions[ $extension ]['unavailable_reason'] = $reason;
525
				$available_extensions[ $extension ]['details']            = $details;
526
			}
527
		}
528
529
		return $available_extensions;
530
	}
531
532
	/**
533
	 * Check if an extension/block is already registered
534
	 *
535
	 * @since 7.2
536
	 *
537
	 * @param string $slug Name of extension/block to check.
538
	 *
539
	 * @return bool
540
	 */
541
	public static function is_registered( $slug ) {
542
		return WP_Block_Type_Registry::get_instance()->is_registered( $slug );
543
	}
544
545
	/**
546
	 * Check if Gutenberg editor is available
547
	 *
548
	 * @since 6.7.0
549
	 *
550
	 * @return bool
551
	 */
552
	public static function is_gutenberg_available() {
553
		return true;
554
	}
555
556
	/**
557
	 * Check whether conditions indicate Gutenberg Extensions (blocks and plugins) should be loaded
558
	 *
559
	 * Loading blocks and plugins is enabled by default and may be disabled via filter:
560
	 *   add_filter( 'jetpack_gutenberg', '__return_false' );
561
	 *
562
	 * @since 6.9.0
563
	 *
564
	 * @return bool
565
	 */
566
	public static function should_load() {
567
		if ( ! Jetpack::is_active() && ! ( new Status() )->is_offline_mode() ) {
568
			return false;
569
		}
570
571
		/**
572
		 * Filter to disable Gutenberg blocks
573
		 *
574
		 * @since 6.5.0
575
		 *
576
		 * @param bool true Whether to load Gutenberg blocks
577
		 */
578
		return (bool) apply_filters( 'jetpack_gutenberg', true );
579
	}
580
581
	/**
582
	 * Only enqueue block assets when needed.
583
	 *
584
	 * @param string $type Slug of the block.
585
	 * @param array  $script_dependencies Script dependencies. Will be merged with automatically
586
	 *                                    detected script dependencies from the webpack build.
587
	 *
588
	 * @return void
589
	 */
590
	public static function load_assets_as_required( $type, $script_dependencies = array() ) {
591
		if ( is_admin() ) {
592
			// A block's view assets will not be required in wp-admin.
593
			return;
594
		}
595
596
		$type = sanitize_title_with_dashes( $type );
597
		self::load_styles_as_required( $type );
598
		self::load_scripts_as_required( $type, $script_dependencies );
599
	}
600
601
	/**
602
	 * Only enqueue block sytles when needed.
603
	 *
604
	 * @param string $type Slug of the block.
605
	 *
606
	 * @since 7.2.0
607
	 *
608
	 * @return void
609
	 */
610
	public static function load_styles_as_required( $type ) {
611
		if ( is_admin() ) {
612
			// A block's view assets will not be required in wp-admin.
613
			return;
614
		}
615
616
		// Enqueue styles.
617
		$style_relative_path = self::get_blocks_directory() . $type . '/view' . ( is_rtl() ? '.rtl' : '' ) . '.css';
618
		if ( self::block_has_asset( $style_relative_path ) ) {
619
			$style_version = self::get_asset_version( $style_relative_path );
620
			$view_style    = plugins_url( $style_relative_path, JETPACK__PLUGIN_FILE );
621
			wp_enqueue_style( 'jetpack-block-' . $type, $view_style, array(), $style_version );
622
		}
623
624
	}
625
626
	/**
627
	 * Only enqueue block scripts when needed.
628
	 *
629
	 * @param string $type Slug of the block.
630
	 * @param array  $script_dependencies Script dependencies. Will be merged with automatically
631
	 *                             detected script dependencies from the webpack build.
632
	 *
633
	 * @since 7.2.0
634
	 *
635
	 * @return void
636
	 */
637
	public static function load_scripts_as_required( $type, $script_dependencies = array() ) {
638
		if ( is_admin() ) {
639
			// A block's view assets will not be required in wp-admin.
640
			return;
641
		}
642
643
		// Enqueue script.
644
		$script_relative_path  = self::get_blocks_directory() . $type . '/view.js';
645
		$script_deps_path      = JETPACK__PLUGIN_DIR . self::get_blocks_directory() . $type . '/view.asset.php';
646
		$script_dependencies[] = 'wp-polyfill';
647
		if ( file_exists( $script_deps_path ) ) {
648
			$asset_manifest      = include $script_deps_path;
649
			$script_dependencies = array_unique( array_merge( $script_dependencies, $asset_manifest['dependencies'] ) );
650
		}
651
652
		if ( ( ! class_exists( 'Jetpack_AMP_Support' ) || ! Jetpack_AMP_Support::is_amp_request() ) && self::block_has_asset( $script_relative_path ) ) {
653
			$script_version = self::get_asset_version( $script_relative_path );
654
			$view_script    = plugins_url( $script_relative_path, JETPACK__PLUGIN_FILE );
655
			wp_enqueue_script( 'jetpack-block-' . $type, $view_script, $script_dependencies, $script_version, false );
656
		}
657
658
		wp_localize_script(
659
			'jetpack-block-' . $type,
660
			'Jetpack_Block_Assets_Base_Url',
661
			plugins_url( self::get_blocks_directory(), JETPACK__PLUGIN_FILE )
662
		);
663
	}
664
665
	/**
666
	 * Check if an asset exists for a block.
667
	 *
668
	 * @param string $file Path of the file we are looking for.
669
	 *
670
	 * @return bool $block_has_asset Does the file exist.
671
	 */
672
	public static function block_has_asset( $file ) {
673
		return file_exists( JETPACK__PLUGIN_DIR . $file );
674
	}
675
676
	/**
677
	 * Get the version number to use when loading the file. Allows us to bypass cache when developing.
678
	 *
679
	 * @param string $file Path of the file we are looking for.
680
	 *
681
	 * @return string $script_version Version number.
682
	 */
683
	public static function get_asset_version( $file ) {
684
		return Jetpack::is_development_version() && self::block_has_asset( $file )
685
			? filemtime( JETPACK__PLUGIN_DIR . $file )
686
			: JETPACK__VERSION;
687
	}
688
689
	/**
690
	 * Load Gutenberg editor assets
691
	 *
692
	 * @since 6.7.0
693
	 *
694
	 * @return void
695
	 */
696
	public static function enqueue_block_editor_assets() {
697
		if ( ! self::should_load() ) {
698
			return;
699
		}
700
701
		// Required for Analytics. See _inc/lib/admin-pages/class.jetpack-admin-page.php.
702
		if ( ! ( new Status() )->is_offline_mode() && Jetpack::is_active() ) {
703
			wp_enqueue_script( 'jp-tracks', '//stats.wp.com/w.js', array(), gmdate( 'YW' ), true );
704
		}
705
706
		$rtl              = is_rtl() ? '.rtl' : '';
707
		$blocks_dir       = self::get_blocks_directory();
708
		$blocks_variation = self::blocks_variation();
709
710
		if ( 'production' !== $blocks_variation ) {
711
			$blocks_env = '-' . esc_attr( $blocks_variation );
712
		} else {
713
			$blocks_env = '';
714
		}
715
716
		$editor_script = plugins_url( "{$blocks_dir}editor{$blocks_env}.js", JETPACK__PLUGIN_FILE );
717
		$editor_style  = plugins_url( "{$blocks_dir}editor{$blocks_env}{$rtl}.css", JETPACK__PLUGIN_FILE );
718
719
		$editor_deps_path = JETPACK__PLUGIN_DIR . $blocks_dir . "editor{$blocks_env}.asset.php";
720
		$editor_deps      = array( 'wp-polyfill' );
721
		if ( file_exists( $editor_deps_path ) ) {
722
			$asset_manifest = include $editor_deps_path;
723
			$editor_deps    = $asset_manifest['dependencies'];
724
		}
725
726
		$version = Jetpack::is_development_version() && file_exists( JETPACK__PLUGIN_DIR . $blocks_dir . 'editor.js' )
727
			? filemtime( JETPACK__PLUGIN_DIR . $blocks_dir . 'editor.js' )
728
			: JETPACK__VERSION;
729
730
		if ( method_exists( 'Jetpack', 'build_raw_urls' ) ) {
731
			$site_fragment = Jetpack::build_raw_urls( home_url() );
732
		} elseif ( class_exists( 'WPCOM_Masterbar' ) && method_exists( 'WPCOM_Masterbar', 'get_calypso_site_slug' ) ) {
733
			$site_fragment = WPCOM_Masterbar::get_calypso_site_slug( get_current_blog_id() );
734
		} else {
735
			$site_fragment = '';
736
		}
737
738
		wp_enqueue_script(
739
			'jetpack-blocks-editor',
740
			$editor_script,
741
			$editor_deps,
742
			$version,
743
			false
744
		);
745
746
		wp_localize_script(
747
			'jetpack-blocks-editor',
748
			'Jetpack_Block_Assets_Base_Url',
749
			plugins_url( $blocks_dir . '/', JETPACK__PLUGIN_FILE )
750
		);
751
752
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
753
			$user                      = wp_get_current_user();
754
			$user_data                 = array(
755
				'userid'   => $user->ID,
756
				'username' => $user->user_login,
757
			);
758
			$blog_id                   = get_current_blog_id();
759
			$is_current_user_connected = true;
760
		} else {
761
			$user_data                 = Jetpack_Tracks_Client::get_connected_user_tracks_identity();
762
			$blog_id                   = Jetpack_Options::get_option( 'id', 0 );
763
			$is_current_user_connected = Jetpack::is_user_connected();
764
		}
765
766
		wp_localize_script(
767
			'jetpack-blocks-editor',
768
			'Jetpack_Editor_Initial_State',
769
			array(
770
				'available_blocks' => self::get_availability(),
771
				'jetpack'          => array(
772
					'is_active'                 => Jetpack::is_active(),
773
					'is_current_user_connected' => $is_current_user_connected,
774
					/** This filter is documented in class.jetpack-gutenberg.php */
775
					'enable_upgrade_nudge'      => apply_filters( 'jetpack_block_editor_enable_upgrade_nudge', false ),
776
				),
777
				'siteFragment'     => $site_fragment,
778
				'adminUrl'         => esc_url( admin_url() ),
779
				'tracksUserData'   => $user_data,
780
				'wpcomBlogId'      => $blog_id,
781
				'allowedMimeTypes' => wp_get_mime_types(),
782
			)
783
		);
784
785
		wp_set_script_translations( 'jetpack-blocks-editor', 'jetpack' );
786
787
		wp_enqueue_style( 'jetpack-blocks-editor', $editor_style, array(), $version );
788
	}
789
790
	/**
791
	 * Some blocks do not depend on a specific module,
792
	 * and can consequently be loaded outside of the usual modules.
793
	 * We will look for such modules in the extensions/ directory.
794
	 *
795
	 * @since 7.1.0
796
	 */
797
	public static function load_independent_blocks() {
798
		if ( self::should_load() ) {
799
			/**
800
			 * Look for files that match our list of available Jetpack Gutenberg extensions (blocks and plugins).
801
			 * If available, load them.
802
			 */
803
			foreach ( self::$extensions as $extension ) {
804
				$extension_file_glob = glob( JETPACK__PLUGIN_DIR . 'extensions/*/' . $extension . '/' . $extension . '.php' );
805
				if ( ! empty( $extension_file_glob ) ) {
806
					include_once $extension_file_glob[0];
807
				}
808
			}
809
		}
810
	}
811
812
	/**
813
	 * Loads PHP components of extended-blocks.
814
	 *
815
	 * @since 8.9.0
816
	 */
817
	public static function load_extended_blocks() {
818
		if ( self::should_load() ) {
819
			$extended_blocks = glob( JETPACK__PLUGIN_DIR . 'extensions/extended-blocks/*' );
820
821
			foreach ( $extended_blocks as $block ) {
822
				$name = basename( $block );
823
				$path = JETPACK__PLUGIN_DIR . 'extensions/extended-blocks/' . $name . '/' . $name . '.php';
824
825
				if ( file_exists( $path ) ) {
826
					include_once $path;
827
				}
828
			}
829
		}
830
	}
831
832
	/**
833
	 * Get CSS classes for a block.
834
	 *
835
	 * @since 7.7.0
836
	 *
837
	 * @param string $slug  Block slug.
838
	 * @param array  $attr  Block attributes.
839
	 * @param array  $extra Potential extra classes you may want to provide.
840
	 *
841
	 * @return string $classes List of CSS classes for a block.
842
	 */
843
	public static function block_classes( $slug = '', $attr, $extra = array() ) {
844
		if ( empty( $slug ) ) {
845
			return '';
846
		}
847
848
		// Basic block name class.
849
		$classes = array(
850
			'wp-block-jetpack-' . $slug,
851
		);
852
853
		// Add alignment if provided.
854
		if (
855
			! empty( $attr['align'] )
856
			&& in_array( $attr['align'], array( 'left', 'center', 'right', 'wide', 'full' ), true )
857
		) {
858
			array_push( $classes, 'align' . $attr['align'] );
859
		}
860
861
		// Add custom classes if provided in the block editor.
862
		if ( ! empty( $attr['className'] ) ) {
863
			array_push( $classes, $attr['className'] );
864
		}
865
866
		// Add any extra classes.
867
		if ( is_array( $extra ) && ! empty( $extra ) ) {
868
			$classes = array_merge( $classes, array_filter( $extra ) );
869
		}
870
871
		return implode( ' ', $classes );
872
	}
873
874
	/**
875
	 * Determine whether a site should use the default set of blocks, or a custom set.
876
	 * Possible variations are currently beta, experimental, and production.
877
	 *
878
	 * @since 8.1.0
879
	 *
880
	 * @return string $block_varation production|beta|experimental
881
	 */
882
	public static function blocks_variation() {
883
		// Default to production blocks.
884
		$block_varation = 'production';
885
886
		if ( Constants::is_true( 'JETPACK_BETA_BLOCKS' ) ) {
887
			$block_varation = 'beta';
888
		}
889
890
		/*
891
		 * Switch to experimental blocks if you use the JETPACK_EXPERIMENTAL_BLOCKS constant.
892
		 */
893
		if ( Constants::is_true( 'JETPACK_EXPERIMENTAL_BLOCKS' ) ) {
894
			$block_varation = 'experimental';
895
		}
896
897
		/**
898
		 * Allow customizing the variation of blocks in use on a site.
899
		 *
900
		 * @since 8.1.0
901
		 *
902
		 * @param string $block_variation Can be beta, experimental, and production. Defaults to production.
903
		 */
904
		return apply_filters( 'jetpack_blocks_variation', $block_varation );
905
	}
906
907
	/**
908
	 * Get a list of extensions available for the variation you chose.
909
	 *
910
	 * @since 8.1.0
911
	 *
912
	 * @param obj    $preset_extensions_manifest List of extensions available in Jetpack.
913
	 * @param string $blocks_variation           Subset of blocks. production|beta|experimental.
914
	 *
915
	 * @return array $preset_extensions Array of extensions for that variation
916
	 */
917
	public static function get_extensions_preset_for_variation( $preset_extensions_manifest, $blocks_variation ) {
918
		$preset_extensions = isset( $preset_extensions_manifest->{ $blocks_variation } )
919
				? (array) $preset_extensions_manifest->{ $blocks_variation }
920
				: array();
921
922
		/*
923
		 * Experimental and Beta blocks need the production blocks as well.
924
		 */
925 View Code Duplication
		if (
926
			'experimental' === $blocks_variation
927
			|| 'beta' === $blocks_variation
928
		) {
929
			$production_extensions = isset( $preset_extensions_manifest->production )
930
				? (array) $preset_extensions_manifest->production
931
				: array();
932
933
			$preset_extensions = array_unique( array_merge( $preset_extensions, $production_extensions ) );
934
		}
935
936
		/*
937
		 * Beta blocks need the experimental blocks as well.
938
		 *
939
		 * If you've chosen to see Beta blocks,
940
		 * we want to make all blocks available to you:
941
		 * - Production
942
		 * - Experimental
943
		 * - Beta
944
		 */
945 View Code Duplication
		if ( 'beta' === $blocks_variation ) {
946
			$production_extensions = isset( $preset_extensions_manifest->experimental )
947
				? (array) $preset_extensions_manifest->experimental
948
				: array();
949
950
			$preset_extensions = array_unique( array_merge( $preset_extensions, $production_extensions ) );
951
		}
952
953
		return $preset_extensions;
954
	}
955
956
	/**
957
	 * Validate a URL used in a SSR block.
958
	 *
959
	 * @since 8.3.0
960
	 *
961
	 * @param string $url      URL saved as an attribute in block.
962
	 * @param array  $allowed  Array of allowed hosts for that block, or regexes to check against.
963
	 * @param bool   $is_regex Array of regexes matching the URL that could be used in block.
964
	 *
965
	 * @return bool|string
966
	 */
967
	public static function validate_block_embed_url( $url, $allowed = array(), $is_regex = false ) {
968
		if (
969
			empty( $url )
970
			|| ! is_array( $allowed )
971
			|| empty( $allowed )
972
		) {
973
			return false;
974
		}
975
976
		$url_components = wp_parse_url( $url );
977
978
		// Bail early if we cannot find a host.
979
		if ( empty( $url_components['host'] ) ) {
980
			return false;
981
		}
982
983
		// Normalize URL.
984
		$url = sprintf(
985
			'%s://%s%s%s',
986
			isset( $url_components['scheme'] ) ? $url_components['scheme'] : 'https',
987
			$url_components['host'],
988
			isset( $url_components['path'] ) ? $url_components['path'] : '/',
989
			isset( $url_components['query'] ) ? '?' . $url_components['query'] : ''
990
		);
991
992
		if ( ! empty( $url_components['fragment'] ) ) {
993
			$url = $url . '#' . rawurlencode( $url_components['fragment'] );
994
		}
995
996
		/*
997
		 * If we're using an allowed list of hosts,
998
		 * check if the URL belongs to one of the domains allowed for that block.
999
		 */
1000
		if (
1001
			false === $is_regex
1002
			&& in_array( $url_components['host'], $allowed, true )
1003
		) {
1004
			return $url;
1005
		}
1006
1007
		/*
1008
		 * If we are using an array of regexes to check against,
1009
		 * loop through that.
1010
		 */
1011
		if ( true === $is_regex ) {
1012
			foreach ( $allowed as $regex ) {
1013
				if ( 1 === preg_match( $regex, $url ) ) {
1014
					return $url;
1015
				}
1016
			}
1017
		}
1018
1019
		return false;
1020
	}
1021
1022
	/**
1023
	 * Output an UpgradeNudge Component on the frontend of a site.
1024
	 *
1025
	 * @since 8.4.0
1026
	 *
1027
	 * @param string $plan The plan that users need to purchase to make the block work.
1028
	 *
1029
	 * @return string
1030
	 */
1031
	public static function upgrade_nudge( $plan ) {
1032
		if (
1033
			! current_user_can( 'manage_options' )
1034
			/** This filter is documented in class.jetpack-gutenberg.php */
1035
			|| ! apply_filters( 'jetpack_block_editor_enable_upgrade_nudge', false )
1036
			/** This filter is documented in _inc/lib/admin-pages/class.jetpack-react-page.php */
1037
			|| ! apply_filters( 'jetpack_show_promotions', true )
1038
			|| is_feed()
1039
		) {
1040
			return;
1041
		}
1042
1043
		jetpack_require_lib( 'components' );
1044
		return Jetpack_Components::render_upgrade_nudge(
1045
			array(
1046
				'plan' => $plan,
1047
			)
1048
		);
1049
	}
1050
1051
	/**
1052
	 * Output a notice within a block.
1053
	 *
1054
	 * @since 8.6.0
1055
	 *
1056
	 * @param string $message Notice we want to output.
1057
	 * @param string $status  Status of the notice. Can be one of success, info, warning, error. info by default.
1058
	 * @param string $classes List of CSS classes.
1059
	 *
1060
	 * @return string
1061
	 */
1062
	public static function notice( $message, $status = 'info', $classes = '' ) {
1063
		if (
1064
			empty( $message )
1065
			|| ! in_array( $status, array( 'success', 'info', 'warning', 'error' ), true )
1066
		) {
1067
			return '';
1068
		}
1069
1070
		$color = '';
1071
		switch ( $status ) {
1072
			case 'success':
1073
				$color = '#46b450';
1074
				break;
1075
			case 'warning':
1076
				$color = '#ffb900';
1077
				break;
1078
			case 'error':
1079
				$color = '#dc3232';
1080
				break;
1081
			case 'info':
1082
			default:
1083
				$color = '#00a0d2';
1084
				break;
1085
		}
1086
1087
		return sprintf(
1088
			'<div class="jetpack-block__notice %1$s %3$s" style="border-left:5px solid %4$s;padding:1em;background-color:#f8f9f9;">%2$s</div>',
1089
			esc_attr( $status ),
1090
			wp_kses(
1091
				$message,
1092
				array(
1093
					'br' => array(),
1094
					'p'  => array(),
1095
				)
1096
			),
1097
			esc_attr( $classes ),
1098
			sanitize_hex_color( $color )
1099
		);
1100
	}
1101
1102
	/**
1103
	 * Set the availability of the block as the editor
1104
	 * is loaded.
1105
	 *
1106
	 * @param string $slug Slug of the block.
1107
	 */
1108
	public static function set_availability_for_plan( $slug ) {
1109
		$is_available = true;
1110
		$plan         = '';
1111
		$slug         = self::remove_extension_prefix( $slug );
1112
1113
		if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
1114
			if ( ! class_exists( 'Store_Product_List' ) ) {
1115
				require WP_CONTENT_DIR . '/admin-plugins/wpcom-billing/store-product-list.php';
1116
			}
1117
			$features_data = Store_Product_List::get_site_specific_features_data();
1118
			$is_available  = in_array( $slug, $features_data['active'], true );
1119
			if ( ! empty( $features_data['available'][ $slug ] ) ) {
1120
				$plan = $features_data['available'][ $slug ][0];
1121
			}
1122
		} elseif ( ! jetpack_is_atomic_site() ) {
1123
			/*
1124
			 * If it's Atomic then assume all features are available
1125
			 * otherwise check against the Jetpack plan.
1126
			 */
1127
			$is_available = Jetpack_Plan::supports( $slug );
1128
			$plan         = Jetpack_Plan::get_minimum_plan_for_feature( $slug );
1129
		}
1130
		if ( $is_available ) {
1131
			self::set_extension_available( $slug );
1132
		} else {
1133
			self::set_extension_unavailable(
1134
				$slug,
1135
				'missing_plan',
1136
				array(
1137
					'required_feature' => $slug,
1138
					'required_plan'    => $plan,
1139
				)
1140
			);
1141
		}
1142
	}
1143
1144
	/**
1145
	 * Wraps the suplied render_callback in a function to check
1146
	 * the availability of the block before rendering it.
1147
	 *
1148
	 * @param string   $slug The block slug, used to check for availability.
1149
	 * @param callable $render_callback The render_callback that will be called if the block is available.
1150
	 */
1151
	public static function get_render_callback_with_availability_check( $slug, $render_callback ) {
1152
		return function ( $prepared_attributes, $block_content ) use ( $render_callback, $slug ) {
1153
			$availability = self::get_availability();
1154
			$bare_slug    = self::remove_extension_prefix( $slug );
1155
			if ( isset( $availability[ $bare_slug ] ) && $availability[ $bare_slug ]['available'] ) {
1156
				return call_user_func( $render_callback, $prepared_attributes, $block_content );
1157
			}
1158
1159
			return null;
1160
		};
1161
	}
1162
}
1163