Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
83.13% covered (warning)
83.13%
138 / 166
64.71% covered (warning)
64.71%
11 / 17
CRAP
0.00% covered (danger)
0.00%
0 / 1
Blocks
83.13% covered (warning)
83.13%
138 / 166
64.71% covered (warning)
64.71%
11 / 17
98.57
0.00% covered (danger)
0.00%
0 / 1
 jetpack_register_block
92.59% covered (success)
92.59%
50 / 54
0.00% covered (danger)
0.00%
0 / 1
17.12
 get_block_metadata
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
6
 get_block_metadata_from_file
75.00% covered (warning)
75.00%
6 / 8
0.00% covered (danger)
0.00%
0 / 1
4.25
 get_block_name
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get_block_name_from_path_convention
60.00% covered (warning)
60.00%
6 / 10
0.00% covered (danger)
0.00%
0 / 1
3.58
 get_block_name_from_metadata
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
3
 get_block_feature
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get_block_feature_from_metadata
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 is_registered
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 remove_extension_prefix
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 is_gutenberg_version_available
36.00% covered (danger)
36.00%
9 / 25
0.00% covered (danger)
0.00%
0 / 1
30.23
 classes
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
7.02
 is_amp_request
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 is_fse_theme
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 is_standalone_block
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 get_path_to_block_metadata
88.89% covered (warning)
88.89%
8 / 9
0.00% covered (danger)
0.00%
0 / 1
4.02
 get_variation
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
9
1<?php
2/** Blocks package.
3 *
4 * @since 1.1.0
5 *
6 * This package lifts elements from Jetpack's Jetpack_Gutenberg class.
7 * It is now an standalone package reusable outside Jetpack.
8 *
9 * @package automattic/jetpack-blocks
10 */
11
12namespace Automattic\Jetpack;
13
14use Automattic\Jetpack\Constants as Jetpack_Constants;
15use Jetpack_Gutenberg;
16
17/**
18 * Register and manage blocks within a plugin. Used to manage block registration, enqueues, and more.
19 *
20 * @since 1.1.0
21 */
22class Blocks {
23    /**
24     * Wrapper function to safely register a Gutenberg block type
25     *
26     * @see register_block_type
27     * @see Automattic\Jetpack\Blocks::is_gutenberg_version_available
28     *
29     * @since 1.1.0
30     *
31     * @param string $slug Slug of the block or absolute path to the block source code directory.
32     * @param array  $args {
33     *     Arguments that are passed into register_block_type.
34     *     See register_block_type for full list of arguments.
35     *     Can also include 2 extra arguments not currently supported by register_block_type.
36     *
37     *     @type array $version_requirements Array containing required Gutenberg version and, if known, the WordPress version that was released with this minimum version.
38     *     @type bool  $plan_check           Should we check for a specific plan before registering the block.
39     * }
40     *
41     * @return \WP_Block_Type|false The registered block type on success, or false on failure.
42     */
43    public static function jetpack_register_block( $slug, $args = array() ) {
44        // Slug doesn't start with `jetpack/`, isn't an absolute path, or doesn't contain a slash
45        // (synonym of a namespace) at all.
46        if ( ! str_starts_with( $slug, 'jetpack/' ) && ! path_is_absolute( $slug ) && ! strpos( $slug, '/' ) ) {
47            _doing_it_wrong( 'jetpack_register_block', 'Prefix the block with jetpack/ ', 'Jetpack 9.0.0' );
48            $slug = 'jetpack/' . $slug;
49        }
50
51        $block_type = $slug;
52
53        // If a path is passed, make sure to get the block.json file from the build directory and get
54        // the block name from that file.
55        if ( path_is_absolute( $slug ) ) {
56            $block_type = self::get_path_to_block_metadata( $slug );
57            $slug       = self::get_block_name_from_path_convention( $slug );
58        }
59
60        if (
61            isset( $args['version_requirements'] )
62            && ! self::is_gutenberg_version_available( $args['version_requirements'], $slug )
63        ) {
64            return false;
65        }
66
67        // Checking whether block is registered to ensure it isn't registered twice.
68        if ( self::is_registered( $slug ) ) {
69            return false;
70        }
71
72        $feature_name = self::remove_extension_prefix( $slug );
73
74        // This is only useful in Jetpack.
75        if ( ! self::is_standalone_block() ) {
76            // If the block is dynamic, and a Jetpack block, wrap the render_callback to check availability.
77            if ( ! empty( $args['plan_check'] ) ) {
78                $existing_attributes = array();
79                $gated_blocks        = array(
80                    'jetpack/calendly',
81                    'jetpack/donations',
82                    'jetpack/payment-buttons',
83                    'jetpack/paypal-payment-buttons',
84                );
85                if ( in_array( $slug, $gated_blocks, true ) &&
86                    is_string( $block_type ) &&
87                    file_exists( $block_type )
88                ) {
89                    $metadata            = self::get_block_metadata( $block_type );
90                    $existing_attributes = $metadata['attributes'] ?? array();
91                }
92
93                // Set up attributes.
94                if ( ! isset( $args['attributes'] ) ) {
95                    $args['attributes'] = array();
96                }
97                $args['attributes'] = array_merge(
98                    $existing_attributes,
99                    $args['attributes'],
100                    array(
101                        // Indicates that this block should display an upgrade nudge on the frontend when applicable.
102                        'shouldDisplayFrontendBanner' => array(
103                            'type'    => 'boolean',
104                            'default' => true,
105                        ),
106                    )
107                );
108                if ( isset( $args['render_callback'] ) ) {
109                    $args['render_callback'] = Jetpack_Gutenberg::get_render_callback_with_availability_check( $feature_name, $args['render_callback'] );
110                }
111                $method_name = 'set_availability_for_plan';
112            } else {
113                $method_name = 'set_extension_available';
114            }
115
116            add_action(
117                'jetpack_register_gutenberg_extensions',
118                function () use ( $feature_name, $method_name ) {
119                    call_user_func( array( 'Jetpack_Gutenberg', $method_name ), $feature_name );
120                }
121            );
122
123            // Ensure editor styles are registered so that the site editor knows about the
124            // editor style dependency when copying styles to the editor iframe.
125            if ( ! isset( $args['editor_style'] ) ) {
126                $args['editor_style'] = 'jetpack-blocks-editor';
127            }
128
129            // Keep track of the JS loading strategy for any block that specifies it.
130            if ( isset( $args['js_loading_strategy'] ) ) {
131                Jetpack_Gutenberg::set_block_js_loading_strategy( $feature_name, $args['js_loading_strategy'] );
132            }
133        }
134
135        return register_block_type( $block_type, $args );
136    }
137
138    /**
139     * Get the block metadata. Accepts a block.json file's path (or its folder's) or its content, in
140     * which case it becomes an identity function.
141     *
142     * It's used by other helpers in this class so that they can accept various types as argument.
143     *
144     * @param string|array $arg Path to block.json or its parent folder, or its content as an array.
145     *
146     * @return array The block metadata.
147     */
148    private static function get_block_metadata( $arg ) {
149        $metadata = is_array( $arg ) ? $arg : null;
150
151        if ( ! isset( $metadata ) ) {
152            $path = is_string( $arg ) ? $arg : null;
153
154            if ( isset( $path ) && ! empty( $path ) ) {
155                $metadata = self::get_block_metadata_from_file( self::get_path_to_block_metadata( $path ) );
156            }
157        }
158
159        return $metadata ?? array();
160    }
161
162    /**
163     * Read block metadata from a block.json file.
164     *
165     * @param string $filename The path to the block.json file or its directory.
166     *
167     * @return array The block metadata.
168     */
169    public static function get_block_metadata_from_file( $filename ) {
170        $metadata = array();
171        $needle   = '/block.json';
172        $filename = $needle === substr( $filename, -strlen( $needle ) ) ? $filename : $filename . $needle;
173
174        if ( file_exists( $filename ) ) {
175            try {
176                $metadata = wp_json_file_decode( $filename, array( 'associative' => true ) );
177            } catch ( \Exception $e ) {
178                $metadata = array();
179            }
180        }
181
182        return $metadata;
183    }
184
185    /**
186     * Get the block name (includes the `jetpack` prefix).
187     *
188     * @param string|array $arg Path to block.json or its parent folder, or its content as an array.
189     *
190     * @return string The block name.
191     */
192    public static function get_block_name( $arg ) {
193        $metadata = self::get_block_metadata( $arg );
194
195        return self::get_block_name_from_metadata( $metadata );
196    }
197
198    /**
199     * Get the block name from the path convention.
200     * For example, path "./extensions/blocks/pinterest" is assumed to define
201     * a block named "jetpack/pinterest" without checking any files.
202     *
203     * Any exceptions should be added to the $breaks_convention array.
204     * For example, blocks/premium-content defines "premium-content\/container", not "jetpack/premium-content".
205     * These paths will use the code that checks the disk for the block name.
206     *
207     * The unit test test_get_block_name_from_path_convention_matches_get_block_name() verifies that
208     * all names are correctly guessed.
209     *
210     * Run with `./vendor/bin/phpunit --filter WP_Test_Jetpack_Gutenberg`.
211     *
212     * @param string $path The path to extract the block name from.
213     *
214     * @return string The block name with 'jetpack/' prefix.
215     */
216    public static function get_block_name_from_path_convention( $path ) {
217        $path_parts = explode( '/', $path );
218        if ( count( $path_parts ) <= 0 ) {
219            $block_type = self::get_path_to_block_metadata( $path );
220            return self::get_block_name( $block_type );
221        }
222
223        $last_part         = $path_parts[ count( $path_parts ) - 1 ];
224        $breaks_convention = array( 'premium-content' );
225
226        if ( in_array( $last_part, $breaks_convention, true ) ) {
227            $block_type = self::get_path_to_block_metadata( $path );
228            return self::get_block_name( $block_type );
229        }
230
231        return 'jetpack/' . $last_part;
232    }
233
234    /**
235     * Get the block name from the its metadata.
236     *
237     * @param array $metadata The block metadata.
238     *
239     * @return string The block name.
240     */
241    public static function get_block_name_from_metadata( $metadata ) {
242        return ! isset( $metadata['name'] ) || empty( $metadata['name'] ) ? '' : $metadata['name'];
243    }
244
245    /**
246     * Get the block feature name (i.e. the name without the `jetpack` prefix).
247     *
248     * @param string|array $arg Path to block.json or its parent folder, or its content as an array.
249     *
250     * @return string The block feature name.
251     */
252    public static function get_block_feature( $arg ) {
253        $metadata = self::get_block_metadata( $arg );
254
255        return self::get_block_feature_from_metadata( $metadata );
256    }
257
258    /**
259     * Get the block feature name (i.e. the name without the `jetpack` prefix) from its metadata.
260     *
261     * @param array $metadata The block metadata.
262     *
263     * @return string The block feature name.
264     */
265    public static function get_block_feature_from_metadata( $metadata ) {
266        return str_replace( 'jetpack/', '', self::get_block_name_from_metadata( $metadata ) );
267    }
268
269    /**
270     * Check if an extension/block is already registered
271     *
272     * @since 1.1.0
273     *
274     * @param string $slug Name of extension/block to check.
275     *
276     * @return bool
277     */
278    public static function is_registered( $slug ) {
279        return \WP_Block_Type_Registry::get_instance()->is_registered( $slug );
280    }
281
282    /**
283     * Remove the 'jetpack/' or jetpack-' prefix from an extension name
284     *
285     * @since 1.1.0
286     *
287     * @param string $extension_name The extension name.
288     *
289     * @return string The unprefixed extension name.
290     */
291    public static function remove_extension_prefix( $extension_name ) {
292        if ( str_starts_with( $extension_name, 'jetpack/' ) || str_starts_with( $extension_name, 'jetpack-' ) ) {
293            return substr( $extension_name, strlen( 'jetpack/' ) );
294        }
295        return $extension_name;
296    }
297
298    /**
299     * Check to see if a minimum version of Gutenberg is available. Because a Gutenberg version is not available in
300     * php if the Gutenberg plugin is not installed, if we know which minimum WP release has the required version we can
301     * optionally fall back to that.
302     *
303     * @since 1.1.0
304     *
305     * @param array  $version_requirements {
306     *     An array containing the required Gutenberg version and, if known, the WordPress version that was released with this minimum version.
307     *
308     *     @type string $gutenberg Gutenberg version.
309     *     @type string $wp        Optional. WordPress version.
310     * }
311     * @param string $slug The slug of the block or plugin that has the Gutenberg version requirement.
312     *
313     * @return boolean True if the version of Gutenberg required by the block or plugin is available.
314     */
315    public static function is_gutenberg_version_available( $version_requirements, $slug ) {
316        global $wp_version;
317
318        // Bail if we don't at least have the Gutenberg version requirement, the WP version is optional.
319        if ( empty( $version_requirements['gutenberg'] ) ) {
320            return false;
321        }
322
323        // If running a local dev build of Gutenberg plugin GUTENBERG_DEVELOPMENT_MODE is set so assume correct version.
324        if ( defined( 'GUTENBERG_DEVELOPMENT_MODE' ) && GUTENBERG_DEVELOPMENT_MODE ) {
325            return true;
326        }
327
328        $version_available = false;
329
330        // If running a production build of the Gutenberg plugin then GUTENBERG_VERSION is set, otherwise if WP version
331        // with required version of Gutenberg is known check that.
332        if ( defined( 'GUTENBERG_VERSION' ) ) {
333            $version_available = version_compare( GUTENBERG_VERSION, $version_requirements['gutenberg'], '>=' );
334        } elseif ( ! empty( $version_requirements['wp'] ) ) {
335            $version_available = version_compare( $wp_version, $version_requirements['wp'], '>=' );
336        }
337
338        if (
339            ! $version_available
340            && ! self::is_standalone_block() // This is only useful in Jetpack.
341        ) {
342            $slug = Jetpack_Gutenberg::remove_extension_prefix( $slug );
343            Jetpack_Gutenberg::set_extension_unavailable(
344                $slug,
345                'incorrect_gutenberg_version',
346                array(
347                    'required_feature' => $slug,
348                    'required_version' => $version_requirements,
349                    'current_version'  => array(
350                        'wp'        => $wp_version,
351                        'gutenberg' => defined( 'GUTENBERG_VERSION' ) ? GUTENBERG_VERSION : null,
352                    ),
353                )
354            );
355        }
356
357        return $version_available;
358    }
359
360    /**
361     * Get CSS classes for a block.
362     *
363     * @since 1.1.0
364     *
365     * @param string $slug  Block slug.
366     * @param array  $attr  Block attributes.
367     * @param array  $extra Potential extra classes you may want to provide.
368     *
369     * @return string $classes List of CSS classes for a block.
370     */
371    public static function classes( $slug, $attr, $extra = array() ) {
372        if ( empty( $slug ) ) {
373            return '';
374        }
375
376        // Basic block name class.
377        $classes = array(
378            'wp-block-jetpack-' . $slug,
379        );
380
381        // Add alignment if provided.
382        if (
383            ! empty( $attr['align'] )
384            && in_array( $attr['align'], array( 'left', 'center', 'right', 'wide', 'full' ), true )
385        ) {
386            $classes[] = 'align' . $attr['align'];
387        }
388
389        // Add custom classes if provided in the block editor.
390        if ( ! empty( $attr['className'] ) ) {
391            $classes[] = $attr['className'];
392        }
393
394        // Add any extra classes.
395        if ( is_array( $extra ) && ! empty( $extra ) ) {
396            $classes = array_merge( $classes, array_filter( $extra ) );
397        }
398
399        return implode( ' ', $classes );
400    }
401
402    /**
403     * Does the page return AMP content.
404     *
405     * @since 1.1.0
406     *
407     * @return bool $is_amp_request Are we on an AMP view.
408     */
409    public static function is_amp_request() {
410        $is_amp_request = ( function_exists( 'is_amp_endpoint' ) && is_amp_endpoint() );
411
412        /** This filter is documented in 3rd-party/class.jetpack-amp-support.php */
413        return apply_filters( 'jetpack_is_amp_request', $is_amp_request );
414    }
415
416    /**
417     * Is the current theme an FSE/Site Editor theme.
418     *
419     * @since 1.4.0
420     * @since 1.4.22 Remove support for deprecated `gutenberg_is_fse_theme` function.
421     *
422     * @return bool True if the current theme is an FSE/Site Editor theme.
423     */
424    public static function is_fse_theme() {
425        $is_fse_theme = wp_is_block_theme();
426
427        /**
428         * Returns true if the current theme is an FSE/Site Editor theme.
429         *
430         * @since 1.4.0
431         *
432         * @param boolean $is_fse_theme Is the theme an FSE theme.
433         */
434        return apply_filters( 'jetpack_is_fse_theme', $is_fse_theme );
435    }
436
437    /**
438     * Check whether or the block being registered is a standalone block,
439     * running in a context outside of the Jetpack plugin.
440     *
441     * @since 1.3.0
442     *
443     * @return bool
444     */
445    public static function is_standalone_block() {
446        $is_standalone_block = ! class_exists( Jetpack_Gutenberg::class );
447
448        /**
449         * Returns true if the block is not being registered within a Jetpack plugin context.
450         *
451         * @since 1.3.0
452         *
453         * @param boolean $is_standalone_block Is the block running standalone versus as part of the Jetpack plugin.
454         */
455        return apply_filters( 'jetpack_is_standalone_block', $is_standalone_block );
456    }
457
458    /**
459     * Returns the path to the directory containing the block.json metadata file of a block, given its
460     * source code directory and, optionally, the directory that holds the blocks built files of
461     * the package. If the directory doesn't exist, falls back to the source directory.
462     *
463     * @since 1.6.0
464     *
465     * @param string $block_src_dir    The path to the folder containing the block source code.
466     *                                 Typically this is done by passing __DIR__ as the argument.
467     * @param string $package_dist_dir Optional. A full path to the directory containing the blocks
468     *                                 built files of the package. Default empty.
469     *
470     * @return string The path to the directory.
471     */
472    public static function get_path_to_block_metadata( $block_src_dir, $package_dist_dir = '' ) {
473        $dir       = basename( $block_src_dir );
474        $dist_path = $package_dist_dir;
475
476        if ( empty( $dist_path ) ) {
477            $plugin_file = Jetpack_Constants::get_constant( 'JETPACK__PLUGIN_FILE' );
478            // Guard against strange situations where JETPACK__PLUGIN_FILE is undefined.
479            if ( empty( $plugin_file ) ) {
480                return $block_src_dir;
481            }
482
483            $dist_path = dirname( $plugin_file ) . '/_inc/blocks';
484        }
485
486        $result = realpath( "$dist_path/$dir" );
487
488        return false === $result ? $block_src_dir : $result;
489    }
490
491    /**
492     * Determine whether a site should use the default set of blocks, or a custom set.
493     * Possible variations are currently beta, experimental, and production.
494     *
495     * @since 3.1.0
496     *
497     * @return string $block_variation production|beta|experimental
498     */
499    public static function get_variation() {
500        // Default to production blocks.
501        $block_variation = 'production';
502
503        /*
504         * Prefer to use this JETPACK_BLOCKS_VARIATION constant
505         * or the jetpack_blocks_variation filter
506         * to set the block variation in your code.
507         */
508        $default = Constants::get_constant( 'JETPACK_BLOCKS_VARIATION' );
509        if ( ! empty( $default ) && in_array( $default, array( 'beta', 'experimental', 'production' ), true ) ) {
510            $block_variation = $default;
511        }
512
513        /**
514        * Alternative to `JETPACK_BETA_BLOCKS`, set to `true` to load Beta Blocks.
515        *
516        * @since jetpack-6.9.0
517        * @deprecated jetpack-11.8.0 Use jetpack_blocks_variation filter instead.
518        *
519        * @param boolean
520        */
521        $is_beta = apply_filters_deprecated(
522            'jetpack_load_beta_blocks',
523            array( false ),
524            'jetpack-11.8.0',
525            'jetpack_blocks_variation'
526        );
527
528        /*
529         * Switch to beta blocks if you use the JETPACK_BETA_BLOCKS constant
530         * or the deprecated jetpack_load_beta_blocks filter.
531         * This only applies when not using the newer JETPACK_BLOCKS_VARIATION constant.
532         */
533        if ( empty( $default )
534                && (
535                    $is_beta
536                    || Constants::is_true( 'JETPACK_BETA_BLOCKS' )
537                )
538            ) {
539            $block_variation = 'beta';
540        }
541
542        /**
543        * Alternative to `JETPACK_EXPERIMENTAL_BLOCKS`, set to `true` to load Experimental Blocks.
544        *
545        * @since jetpack-6.9.0
546        * @deprecated jetpack-11.8.0 Use jetpack_blocks_variation filter instead.
547        *
548        * @param boolean
549        */
550        $is_experimental = apply_filters_deprecated(
551            'jetpack_load_experimental_blocks',
552            array( false ),
553            'jetpack-11.8.0',
554            'jetpack_blocks_variation'
555        );
556
557        /*
558         * Switch to experimental blocks if you use the JETPACK_EXPERIMENTAL_BLOCKS constant
559         * or the deprecated jetpack_load_experimental_blocks filter.
560         * This only applies when not using the newer JETPACK_BLOCKS_VARIATION constant.
561         */
562        if ( empty( $default )
563            && (
564                $is_experimental
565                || Constants::is_true( 'JETPACK_EXPERIMENTAL_BLOCKS' )
566            )
567            ) {
568            $block_variation = 'experimental';
569        }
570
571        /**
572         * Allow customizing the variation of blocks in use on a site.
573         * Overwrites any previously set values, whether by constant or filter.
574         *
575         * @since jetpack-8.1.0
576         *
577         * @param string $block_variation Can be beta, experimental, and production. Defaults to production.
578         */
579        return apply_filters( 'jetpack_blocks_variation', $block_variation );
580    }
581}