Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
39.19% covered (danger)
39.19%
185 / 472
33.33% covered (danger)
33.33%
14 / 42
CRAP
0.00% covered (danger)
0.00%
0 / 1
Jetpack_Gutenberg
39.53% covered (danger)
39.53%
185 / 468
33.33% covered (danger)
33.33%
14 / 42
7835.76
0.00% covered (danger)
0.00%
0 / 1
 is_gutenberg_version_available
87.50% covered (warning)
87.50%
21 / 24
0.00% covered (danger)
0.00%
0 / 1
8.12
 prepend_block_prefix
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 remove_extension_prefix
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 share_items
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 set_extension_available
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 set_extension_unavailable
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
4.02
 init
n/a
0 / 0
n/a
0 / 0
1
 reset
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 get_blocks_directory
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 preset_exists
n/a
0 / 0
n/a
0 / 0
1
 get_preset
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 get_jetpack_gutenberg_extensions_allowed_list
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 get_available_extensions
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
3.07
 is_registered_and_no_entry_in_availability
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 is_available
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 get_cached_availability
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
6
 get_availability
100.00% covered (success)
100.00%
13 / 13
100.00% covered (success)
100.00%
1 / 1
6
 get_extensions
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
4.25
 is_registered
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_gutenberg_available
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 should_load
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 register_blocks_assets_base_url
100.00% covered (success)
100.00%
10 / 10
100.00% covered (success)
100.00%
1 / 1
3
 load_assets_as_required
90.00% covered (success)
90.00%
9 / 10
0.00% covered (danger)
0.00%
0 / 1
4.02
 load_styles_as_required
27.27% covered (danger)
27.27%
3 / 11
0.00% covered (danger)
0.00%
0 / 1
19.85
 load_scripts_as_required
31.82% covered (danger)
31.82%
7 / 22
0.00% covered (danger)
0.00%
0 / 1
28.29
 block_has_asset
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_asset_version
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
12
 enqueue_block_editor_assets
0.00% covered (danger)
0.00%
0 / 90
0.00% covered (danger)
0.00%
0 / 1
210
 load_independent_blocks
12.50% covered (danger)
12.50%
1 / 8
0.00% covered (danger)
0.00%
0 / 1
21.75
 is_block_editor_context
85.00% covered (warning)
85.00%
17 / 20
0.00% covered (danger)
0.00%
0 / 1
14.66
 load_block_editor_extensions
6.25% covered (danger)
6.25%
1 / 16
0.00% covered (danger)
0.00%
0 / 1
47.37
 blocks_variation
88.00% covered (warning)
88.00%
22 / 25
0.00% covered (danger)
0.00%
0 / 1
9.14
 get_extensions_preset_for_variation
40.00% covered (danger)
40.00%
6 / 15
0.00% covered (danger)
0.00%
0 / 1
17.58
 validate_block_embed_url
95.83% covered (success)
95.83%
23 / 24
0.00% covered (danger)
0.00%
0 / 1
12
 should_show_frontend_preview
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
12
 upgrade_nudge
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
2
 notice
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
72
 get_site_specific_features
0.00% covered (danger)
0.00%
0 / 8
0.00% covered (danger)
0.00%
0 / 1
12
 set_availability_for_plan
0.00% covered (danger)
0.00%
0 / 27
0.00% covered (danger)
0.00%
0 / 1
90
 get_render_callback_with_availability_check
0.00% covered (danger)
0.00%
0 / 14
0.00% covered (danger)
0.00%
0 / 1
56
 display_deprecated_block_message
0.00% covered (danger)
0.00%
0 / 9
0.00% covered (danger)
0.00%
0 / 1
20
 register_block_metadata_collection
33.33% covered (danger)
33.33%
2 / 6
0.00% covered (danger)
0.00%
0 / 1
3.19
 set_block_js_loading_strategy
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 get_block_js_loading_strategy
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
2
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
9use Automattic\Jetpack\Assets;
10use Automattic\Jetpack\Blocks;
11use Automattic\Jetpack\Connection\Initial_State as Connection_Initial_State;
12use Automattic\Jetpack\Connection\Manager as Connection_Manager;
13use Automattic\Jetpack\Constants;
14use Automattic\Jetpack\Current_Plan as Jetpack_Plan;
15use Automattic\Jetpack\Modules;
16use Automattic\Jetpack\My_Jetpack\Initializer as My_Jetpack_Initializer;
17use Automattic\Jetpack\Status;
18use Automattic\Jetpack\Status\Host;
19
20if ( ! defined( 'ABSPATH' ) ) {
21    exit( 0 );
22}
23
24/**
25 * General Gutenberg editor specific functionality
26 */
27class Jetpack_Gutenberg {
28
29    /**
30     * Only these extensions can be registered. Used to control availability of beta blocks.
31     *
32     * @var array|null Extensions allowed list or `null` if not initialized yet.
33     * @see static::get_extensions()
34     */
35    private static $extensions = null;
36
37    /**
38     * Keeps track of the reasons why a given extension is unavailable.
39     *
40     * @var array Extensions availability information
41     */
42    private static $availability = array();
43
44    /**
45     * A cached array of the fully processed availability data. Keeps track of
46     * reasons why an extension is unavailable or missing.
47     *
48     * @var array Extensions availability information.
49     */
50    private static $cached_availability = null;
51
52    /**
53     * Site-specific features available.
54     * Their calculation can be expensive and slow, so we're caching it for the request.
55     *
56     * @var array Site-specific features
57     */
58    private static $site_specific_features = array();
59
60    /**
61     * List of deprecated blocks.
62     *
63     * @var array List of deprecated blocks.
64     */
65    private static $deprecated_blocks = array(
66        'jetpack/revue',
67    );
68
69    /**
70     * Fallback minimum plan requirements for WordPress.com/Atomic sites.
71     *
72     * Used when features have conditional availability (e.g., sticker-based gating)
73     * and don't appear in features_data['available']. This only affects the upsell
74     * message shown to users.
75     *
76     * @since 15.5
77     * @var array Feature slug => minimum WordPress.com plan slug.
78     */
79    private static $wpcom_minimum_plan_fallbacks = array(
80        'donations'              => 'value_bundle',
81        'payment-buttons'        => 'value_bundle',
82        'paypal-payment-buttons' => 'value_bundle',
83    );
84
85    /**
86     * Storing the contents of the preset file.
87     *
88     * Already been json_decode.
89     *
90     * @var null|object JSON decoded object after first usage.
91     */
92    private static $preset_cache = null;
93
94    /**
95     * Keep track of JS loading strategies for each block that needs it.
96     *
97     * @var array<string, array|bool>
98     *
99     * @since 15.0
100     */
101    private static $block_js_loading_strategies = array();
102
103    /**
104     * Check to see if a minimum version of Gutenberg is available. Because a Gutenberg version is not available in
105     * php if the Gutenberg plugin is not installed, if we know which minimum WP release has the required version we can
106     * optionally fall back to that.
107     *
108     * @param array  $version_requirements An array containing the required Gutenberg version and, if known, the WordPress version that was released with this minimum version.
109     * @param string $slug The slug of the block or plugin that has the gutenberg version requirement.
110     *
111     * @since 8.3.0
112     *
113     * @return boolean True if the version of gutenberg required by the block or plugin is available.
114     */
115    public static function is_gutenberg_version_available( $version_requirements, $slug ) {
116        global $wp_version;
117
118        // Bail if we don't at least have the gutenberg version requirement, the WP version is optional.
119        if ( empty( $version_requirements['gutenberg'] ) ) {
120            return false;
121        }
122
123        // If running a local dev build of gutenberg plugin GUTENBERG_DEVELOPMENT_MODE is set so assume correct version.
124        if ( defined( 'GUTENBERG_DEVELOPMENT_MODE' ) && GUTENBERG_DEVELOPMENT_MODE ) {
125            return true;
126        }
127
128        $version_available = false;
129
130        // If running a production build of the gutenberg plugin then GUTENBERG_VERSION is set, otherwise if WP version
131        // with required version of Gutenberg is known check that.
132        if ( defined( 'GUTENBERG_VERSION' ) ) {
133            $version_available = version_compare( GUTENBERG_VERSION, $version_requirements['gutenberg'], '>=' );
134        } elseif ( ! empty( $version_requirements['wp'] ) ) {
135            $version_available = version_compare( $wp_version, $version_requirements['wp'], '>=' );
136        }
137
138        if ( ! $version_available ) {
139            $slug = self::remove_extension_prefix( $slug );
140            self::set_extension_unavailable(
141                $slug,
142                'incorrect_gutenberg_version',
143                array(
144                    'required_feature' => $slug,
145                    'required_version' => $version_requirements,
146                    'current_version'  => array(
147                        'wp'        => $wp_version,
148                        'gutenberg' => defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : null,
149                    ),
150                )
151            );
152        }
153
154        return $version_available;
155    }
156
157    /**
158     * Prepend the 'jetpack/' prefix to a block name
159     *
160     * @param string $block_name The block name.
161     *
162     * @return string The prefixed block name.
163     */
164    private static function prepend_block_prefix( $block_name ) {
165        return 'jetpack/' . $block_name;
166    }
167
168    /**
169     * Remove the 'jetpack/' or jetpack-' prefix from an extension name
170     *
171     * @param string $extension_name The extension name.
172     *
173     * @return string The unprefixed extension name.
174     */
175    public static function remove_extension_prefix( $extension_name ) {
176        if ( str_starts_with( $extension_name, 'jetpack/' ) || str_starts_with( $extension_name, 'jetpack-' ) ) {
177            return substr( $extension_name, strlen( 'jetpack/' ) );
178        }
179        return $extension_name;
180    }
181
182    /**
183     * Whether two arrays share at least one item
184     *
185     * @param array $a An array.
186     * @param array $b Another array.
187     *
188     * @return boolean True if $a and $b share at least one item
189     */
190    protected static function share_items( $a, $b ) {
191        return array_intersect( $a, $b ) !== array();
192    }
193
194    /**
195     * Set a (non-block) extension as available
196     *
197     * @param string $slug Slug of the extension.
198     */
199    public static function set_extension_available( $slug ) {
200        $slug                        = self::remove_extension_prefix( $slug );
201        self::$availability[ $slug ] = true;
202    }
203
204    /**
205     * Set the reason why an extension (block or plugin) is unavailable
206     *
207     * @param string $slug Slug of the extension.
208     * @param string $reason A string representation of why the extension is unavailable.
209     * @param array  $details A free-form array containing more information on why the extension is unavailable.
210     */
211    public static function set_extension_unavailable( $slug, $reason, $details = array() ) {
212        if (
213            // Extensions that require a plan may be eligible for upgrades.
214            'missing_plan' === $reason
215            && (
216                /**
217                 * Filter 'jetpack_block_editor_enable_upgrade_nudge' with `true` to enable or `false`
218                 * to disable paid feature upgrade nudges in the block editor.
219                 *
220                 * When this is changed to default to `true`, you should also update `modules/memberships/class-jetpack-memberships.php`
221                 * See https://github.com/Automattic/jetpack/pull/13394#pullrequestreview-293063378
222                 *
223                 * @since 7.7.0
224                 *
225                 * @param boolean
226                 */
227                ! apply_filters( 'jetpack_block_editor_enable_upgrade_nudge', false )
228                /** This filter is documented in _inc/lib/admin-pages/class.jetpack-react-page.php */
229                || ! apply_filters( 'jetpack_show_promotions', true )
230            )
231        ) {
232            // The block editor may apply an upgrade nudge if `missing_plan` is the reason.
233            // Add a descriptive suffix to disable behavior but provide informative reason.
234            $reason .= '__nudge_disabled';
235        }
236        $slug                        = self::remove_extension_prefix( $slug );
237        self::$availability[ $slug ] = array(
238            'reason'  => $reason,
239            'details' => $details,
240        );
241    }
242
243    /**
244     * Used to initialize the class, no longer in use.
245     *
246     * @return void
247     * @deprecated 12.2 No longer needed.
248     */
249    public static function init() {
250        _deprecated_function( __METHOD__, '12.2' );
251    }
252
253    /**
254     * Resets the class to its original state
255     *
256     * Used in unit tests
257     *
258     * @return void
259     */
260    public static function reset() {
261        self::$extensions                  = null;
262        self::$availability                = array();
263        self::$cached_availability         = null;
264        self::$block_js_loading_strategies = array();
265    }
266
267    /**
268     * Return the Gutenberg extensions (blocks and plugins) directory
269     *
270     * @return string The Gutenberg extensions directory
271     */
272    public static function get_blocks_directory() {
273        /**
274         * Filter to select Gutenberg blocks directory
275         *
276         * @since 6.9.0
277         *
278         * @param string default: '_inc/blocks/'
279         */
280        return apply_filters( 'jetpack_blocks_directory', '_inc/blocks/' );
281    }
282
283    /**
284     * Checks for a given .json file in the blocks folder.
285     *
286     * @deprecated 14.3
287     *
288     * @param string $preset The name of the .json file to look for.
289     *
290     * @return bool True if the file is found.
291     */
292    public static function preset_exists( $preset ) {
293        _deprecated_function( __METHOD__, '14.3' );
294        return file_exists( JETPACK__PLUGIN_DIR . self::get_blocks_directory() . $preset . '.json' );
295    }
296
297    /**
298     * Decodes JSON loaded from the preset file in the blocks folder
299     *
300     * @since 14.3 Deprecated argument. Only one value is ever used.
301     *
302     * @param null $deprecated No longer used.
303     *
304     * @return mixed Returns an object if the file is present, or false if a valid .json file is not present.
305     */
306    public static function get_preset( $deprecated = null ) {
307        if ( $deprecated ) {
308            _deprecated_argument( __METHOD__, '14.3', 'The $preset argument is no longer needed or used.' );
309        }
310
311        if ( self::$preset_cache ) {
312            return self::$preset_cache;
313        }
314
315        self::$preset_cache = json_decode(
316        // phpcs:ignore WordPress.WP.AlternativeFunctions.file_get_contents_file_get_contents
317            file_get_contents( JETPACK__PLUGIN_DIR . self::get_blocks_directory() . 'index.json' )
318        );
319        return self::$preset_cache;
320    }
321
322    /**
323     * Returns a list of Jetpack Gutenberg extensions (blocks and plugins), based on index.json
324     *
325     * @return array A list of blocks: eg [ 'publicize', 'markdown' ]
326     */
327    public static function get_jetpack_gutenberg_extensions_allowed_list() {
328        $preset_extensions_manifest = ( defined( 'TESTING_IN_JETPACK' ) && TESTING_IN_JETPACK ) ? array() : self::get_preset();
329        $blocks_variation           = self::blocks_variation();
330
331        return self::get_extensions_preset_for_variation( $preset_extensions_manifest, $blocks_variation );
332    }
333
334    /**
335     * Returns a diff from a combined list of allowed extensions and extensions determined to be excluded
336     *
337     * @param  array $allowed_extensions An array of allowed extensions.
338     *
339     * @return array A list of blocks: eg array( 'publicize', 'markdown' )
340     */
341    public static function get_available_extensions( $allowed_extensions = null ) {
342        $exclusions         = get_option( 'jetpack_excluded_extensions', array() );
343        $allowed_extensions = $allowed_extensions === null ? self::get_jetpack_gutenberg_extensions_allowed_list() : $allowed_extensions;
344
345        // Avoid errors if option data is not as expected.
346        if ( ! is_array( $exclusions ) ) {
347            $exclusions = array();
348        }
349
350        return array_diff( $allowed_extensions, $exclusions );
351    }
352
353    /**
354     * Return true if the extension has been registered and there's nothing in the availablilty array.
355     *
356     * @param string $extension The name of the extension.
357     *
358     * @return bool whether the extension has been registered and there's nothing in the availablilty array.
359     */
360    public static function is_registered_and_no_entry_in_availability( $extension ) {
361        return self::is_registered( 'jetpack/' . $extension ) && ! isset( self::$availability[ $extension ] );
362    }
363
364    /**
365     * Return true if the extension has a true entry in the availablilty array.
366     *
367     * @param string $extension The name of the extension.
368     *
369     * @return bool whether the extension has a true entry in the availablilty array.
370     */
371    public static function is_available( $extension ) {
372        return isset( self::$availability[ $extension ] ) && true === self::$availability[ $extension ];
373    }
374
375    /**
376     * Get the availability of each block / plugin, or return the cached availability
377     * if it has already been calculated. Avoids re-registering extensions when not
378     * necessary.
379     *
380     * @return array A list of block and plugins and their availability status.
381     */
382    public static function get_cached_availability() {
383        if ( null === self::$cached_availability ) {
384            self::$cached_availability = self::get_availability();
385        }
386        return self::$cached_availability;
387    }
388
389    /**
390     * Get availability of each block / plugin.
391     *
392     * @return array A list of block and plugins and their availablity status
393     */
394    public static function get_availability() {
395        /**
396         * Fires before Gutenberg extensions availability is computed.
397         *
398         * In the function call you supply, use `Blocks::jetpack_register_block()` to set a block as available.
399         * Alternatively, use `Jetpack_Gutenberg::set_extension_available()` (for a non-block plugin), and
400         * `Jetpack_Gutenberg::set_extension_unavailable()` (if the block or plugin should not be registered
401         * but marked as unavailable).
402         *
403         * @since 7.0.0
404         */
405        do_action( 'jetpack_register_gutenberg_extensions' );
406
407        $available_extensions = array();
408
409        foreach ( static::get_extensions() as $extension ) {
410            $is_available                       = self::is_registered_and_no_entry_in_availability( $extension ) || self::is_available( $extension );
411            $available_extensions[ $extension ] = array(
412                'available' => $is_available,
413            );
414
415            if ( ! $is_available ) {
416                $reason  = isset( self::$availability[ $extension ] ) ? self::$availability[ $extension ]['reason'] : 'missing_module';
417                $details = isset( self::$availability[ $extension ] ) ? self::$availability[ $extension ]['details'] : array();
418                $available_extensions[ $extension ]['unavailable_reason'] = $reason;
419                $available_extensions[ $extension ]['details']            = $details;
420            }
421        }
422
423        return $available_extensions;
424    }
425
426    /**
427     * Return the list of extensions that are available.
428     *
429     * @since 11.9
430     *
431     * @return array A list of block and plugins and their availability status.
432     */
433    public static function get_extensions() {
434        if ( ! static::should_load() ) {
435            return array();
436        }
437
438        if ( null === self::$extensions ) {
439            /**
440             * Filter the list of block editor extensions that are available through Jetpack.
441             *
442             * @since 7.0.0
443             *
444             * @param array
445             */
446            self::$extensions = apply_filters( 'jetpack_set_available_extensions', self::get_available_extensions() );
447
448            if ( ! is_array( self::$extensions ) ) {
449                _doing_it_wrong( __METHOD__, esc_html__( 'The jetpack_set_available_extensions filter must return an array.', 'jetpack' ), '14.9' );
450                self::$extensions = array();
451            }
452        }
453
454        return self::$extensions;
455    }
456
457    /**
458     * Check if an extension/block is already registered
459     *
460     * @since 7.2
461     *
462     * @param string $slug Name of extension/block to check.
463     *
464     * @return bool
465     */
466    public static function is_registered( $slug ) {
467        return WP_Block_Type_Registry::get_instance()->is_registered( $slug );
468    }
469
470    /**
471     * Check if Gutenberg editor is available
472     *
473     * @since 6.7.0
474     *
475     * @return bool
476     */
477    public static function is_gutenberg_available() {
478        return true;
479    }
480
481    /**
482     * Check whether conditions indicate Gutenberg Extensions (blocks and plugins) should be loaded
483     *
484     * Loading blocks and plugins is enabled by default and may be disabled via filter:
485     *   add_filter( 'jetpack_gutenberg', '__return_false' );
486     *
487     * @since 6.9.0
488     *
489     * @return bool
490     */
491    public static function should_load() {
492        if ( ! Jetpack::is_connection_ready() && ! ( new Status() )->is_offline_mode() ) {
493            return false;
494        }
495
496        $return = true;
497
498        if ( ! ( new Modules() )->is_active( 'blocks' ) ) {
499            $return = false;
500        }
501
502        /**
503         * Filter to enable Gutenberg blocks.
504         *
505         * Defaults to true if (connected or in offline mode) and the Blocks module is active.
506         *
507         * @since 6.5.0
508         * @since 13.9 Filter is able to activate or deactivate Gutenberg blocks.
509         *
510         * @param bool true Whether to load Gutenberg blocks
511         */
512        return (bool) apply_filters( 'jetpack_gutenberg', $return );
513    }
514
515    /**
516     * Queue a script to set `Jetpack_Block_Assets_Base_Url`.
517     *
518     * In certain cases Webpack needs to know a base to load additional assets from.
519     * Normally it can determine that itself, but when JS concatenation is involved that tends to confuse it.
520     * We work around that by explicitly outputting a variable with the correct URL.
521     * We set that as its own "script" so we can reliably only output it once.
522     */
523    private static function register_blocks_assets_base_url() {
524        if ( ! wp_script_is( 'jetpack-blocks-assets-base-url', 'registered' ) ) {
525            // phpcs:ignore WordPress.WP.EnqueuedResourceParameters.MissingVersion -- No actual script, so no version needed.
526            wp_register_script( 'jetpack-blocks-assets-base-url', false, array(), null, array( 'in_footer' => false ) );
527            $json_encode_flags = JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP;
528            if ( get_option( 'blog_charset' ) === 'UTF-8' ) {
529                $json_encode_flags |= JSON_UNESCAPED_UNICODE;
530            }
531            wp_add_inline_script(
532                'jetpack-blocks-assets-base-url',
533                'var Jetpack_Block_Assets_Base_Url=' . wp_json_encode( plugins_url( self::get_blocks_directory(), JETPACK__PLUGIN_FILE ), $json_encode_flags ) . ';',
534                'before'
535            );
536        }
537    }
538
539    /**
540     * Only enqueue block assets when needed.
541     *
542     * @param string $type Slug of the block or absolute path to the block source code directory.
543     * @param array  $script_dependencies Script dependencies. Will be merged with automatically
544     *                                    detected script dependencies from the webpack build.
545     *
546     * @return void
547     */
548    public static function load_assets_as_required( $type, $script_dependencies = array() ) {
549        if ( is_admin() ) {
550            // A block's view assets will not be required in wp-admin.
551            return;
552        }
553
554        // Retrieve the feature from block.json if a path is passed.
555        if ( path_is_absolute( $type ) ) {
556            $metadata = Blocks::get_block_metadata_from_file( Blocks::get_path_to_block_metadata( $type ) );
557            $feature  = Blocks::get_block_feature_from_metadata( $metadata );
558
559            if ( ! empty( $feature ) ) {
560                $type = $feature;
561            }
562        }
563
564        $type = sanitize_title_with_dashes( $type );
565        self::load_styles_as_required( $type );
566        self::load_scripts_as_required( $type, $script_dependencies );
567    }
568
569    /**
570     * Only enqueue block sytles when needed.
571     *
572     * @param string $type Slug of the block.
573     *
574     * @since 7.2.0
575     *
576     * @return void
577     */
578    public static function load_styles_as_required( $type ) {
579        if ( is_admin() ) {
580            // A block's view assets will not be required in wp-admin.
581            return;
582        }
583
584        // Enqueue styles.
585        $style_relative_path = self::get_blocks_directory() . $type . '/view' . ( is_rtl() ? '.rtl' : '' ) . '.css';
586        if ( self::block_has_asset( $style_relative_path ) ) {
587            $style_version = self::get_asset_version( $style_relative_path );
588            $view_style    = plugins_url( $style_relative_path, JETPACK__PLUGIN_FILE );
589            $view_style    = add_query_arg( 'minify', 'false', $view_style );
590
591            // If this is a customizer preview, render the style directly to the preview after autosave.
592            // phpcs:ignore WordPress.Security.NonceVerification.Recommended
593            if ( is_customize_preview() && ! empty( $_GET['customize_autosaved'] ) ) {
594                // phpcs:ignore WordPress.WP.EnqueuedResources.NonEnqueuedStylesheet
595                echo '<link rel="stylesheet" id="jetpack-block-' . esc_attr( $type ) . '" href="' . esc_attr( $view_style ) . '&amp;ver=' . esc_attr( $style_version ) . '" media="all">';
596            } else {
597                wp_enqueue_style( 'jetpack-block-' . $type, $view_style, array(), $style_version );
598                wp_style_add_data( 'jetpack-block-' . $type, 'path', JETPACK__PLUGIN_DIR . $style_relative_path );
599            }
600        }
601    }
602
603    /**
604     * Only enqueue block scripts when needed.
605     *
606     * @param string $type Slug of the block.
607     * @param array  $script_dependencies Script dependencies. Will be merged with automatically
608     *                             detected script dependencies from the webpack build.
609     *
610     * @since 7.2.0
611     *
612     * @return void
613     */
614    public static function load_scripts_as_required( $type, $script_dependencies = array() ) {
615        if ( is_admin() ) {
616            // A block's view assets will not be required in wp-admin.
617            return;
618        }
619
620        self::register_blocks_assets_base_url();
621
622        // Enqueue script.
623        $script_relative_path  = self::get_blocks_directory() . $type . '/view.js';
624        $script_deps_path      = JETPACK__PLUGIN_DIR . self::get_blocks_directory() . $type . '/view.asset.php';
625        $script_dependencies[] = 'jetpack-blocks-assets-base-url';
626        if ( file_exists( $script_deps_path ) ) {
627            $asset_manifest      = include $script_deps_path;
628            $script_dependencies = array_unique( array_merge( $script_dependencies, $asset_manifest['dependencies'] ) );
629        }
630
631        if ( ! Blocks::is_amp_request() && self::block_has_asset( $script_relative_path ) ) {
632            $script_version = self::get_asset_version( $script_relative_path );
633            $view_script    = plugins_url( $script_relative_path, JETPACK__PLUGIN_FILE );
634            $view_script    = add_query_arg( 'minify', 'false', $view_script );
635            $strategy       = self::get_block_js_loading_strategy( $type );
636
637            // Enqueue dependencies.
638            wp_enqueue_script( 'jetpack-block-' . $type, $view_script, $script_dependencies, $script_version, $strategy );
639
640            // If this is a customizer preview, enqueue the dependencies and render the script directly to the preview after autosave.
641            // phpcs:ignore WordPress.Security.NonceVerification.Recommended
642            if ( is_customize_preview() && ! empty( $_GET['customize_autosaved'] ) ) {
643                // The Map block is dependent on wp-element, and it doesn't appear to to be possible to load
644                // this dynamically into the customizer iframe currently.
645                if ( 'map' === $type ) {
646                    echo '<div>' . esc_html__( 'No map preview available. Publish and refresh to see this widget.', 'jetpack' ) . '</div>';
647                    echo '<script>';
648                    echo 'Array.from(document.getElementsByClassName(\'wp-block-jetpack-map\')).forEach(function(element){element.style.display = \'none\';})';
649                    echo '</script>';
650                } else {
651                    echo '<script id="jetpack-block-' . esc_attr( $type ) . '" src="' . esc_attr( $view_script ) . '&amp;ver=' . esc_attr( $script_version ) . '"></script>';
652                }
653            }
654        }
655    }
656
657    /**
658     * Check if an asset exists for a block.
659     *
660     * @param string $file Path of the file we are looking for.
661     *
662     * @return bool $block_has_asset Does the file exist.
663     */
664    public static function block_has_asset( $file ) {
665        return file_exists( JETPACK__PLUGIN_DIR . $file );
666    }
667
668    /**
669     * Get the version number to use when loading the file. Allows us to bypass cache when developing.
670     *
671     * @param string $file Path of the file we are looking for.
672     *
673     * @return string $script_version Version number.
674     */
675    public static function get_asset_version( $file ) {
676        return Jetpack::is_development_version() && self::block_has_asset( $file )
677            ? filemtime( JETPACK__PLUGIN_DIR . $file )
678            : JETPACK__VERSION;
679    }
680
681    /**
682     * Load Gutenberg editor assets
683     *
684     * @since 6.7.0
685     *
686     * @return void
687     */
688    public static function enqueue_block_editor_assets() {
689        if ( ! self::should_load() ) {
690            return;
691        }
692
693        $status = new Status();
694
695        // Required for Analytics. See _inc/lib/admin-pages/class.jetpack-admin-page.php.
696        if ( ! $status->is_offline_mode() && Jetpack::is_connection_ready() ) {
697            wp_enqueue_script( 'jp-tracks', '//stats.wp.com/w.js', array(), gmdate( 'YW' ), true );
698        }
699
700        $blocks_dir       = self::get_blocks_directory();
701        $blocks_variation = self::blocks_variation();
702
703        if ( 'production' !== $blocks_variation ) {
704            $blocks_env = '-' . esc_attr( $blocks_variation );
705        } else {
706            $blocks_env = '';
707        }
708
709        self::register_blocks_assets_base_url();
710
711        Assets::register_script(
712            'jetpack-blocks-editor',
713            "{$blocks_dir}editor{$blocks_env}.js",
714            JETPACK__PLUGIN_FILE,
715            array(
716                'textdomain'   => 'jetpack',
717                'dependencies' => array( 'jetpack-blocks-assets-base-url' ),
718            )
719        );
720
721        /**
722         * This can be called multiple times per page load in the admin, during the `enqueue_block_assets` action.
723         * These assets are necessary for the admin for editing but are not necessary for each pattern preview.
724         * Therefore we dequeue them, so they don't load for each pattern preview iframe.
725         */
726        if ( ! wp_should_load_block_editor_scripts_and_styles() ) {
727            wp_dequeue_script( 'jp-tracks' );
728            wp_dequeue_script( 'jetpack-blocks-editor' );
729
730            return;
731        }
732
733        // Hack around #20357 (specifically, that the editor bundle depends on
734        // wp-edit-post but wp-edit-post's styles break the Widget Editor and
735        // Site Editor) until a real fix gets unblocked.
736        // @todo Remove this once #20357 is properly fixed.
737        $wp_styles_fix = wp_styles()->query( 'jetpack-blocks-editor', 'registered' );
738        if ( empty( $wp_styles_fix ) ) {
739            wp_die( 'Your installation of Jetpack is incomplete. Please run "jetpack build plugins/jetpack" in your dev env.' );
740        }
741        wp_styles()->query( 'jetpack-blocks-editor', 'registered' )->deps = array();
742
743        Assets::enqueue_script( 'jetpack-blocks-editor' );
744
745        if ( defined( 'IS_WPCOM' ) && IS_WPCOM ) {
746            $user                      = wp_get_current_user();
747            $user_data                 = array(
748                'email'    => $user->user_email,
749                'userid'   => $user->ID,
750                'username' => $user->user_login,
751            );
752            $blog_id                   = get_current_blog_id();
753            $is_current_user_connected = true;
754        } else {
755            $user_data                 = Jetpack_Tracks_Client::get_connected_user_tracks_identity();
756            $blog_id                   = Jetpack_Options::get_option( 'id', 0 );
757            $is_current_user_connected = ( new Connection_Manager( 'jetpack' ) )->is_user_connected();
758        }
759
760        if ( $blocks_variation === 'beta' && $is_current_user_connected ) {
761            wp_enqueue_style( 'recoleta-font', '//s1.wp.com/i/fonts/recoleta/css/400.min.css', array(), Constants::get_constant( 'JETPACK__VERSION' ) );
762        }
763        // AI Assistant
764        $ai_assistant_state = array(
765            'is-enabled' => apply_filters( 'jetpack_ai_enabled', true ),
766        );
767
768        $screen_base = null;
769        if ( function_exists( 'get_current_screen' ) ) {
770            $current_screen = get_current_screen();
771            $screen_base    = $current_screen ? $current_screen->base : null;
772        }
773
774        $modules = array();
775        if ( class_exists( 'Jetpack_Core_API_Module_List_Endpoint' ) ) {
776            $module_list_endpoint = new Jetpack_Core_API_Module_List_Endpoint();
777            $modules              = $module_list_endpoint->get_modules();
778        }
779
780        $jetpack_plan  = Jetpack_Plan::get();
781        $initial_state = array(
782            'available_blocks' => self::get_availability(),
783            'blocks_variation' => $blocks_variation,
784            'modules'          => $modules,
785            'jetpack'          => array(
786                'is_active'                     => Jetpack::is_connection_ready(),
787                'is_current_user_connected'     => $is_current_user_connected,
788                /** This filter is documented in class.jetpack-gutenberg.php */
789                'enable_upgrade_nudge'          => apply_filters( 'jetpack_block_editor_enable_upgrade_nudge', false ),
790                'is_private_site'               => $status->is_private_site(),
791                'is_coming_soon'                => $status->is_coming_soon(),
792                'is_offline_mode'               => $status->is_offline_mode(),
793                'is_newsletter_feature_enabled' => class_exists( '\Jetpack_Memberships' ),
794                // this is the equivalent of JP initial state siteData.showMyJetpack (class-jetpack-redux-state-helper)
795                // used to determine if we can link to My Jetpack from the block editor
796                'is_my_jetpack_available'       => My_Jetpack_Initializer::should_initialize(),
797                'jetpack_plan'                  => array(
798                    'data' => $jetpack_plan['product_slug'],
799                ),
800                /**
801                 * Enable the RePublicize UI in the block editor context.
802                 *
803                 * @module publicize
804                 *
805                 * @since 10.3.0
806                 * @deprecated 11.5 This is a feature flag that is no longer used.
807                 *
808                 * @param bool true Enable the RePublicize UI in the block editor context. Defaults to true.
809                 */
810                'republicize_enabled'           => apply_filters( 'jetpack_block_editor_republicize_feature', true ),
811            ),
812            'siteFragment'     => $status->get_site_suffix(),
813            'adminUrl'         => esc_url( admin_url() ),
814            'tracksUserData'   => $user_data,
815            'wpcomBlogId'      => $blog_id,
816            'allowedMimeTypes' => wp_get_mime_types(),
817            'siteLocale'       => str_replace( '_', '-', get_locale() ),
818            'ai-assistant'     => $ai_assistant_state,
819            'screenBase'       => $screen_base,
820            /**
821             * Add your own feature flags to the block editor.
822             *
823             * You can access the feature flags in the block editor via hasFeatureFlag( 'your-feature-flag' ) function.
824             *
825             * @since 14.8
826             *
827             * @param array true Enable the RePublicize UI in the block editor context. Defaults to true.
828             */
829            'feature_flags'    => apply_filters( 'jetpack_block_editor_feature_flags', array() ),
830            'pluginBasePath'   => plugins_url( '', Constants::get_constant( 'JETPACK__PLUGIN_FILE' ) ),
831        );
832
833        wp_localize_script(
834            'jetpack-blocks-editor',
835            'Jetpack_Editor_Initial_State',
836            $initial_state
837        );
838
839        // Adds Connection package initial state.
840        Connection_Initial_State::render_script( 'jetpack-blocks-editor' );
841    }
842
843    /**
844     * Some blocks do not depend on a specific module,
845     * and can consequently be loaded outside of the usual modules.
846     * We will look for such modules in the extensions/ directory.
847     *
848     * @since 7.1.0
849     * @see wp_common_block_scripts_and_styles()
850     */
851    public static function load_independent_blocks() {
852        if ( self::should_load() ) {
853            /**
854             * Look for files that match our list of available Jetpack Gutenberg extensions (blocks and plugins).
855             * If available, load them.
856             */
857            $directories = array( 'blocks', 'plugins', 'extended-blocks' );
858
859            foreach ( static::get_extensions() as $extension ) {
860                foreach ( $directories as $dirname ) {
861                    $path = JETPACK__PLUGIN_DIR . "extensions/{$dirname}/{$extension}/{$extension}.php";
862
863                    if ( file_exists( $path ) ) {
864                        include_once $path;
865                        continue 2;
866                    }
867                }
868            }
869        }
870    }
871
872    /**
873     * Determine whether the current request is a block-editor context that needs the
874     * editor-oriented extension files loaded.
875     *
876     * The `extensions/plugins/*` and `extensions/extended-blocks/*` files are, for the
877     * most part, only relevant to the editor (availability/plan gating, editor panels,
878     * REST fields, post type support). On plain front-end requests they would otherwise
879     * be included for nothing, inflating the per-request PHP/opcache footprint. A small
880     * allow-list of extensions that genuinely have front-end side effects keeps loading
881     * unconditionally (see self::$frontend_editor_extensions).
882     *
883     * @since 16.0
884     *
885     * @return bool False only for plain front-end web requests. Admin, REST, cron,
886     *              WP-CLI and XML-RPC contexts all return true so editor extensions load.
887     */
888    private static function is_block_editor_context() {
889        if ( is_admin() ) {
890            return true;
891        }
892
893        /*
894         * Load the editor extensions for any non-front-end execution context. These
895         * are not the front-end hot path this gate optimizes, and some of them still
896         * render block content (e.g. cron-generated subscription emails), which can
897         * depend on an editor extension's registrations.
898         */
899        if (
900            ( defined( 'DOING_CRON' ) && DOING_CRON )
901            || ( defined( 'WP_CLI' ) && WP_CLI )
902            || ( defined( 'XMLRPC_REQUEST' ) && XMLRPC_REQUEST )
903        ) {
904            return true;
905        }
906
907        /*
908         * This runs at module-load time (around plugins_loaded), before core
909         * defines REST_REQUEST during parse_request, so REST requests can't be
910         * detected via that constant here. Detect them from the request URL
911         * instead: both the rewritten `/wp-json/` form and the plain-permalink
912         * `?rest_route=` form.
913         */
914        $request_uri = isset( $_SERVER['REQUEST_URI'] ) ? sanitize_text_field( wp_unslash( $_SERVER['REQUEST_URI'] ) ) : '';
915        if ( '' === $request_uri ) {
916            return false;
917        }
918
919        /*
920         * Anchor the REST root (home path + prefix) at the start of the request
921         * path, so a front-end URL that merely carries the prefix in a query value
922         * (e.g. ?redirect=/wp-json/...) or as a deeper path segment (e.g.
923         * /docs/wp-json/...) is not misread as a REST request. home_url() is used
924         * rather than rest_url() so detection does not depend on permalink structure.
925         */
926        $path = (string) wp_parse_url( $request_uri, PHP_URL_PATH );
927        if ( '' !== $path ) {
928            $rest_root = trailingslashit( (string) wp_parse_url( home_url(), PHP_URL_PATH ) ) . trailingslashit( rest_get_url_prefix() );
929            if ( str_starts_with( trailingslashit( $path ), $rest_root ) ) {
930                return true;
931            }
932        }
933
934        // Plain-permalink REST uses a `rest_route` query var; match the exact key
935        // rather than a substring (which would also match e.g. ?not_rest_route=).
936        $query = (string) wp_parse_url( $request_uri, PHP_URL_QUERY );
937        if ( '' !== $query ) {
938            parse_str( $query, $query_vars );
939            if ( ! empty( $query_vars['rest_route'] ) ) {
940                return true;
941            }
942        }
943
944        return false;
945    }
946
947    /**
948     * Editor-oriented extensions that nonetheless have front-end side effects and must
949     * therefore keep loading on every request, even outside the block editor.
950     *
951     * Keyed by directory ('plugins' / 'extended-blocks') for an exact, intentional match.
952     *
953     * @since 16.0
954     *
955     * @var array
956     */
957    private static $frontend_editor_extensions = array(
958        'plugins'         => array(
959            // Mounts the Reader Chat widget on the front end (wp_enqueue_scripts) and
960            // wires the AI sidebar/provider registration AI Assistant depends on.
961            'ai-assistant-plugin',
962            // Signals Big Sky via the jetpack_image_studio_enabled filter on `init`,
963            // which can run on the front end.
964            'image-studio',
965            // Filters get_avatar_data on the front end to customize AI-authored note avatars.
966            'block-notes',
967        ),
968        'extended-blocks' => array(
969            // Registers the videopress/video block on `init`, required to render it on the front end.
970            'videopress-video',
971            // Registers the `premium-content/container` plan availability that the Premium Content
972            // block's front-end render reads via required_plan_checks(); skipping it breaks the paywall.
973            'premium-content-container',
974        ),
975    );
976
977    /**
978     * Loads PHP components of block editor extensions.
979     *
980     * @since 8.9.0
981     */
982    public static function load_block_editor_extensions() {
983        if ( self::should_load() ) {
984            // Block editor extensions to load.
985            $extensions_to_load = array(
986                'extended-blocks',
987                'plugins',
988            );
989
990            $is_editor_context = self::is_block_editor_context();
991
992            // Collect the extension paths.
993            foreach ( $extensions_to_load as $extension_to_load ) {
994                $extensions_folder = glob( JETPACK__PLUGIN_DIR . 'extensions/' . $extension_to_load . '/*' );
995
996                $frontend_allow_list = self::$frontend_editor_extensions[ $extension_to_load ] ?? array();
997
998                // Require each of the extension files, in case it exists.
999                foreach ( $extensions_folder as $extension_folder ) {
1000                    $name = basename( $extension_folder );
1001
1002                    /*
1003                     * On plain front-end requests, only load extensions that have known
1004                     * front-end side effects. Editor-only extensions are skipped here and
1005                     * loaded on admin/REST (block-editor) requests instead, reducing the
1006                     * per-front-end-request PHP/opcache footprint.
1007                     */
1008                    if ( ! $is_editor_context && ! in_array( $name, $frontend_allow_list, true ) ) {
1009                        continue;
1010                    }
1011
1012                    $extension_file_path = JETPACK__PLUGIN_DIR . 'extensions/' . $extension_to_load . '/' . $name . '/' . $name . '.php';
1013
1014                    if ( file_exists( $extension_file_path ) ) {
1015                        include_once $extension_file_path;
1016                    }
1017                }
1018            }
1019        }
1020    }
1021
1022    /**
1023     * Determine whether a site should use the default set of blocks, or a custom set.
1024     * Possible variations are currently beta, experimental, and production.
1025     *
1026     * @since 8.1.0
1027     *
1028     * @return string $block_varation production|beta|experimental
1029     */
1030    public static function blocks_variation() {
1031        // Default to production blocks.
1032        $block_varation = 'production';
1033
1034        /*
1035         * Prefer to use this JETPACK_BLOCKS_VARIATION constant
1036         * or the jetpack_blocks_variation filter
1037         * to set the block variation in your code.
1038         */
1039        $default = Constants::get_constant( 'JETPACK_BLOCKS_VARIATION' );
1040        if ( ! empty( $default ) && in_array( $default, array( 'beta', 'experimental', 'production' ), true ) ) {
1041            $block_varation = $default;
1042        }
1043
1044        /**
1045        * Alternative to `JETPACK_BETA_BLOCKS`, set to `true` to load Beta Blocks.
1046        *
1047        * @since 6.9.0
1048        * @deprecated 11.8.0 Use jetpack_blocks_variation filter instead.
1049        *
1050        * @param boolean
1051        */
1052        $is_beta = apply_filters_deprecated(
1053            'jetpack_load_beta_blocks',
1054            array( false ),
1055            'jetpack-11.8.0',
1056            'jetpack_blocks_variation'
1057        );
1058
1059        /*
1060         * Switch to beta blocks if you use the JETPACK_BETA_BLOCKS constant
1061         * or the deprecated jetpack_load_beta_blocks filter.
1062         * This only applies when not using the newer JETPACK_BLOCKS_VARIATION constant.
1063         */
1064        if (
1065            empty( $default )
1066            && (
1067                $is_beta
1068                || Constants::is_true( 'JETPACK_BETA_BLOCKS' )
1069            )
1070        ) {
1071            $block_varation = 'beta';
1072        }
1073
1074        /**
1075        * Alternative to `JETPACK_EXPERIMENTAL_BLOCKS`, set to `true` to load Experimental Blocks.
1076        *
1077        * @since 6.9.0
1078        * @deprecated 11.8.0 Use jetpack_blocks_variation filter instead.
1079        *
1080        * @param boolean
1081        */
1082        $is_experimental = apply_filters_deprecated(
1083            'jetpack_load_experimental_blocks',
1084            array( false ),
1085            'jetpack-11.8.0',
1086            'jetpack_blocks_variation'
1087        );
1088
1089        /*
1090         * Switch to experimental blocks if you use the JETPACK_EXPERIMENTAL_BLOCKS constant
1091         * or the deprecated jetpack_load_experimental_blocks filter.
1092         * This only applies when not using the newer JETPACK_BLOCKS_VARIATION constant.
1093         */
1094        if (
1095            empty( $default )
1096            && (
1097                $is_experimental
1098                || Constants::is_true( 'JETPACK_EXPERIMENTAL_BLOCKS' )
1099            )
1100        ) {
1101            $block_varation = 'experimental';
1102        }
1103
1104        /**
1105         * Allow customizing the variation of blocks in use on a site.
1106         * Overwrites any previously set values, whether by constant or filter.
1107         *
1108         * @since 8.1.0
1109         *
1110         * @param string $block_variation Can be beta, experimental, and production. Defaults to production.
1111         */
1112        return apply_filters( 'jetpack_blocks_variation', $block_varation );
1113    }
1114
1115    /**
1116     * Get a list of extensions available for the variation you chose.
1117     *
1118     * @since 8.1.0
1119     *
1120     * @param object $preset_extensions_manifest List of extensions available in Jetpack.
1121     * @param string $blocks_variation           Subset of blocks. production|beta|experimental.
1122     *
1123     * @return array $preset_extensions Array of extensions for that variation
1124     */
1125    public static function get_extensions_preset_for_variation( $preset_extensions_manifest, $blocks_variation ) {
1126        $preset_extensions = isset( $preset_extensions_manifest->{ $blocks_variation } )
1127                ? (array) $preset_extensions_manifest->{ $blocks_variation }
1128                : array();
1129
1130        /*
1131         * Experimental and Beta blocks need the production blocks as well.
1132         */
1133        if (
1134            'experimental' === $blocks_variation
1135            || 'beta' === $blocks_variation
1136        ) {
1137            $production_extensions = isset( $preset_extensions_manifest->production )
1138                ? (array) $preset_extensions_manifest->production
1139                : array();
1140
1141            $preset_extensions = array_unique( array_merge( $preset_extensions, $production_extensions ) );
1142        }
1143
1144        /*
1145         * Beta blocks need the experimental blocks as well.
1146         *
1147         * If you've chosen to see Beta blocks,
1148         * we want to make all blocks available to you:
1149         * - Production
1150         * - Experimental
1151         * - Beta
1152         */
1153        if ( 'beta' === $blocks_variation ) {
1154            $production_extensions = isset( $preset_extensions_manifest->experimental )
1155                ? (array) $preset_extensions_manifest->experimental
1156                : array();
1157
1158            $preset_extensions = array_unique( array_merge( $preset_extensions, $production_extensions ) );
1159        }
1160
1161        return $preset_extensions;
1162    }
1163
1164    /**
1165     * Validate a URL used in a SSR block.
1166     *
1167     * @since 8.3.0
1168     *
1169     * @param string $url      URL saved as an attribute in block.
1170     * @param array  $allowed  Array of allowed hosts for that block, or regexes to check against.
1171     * @param bool   $is_regex Array of regexes matching the URL that could be used in block.
1172     *
1173     * @return bool|string
1174     */
1175    public static function validate_block_embed_url( $url, $allowed = array(), $is_regex = false ) {
1176        if (
1177            empty( $url )
1178            || ! is_array( $allowed )
1179            || empty( $allowed )
1180        ) {
1181            return false;
1182        }
1183
1184        $url_components = wp_parse_url( $url );
1185
1186        // Bail early if we cannot find a host.
1187        if ( empty( $url_components['host'] ) ) {
1188            return false;
1189        }
1190
1191        // Normalize URL.
1192        $url = sprintf(
1193            '%s://%s%s%s',
1194            $url_components['scheme'] ?? 'https',
1195            $url_components['host'],
1196            $url_components['path'] ?? '/',
1197            isset( $url_components['query'] ) ? '?' . $url_components['query'] : ''
1198        );
1199
1200        if ( ! empty( $url_components['fragment'] ) ) {
1201            $url = $url . '#' . rawurlencode( $url_components['fragment'] );
1202        }
1203
1204        /*
1205         * If we're using an allowed list of hosts,
1206         * check if the URL belongs to one of the domains allowed for that block.
1207         */
1208        if (
1209            false === $is_regex
1210            && in_array( $url_components['host'], $allowed, true )
1211        ) {
1212            return $url;
1213        }
1214
1215        /*
1216         * If we are using an array of regexes to check against,
1217         * loop through that.
1218         */
1219        if ( true === $is_regex ) {
1220            foreach ( $allowed as $regex ) {
1221                if ( 1 === preg_match( $regex, $url ) ) {
1222                    return $url;
1223                }
1224            }
1225        }
1226
1227        return false;
1228    }
1229
1230    /**
1231     * Determines whether a preview of the block with an upgrade nudge should
1232     * be displayed for admins on the site frontend.
1233     *
1234     * @since 8.4.0
1235     *
1236     * @param array $availability_for_block The availability for the block.
1237     *
1238     * @return bool
1239     */
1240    public static function should_show_frontend_preview( $availability_for_block ) {
1241        return (
1242            isset( $availability_for_block['details']['required_plan'] )
1243            && current_user_can( 'manage_options' )
1244            && ! is_feed()
1245        );
1246    }
1247
1248    /**
1249     * Output an UpgradeNudge Component on the frontend of a site.
1250     *
1251     * @since 8.4.0
1252     *
1253     * @param string $plan The plan that users need to purchase to make the block work.
1254     *
1255     * @return string
1256     */
1257    public static function upgrade_nudge( $plan ) {
1258        require_once JETPACK__PLUGIN_DIR . '_inc/lib/components.php';
1259        return Jetpack_Components::render_upgrade_nudge(
1260            array(
1261                'plan' => $plan,
1262            )
1263        );
1264    }
1265
1266    /**
1267     * Output a notice within a block.
1268     *
1269     * @since 8.6.0
1270     *
1271     * @param string $message Notice we want to output.
1272     * @param string $status  Status of the notice. Can be one of success, info, warning, error. info by default.
1273     * @param string $classes List of CSS classes.
1274     *
1275     * @return string
1276     */
1277    public static function notice( $message, $status = 'info', $classes = '' ) {
1278        if (
1279            empty( $message )
1280            || ! in_array( $status, array( 'success', 'info', 'warning', 'error' ), true )
1281        ) {
1282            return '';
1283        }
1284
1285        $color = '';
1286        switch ( $status ) {
1287            case 'success':
1288                $color = '#00a32a';
1289                break;
1290            case 'warning':
1291                $color = '#dba617';
1292                break;
1293            case 'error':
1294                $color = '#d63638';
1295                break;
1296            case 'info':
1297            default:
1298                $color = '#72aee6';
1299                break;
1300        }
1301
1302        return sprintf(
1303            '<div class="jetpack-block__notice %1$s %3$s" style="border-left:5px solid %4$s;padding:1em;background-color:#f8f9f9;">%2$s</div>',
1304            esc_attr( $status ),
1305            wp_kses(
1306                $message,
1307                array(
1308                    'br' => array(),
1309                    'p'  => array(),
1310                    'a'  => array(
1311                        'href'   => array(),
1312                        'target' => array(),
1313                        'rel'    => array(),
1314                    ),
1315                )
1316            ),
1317            esc_attr( $classes ),
1318            sanitize_hex_color( $color )
1319        );
1320    }
1321
1322    /**
1323     * Retrieve site-specific features for Simple sites.
1324     *
1325     * We're caching the data for the lifetime of the request, because it can be slow to calculate,
1326     * and it can be called multiple times per single request.
1327     *
1328     * We intentionally don't use object caching or any other type of persistent caching,
1329     * in order to avoid complex cache invalidation on subscription addition or removal.
1330     *
1331     * @since 10.7
1332     *
1333     * @return array
1334     */
1335    private static function get_site_specific_features() {
1336        $current_blog_id = get_current_blog_id();
1337
1338        if ( isset( self::$site_specific_features[ $current_blog_id ] ) ) {
1339            return self::$site_specific_features[ $current_blog_id ];
1340        }
1341
1342        if ( ! class_exists( 'Store_Product_List' ) ) {
1343            require WP_CONTENT_DIR . '/admin-plugins/wpcom-billing/store-product-list.php';
1344        }
1345
1346        $site_specific_features                           = Store_Product_List::get_site_specific_features_data( $current_blog_id );
1347        self::$site_specific_features[ $current_blog_id ] = $site_specific_features;
1348
1349        return $site_specific_features;
1350    }
1351
1352    /**
1353     * Set the availability of the block as the editor
1354     * is loaded.
1355     *
1356     * @param string $slug Slug of the block.
1357     */
1358    public static function set_availability_for_plan( $slug ) {
1359        $slug = self::remove_extension_prefix( $slug );
1360
1361        if ( Jetpack_Plan::supports( $slug ) ) {
1362            self::set_extension_available( $slug );
1363            return;
1364        }
1365
1366        // Check what's the minimum plan where the feature is available.
1367        $plan           = '';
1368        $features_data  = array();
1369        $is_simple_site = defined( 'IS_WPCOM' ) && IS_WPCOM;
1370        $is_atomic_site = ( new Host() )->is_woa_site();
1371
1372        if ( $is_simple_site || $is_atomic_site ) {
1373            // Simple sites.
1374            if ( $is_simple_site ) {
1375                $features_data = self::get_site_specific_features();
1376            } else {
1377                // Atomic sites.
1378                $option = get_option( 'jetpack_active_plan' );
1379                if ( isset( $option['features'] ) ) {
1380                    $features_data = $option['features'];
1381                }
1382            }
1383
1384            if ( ! empty( $features_data['available'][ $slug ] ) ) {
1385                $plan = $features_data['available'][ $slug ][0];
1386            } elseif ( isset( self::$wpcom_minimum_plan_fallbacks[ $slug ] ) ) {
1387                // Fallback for features with conditional availability (e.g., sticker-based gating)
1388                // that don't appear in features_data['available'].
1389                $plan = self::$wpcom_minimum_plan_fallbacks[ $slug ];
1390            }
1391        } else {
1392            // Jetpack sites.
1393            $plan = Jetpack_Plan::get_minimum_plan_for_feature( $slug );
1394        }
1395
1396        self::set_extension_unavailable(
1397            $slug,
1398            'missing_plan',
1399            array(
1400                'required_feature' => $slug,
1401                'required_plan'    => $plan,
1402            )
1403        );
1404    }
1405
1406    /**
1407     * Wraps the suplied render_callback in a function to check
1408     * the availability of the block before rendering it.
1409     *
1410     * @param string   $slug The block slug, used to check for availability.
1411     * @param callable $render_callback The render_callback that will be called if the block is available.
1412     */
1413    public static function get_render_callback_with_availability_check( $slug, $render_callback ) {
1414        return function ( $prepared_attributes, $block_content, $block ) use ( $render_callback, $slug ) {
1415            $availability = self::get_cached_availability();
1416            $bare_slug    = self::remove_extension_prefix( $slug );
1417            if ( isset( $availability[ $bare_slug ] ) && $availability[ $bare_slug ]['available'] ) {
1418                return call_user_func( $render_callback, $prepared_attributes, $block_content, $block );
1419            }
1420
1421            // A preview of the block is rendered for admins on the frontend with an upgrade nudge.
1422            if ( isset( $availability[ $bare_slug ] ) ) {
1423                if ( self::should_show_frontend_preview( $availability[ $bare_slug ] ) ) {
1424                    $block_preview = call_user_func( $render_callback, $prepared_attributes, $block_content, $block );
1425
1426                    // If the upgrade nudge isn't already being displayed by a parent block, display the nudge.
1427                    if ( isset( $block->attributes['shouldDisplayFrontendBanner'] ) && $block->attributes['shouldDisplayFrontendBanner'] ) {
1428                        $upgrade_nudge = self::upgrade_nudge( $availability[ $bare_slug ]['details']['required_plan'] );
1429                        return $upgrade_nudge . $block_preview;
1430                    }
1431
1432                    return $block_preview;
1433                }
1434            }
1435
1436            return null;
1437        };
1438    }
1439
1440    /**
1441     * Display a message to site editors and roles above when a block is no longer supported.
1442     * This is only displayed on the frontend.
1443     *
1444     * @since 12.3
1445     *
1446     * @param string $block_content The block content.
1447     * @param array  $block         The full block, including name and attributes.
1448     *
1449     * @return string
1450     */
1451    public static function display_deprecated_block_message( $block_content, $block ) {
1452        if ( isset( $block['blockName'] ) && in_array( $block['blockName'], self::$deprecated_blocks, true ) ) {
1453            if ( current_user_can( 'edit_posts' ) ) {
1454                $block_content = self::notice(
1455                    __( 'This block is no longer supported. Its contents will no longer be displayed to your visitors and as such this block should be removed.', 'jetpack' ),
1456                    'warning',
1457                    'jetpack-block-deprecated'
1458                );
1459            } else {
1460                $block_content = '';
1461            }
1462        }
1463
1464        return $block_content;
1465    }
1466
1467    /**
1468     * Register block metadata collection for Jetpack blocks.
1469     * This allows for more efficient block metadata loading by avoiding
1470     * individual block.json file reads at runtime.
1471     *
1472     * Uses wp_register_block_metadata_collection() if the manifest file
1473     * exists. The manifest file is auto-generated during the build process.
1474     *
1475     * Runs on plugins_loaded to ensure registration happens before individual
1476     * blocks register themselves on init.
1477     *
1478     * @static
1479     * @since 14.1
1480     * @return void
1481     */
1482    public static function register_block_metadata_collection() {
1483        $meta_file_path = JETPACK__PLUGIN_DIR . '_inc/blocks/blocks-manifest.php';
1484        if ( file_exists( $meta_file_path ) ) {
1485            wp_register_block_metadata_collection(
1486                JETPACK__PLUGIN_DIR . '_inc/blocks/',
1487                $meta_file_path
1488            );
1489        }
1490    }
1491
1492    /**
1493     * Set the JS loading strategy for a block.
1494     *
1495     * @param string     $block_name The block name.
1496     * @param array|bool $strategy   The JS loading strategy.
1497     *
1498     * @since 15.0
1499     */
1500    public static function set_block_js_loading_strategy( $block_name, $strategy ) {
1501        self::$block_js_loading_strategies[ $block_name ] = $strategy;
1502    }
1503
1504    /**
1505     * Get the JS loading strategy for a block.
1506     *
1507     * @param string $block_name The block name.
1508     *
1509     * @return array|bool The JS loading strategy for the block.
1510     *
1511     * @since 15.0
1512     */
1513    public static function get_block_js_loading_strategy( $block_name ) {
1514        $strategy = array(
1515            'strategy'  => 'defer',
1516            'in_footer' => true,
1517        );
1518
1519        if ( isset( self::$block_js_loading_strategies[ $block_name ] ) ) {
1520            $strategy = self::$block_js_loading_strategies[ $block_name ];
1521        }
1522
1523        return $strategy;
1524    }
1525}
1526
1527if ( ( new Host() )->is_woa_site() ) {
1528    /**
1529    * Enable upgrade nudge for Atomic sites.
1530     * This feature is false as default,
1531     * so let's enable it through this filter.
1532     *
1533     * More doc: https://github.com/Automattic/jetpack/blob/trunk/projects/plugins/jetpack/extensions/README.md#upgrades-for-blocks
1534     */
1535    add_filter( 'jetpack_block_editor_enable_upgrade_nudge', '__return_true' );
1536}