Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
90.95% covered (success)
90.95%
673 / 740
54.17% covered (warning)
54.17%
13 / 24
CRAP
0.00% covered (danger)
0.00%
0 / 1
Contact_Form_Block
90.95% covered (success)
90.95%
673 / 740
54.17% covered (warning)
54.17%
13 / 24
85.87
0.00% covered (danger)
0.00%
0 / 1
 register_block
98.18% covered (success)
98.18%
54 / 55
0.00% covered (danger)
0.00%
0 / 1
2
 disable_field_visibility_support
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
7
 drop_field_hidden_everywhere
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
3
 register_feature
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
1
 register_central_form_management_default
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 find_nested_html_block
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
5
 render_wrapped_html_block
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 register_child_blocks
99.80% covered (success)
99.80%
490 / 491
0.00% covered (danger)
0.00%
0 / 1
2
 set_file_field_extension_available
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 pre_render_contact_form
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
6
 count_form_steps_in_block
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
4
 get_form_step_count
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 render_fallback
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
1
 render_email
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 submit_button_element
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 gutenblock_render_form
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
 render_form
81.82% covered (warning)
81.82%
9 / 11
0.00% covered (danger)
0.00%
0 / 1
9.49
 apply_background_support
96.97% covered (success)
96.97%
32 / 33
0.00% covered (danger)
0.00%
0 / 1
12
 load_editor_styles
n/a
0 / 0
n/a
0 / 0
1
 load_editor_scripts
0.00% covered (danger)
0.00%
0 / 34
0.00% covered (danger)
0.00%
0 / 1
6
 maybe_load_ai_integration
0.00% covered (danger)
0.00%
0 / 20
0.00% covered (danger)
0.00%
0 / 1
30
 preload_endpoints
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 load_view_scripts
92.31% covered (success)
92.31%
12 / 13
0.00% covered (danger)
0.00%
0 / 1
2.00
 maybe_register_blocks_editor_script
94.74% covered (success)
94.74%
18 / 19
0.00% covered (danger)
0.00%
0 / 1
4.00
 can_manage_block
80.00% covered (warning)
80.00%
4 / 5
0.00% covered (danger)
0.00%
0 / 1
4.13
1<?php
2/**
3 * Contact Form Block.
4 *
5 * @package automattic/jetpack
6 */
7
8namespace Automattic\Jetpack\Extensions\Contact_Form;
9
10use Automattic\Jetpack\Assets;
11use Automattic\Jetpack\Assets\Shared_Stores_Assets;
12use Automattic\Jetpack\Blocks;
13use Automattic\Jetpack\Current_Plan;
14use Automattic\Jetpack\Forms\ContactForm\Contact_Form;
15use Automattic\Jetpack\Forms\ContactForm\Contact_Form_Field;
16use Automattic\Jetpack\Forms\ContactForm\Contact_Form_Plugin;
17use Automattic\Jetpack\Forms\ContactForm\Form_Preview;
18use Automattic\Jetpack\Forms\Dashboard\Dashboard as Forms_Dashboard;
19use Automattic\Jetpack\Forms\Jetpack_Forms;
20use Automattic\Jetpack\Modules;
21use Automattic\Jetpack\Status\Request;
22use Jetpack;
23
24/**
25 * Contact Form block render callback.
26 */
27class Contact_Form_Block {
28    /**
29     * Class on the Form block's own element, the one carrying its style supports.
30     *
31     * @var string
32     */
33    const FORM_BLOCK_CLASS = 'wp-block-jetpack-contact-form';
34
35    /**
36     * Class on the Step block's own element, the one carrying its style supports.
37     *
38     * @var string
39     */
40    const STEP_BLOCK_CLASS = 'wp-block-jetpack-form-step';
41
42    /**
43     * Register the Contact Form block.
44     * We are core block dependent only on whether the jetpack contact form plugin
45     * is active or not. This is allowing us to make it more discoverable
46     * and enable the plugin in one click
47     */
48    public static function register_block() {
49        /*
50         * The block is available even when the module is not active,
51         * so we can display a nudge to activate the module instead of the block.
52         * However, since non-admins cannot activate modules, we do not display the empty block for them.
53         */
54        if ( ! self::can_manage_block() ) {
55            return;
56        }
57
58        Blocks::jetpack_register_block(
59            'jetpack/contact-form',
60            array(
61                'render_email_callback' => array( __CLASS__, 'render_email' ),
62                'render_callback'       => array( __CLASS__, 'gutenblock_render_form' ),
63                'supports'              => array(
64
65                    /*
66                     * Deliberately not mirrored in block.json: serialization is skipped here so
67                     * self::apply_background_support() can put the image on the block's own
68                     * element, and skipping it in JS too would drop the editor preview.
69                     */
70                    'background'           => array(
71                        'backgroundImage'                 => true,
72                        'backgroundSize'                  => true,
73                        '__experimentalSkipSerialization' => true,
74                        '__experimentalDefaultControls'   => array(
75                            'backgroundImage' => true,
76                        ),
77                    ),
78                    'layout'               => array(
79                        'default'                => array(
80                            'type'              => 'flex',
81                            'flexWrap'          => 'wrap',
82                            'orientation'       => 'horizontal',
83                            'justifyContent'    => 'left',
84                            'verticalAlignment' => 'top',
85                        ),
86                        'allowSwitching'         => false,
87                        'allowEditing'           => true,
88                        'allowOrientation'       => true,
89                        'allowVerticalAlignment' => true,
90                        'allowJustification'     => true,
91                        'allowWrap'              => false,
92                    ),
93                    '__experimentalBorder' => array(
94                        'color'                         => true,
95                        'radius'                        => true,
96                        'style'                         => true,
97                        'width'                         => true,
98                        '__experimentalDefaultControls' => array(
99                            'color'  => true,
100                            'radius' => true,
101                            'style'  => true,
102                            'width'  => true,
103                        ),
104                    ),
105                    'listView'             => true,
106                ),
107                'style_handles'         => array( 'jetpack-forms-layout' ),
108            )
109        );
110
111        add_filter( 'render_block_data', array( __CLASS__, 'find_nested_html_block' ), 10, 3 );
112        add_filter( 'render_block_core/html', array( __CLASS__, 'render_wrapped_html_block' ), 10, 2 );
113        add_filter( 'jetpack_block_editor_feature_flags', array( __CLASS__, 'register_feature' ) );
114        add_filter( 'pre_render_block', array( __CLASS__, 'pre_render_contact_form' ), 10, 3 );
115
116        add_filter( 'block_editor_rest_api_preload_paths', array( __CLASS__, 'preload_endpoints' ) );
117
118        // Load scripts for the editing interface
119        add_action( 'enqueue_block_editor_assets', array( __CLASS__, 'load_editor_scripts' ), 9 );
120
121        // Load AI integration after Jetpack_Gutenberg registers extensions (priority 10)
122        add_action( 'enqueue_block_editor_assets', array( __CLASS__, 'maybe_load_ai_integration' ), 11 );
123    }
124
125    /**
126     * Disable the block "visibility" support on form field and input blocks.
127     *
128     * FORMS-694 (interim). The per-viewport "Hide on…" option is not honored in
129     * the forms render pipeline â€” fields flatten to a shortcode and bypass core's
130     * render_block class injection â€” and on a required field it cannot be made
131     * safe (server- and client-side validation are both viewport-blind). "Hide
132     * everywhere" does work for fields, but the control bundles both modes under
133     * one boolean, so we disable it wholesale on fields, inputs, and choice/option
134     * blocks as an interim. This mirrors the JS registration. The label is the
135     * only block that keeps visibility support â€” it honors hiding (full-hide via
136     * labelhiddenbyblockvisibility, per-viewport via wp-block-hidden-* classes)
137     * and never affects validation or submission. Full field visibility is a
138     * separate decision.
139     *
140     * @param array  $args       Block type registration args.
141     * @param string $block_name Block name being registered.
142     * @return array
143     */
144    public static function disable_field_visibility_support( $args, $block_name ) {
145        // TODO: refactor into an array_merge'd shared supports array mirroring the JS defaultSettings, instead of this filter.
146        // Fields, incl. the deprecated field-option-* choice blocks.
147        $is_field = strpos( $block_name, 'jetpack/field-' ) === 0;
148        // All input variants: jetpack/input, input-range, input-rating,
149        // input-image-option, plus the differently-named phone-input and dropzone.
150        $is_input = strpos( $block_name, 'jetpack/input' ) === 0 || in_array( $block_name, array( 'jetpack/phone-input', 'jetpack/dropzone' ), true );
151        // Choice/option containers.
152        $is_option = in_array( $block_name, array( 'jetpack/option', 'jetpack/options', 'jetpack/fieldset-image-options' ), true );
153
154        if ( $is_input || $is_field || $is_option ) {
155            if ( ! isset( $args['supports'] ) || ! is_array( $args['supports'] ) ) {
156                $args['supports'] = array();
157            }
158            $args['supports']['visibility'] = false;
159        }
160
161        return $args;
162    }
163
164    /**
165     * Drop a field that has been hidden everywhere via block visibility.
166     *
167     * FORMS-694. "Hide everywhere" (metadata.blockVisibility === false) must keep
168     * working on fields even though we disable the visibility *support* on them
169     * (see disable_field_visibility_support). We can't rely on core's render_block
170     * visibility filter for this: on released WP it gates the full-hide branch
171     * behind the support check, so once support is false the field would reappear
172     * (the fix that reorders those â€” WordPress/gutenberg#78780 â€” is unreleased).
173     *
174     * Dropping the field's rendered output ('') before Contact_Form::parse() sees
175     * it removes the [contact-field] shortcode entirely, so the field is never
176     * parsed, rendered, or validated â€” which is what makes this required-safe (a
177     * required hidden-everywhere field simply isn't in the form). This is a full
178     * removal, not a CSS hide; the per-viewport "Hide on…" mode (which leaves the
179     * field in place and only display:none's it) is intentionally not honored on
180     * fields for exactly that reason.
181     *
182     * @param string $block_content Rendered block content.
183     * @param array  $block         Parsed block.
184     * @return string
185     */
186    public static function drop_field_hidden_everywhere( $block_content, $block ) {
187        $block_name       = $block['blockName'] ?? '';
188        $block_visibility = $block['attrs']['metadata']['blockVisibility'] ?? null;
189
190        if ( false === $block_visibility && strpos( $block_name, 'jetpack/field-' ) === 0 ) {
191            return '';
192        }
193
194        return $block_content;
195    }
196
197    /**
198     * Register the contact form block feature flag.
199     *
200     * @param array $features - the features array.
201     *
202     * @return array
203     */
204    public static function register_feature( $features ) {
205        // Features that are only available to users with a paid plan.
206        $features['multistep-form'] = Current_Plan::supports( 'multistep-form' );
207        $features['form-webhooks']  = Current_Plan::supports( 'form-webhooks' );
208
209        // Bridges the jetpack-feature-flags registration to the editor, so JS `hasFeatureFlag()`
210        // and PHP `Feature_Flags::is_enabled()` answer from one source under one name.
211        $features[ Jetpack_Forms::CONDITIONAL_LOGIC_FLAG ] = Jetpack_Forms::is_conditional_logic_enabled();
212
213        return self::register_central_form_management_default( $features );
214    }
215
216    /**
217     * Lightweight bootstrap default for the `central-form-management` feature flag.
218     *
219     * Registered from Util::init() so that early callers of
220     * Contact_Form_Plugin::has_editor_feature_flag() â€” such as the Forms dashboard
221     * default-tab redirect, which runs before the WP `init` hook fires â€” see the
222     * correct flag value. Kept separate from register_feature() so the early-boot
223     * code path avoids the Current_Plan::supports() calls used by the paid-plan flags.
224     *
225     * @param array $features - the features array.
226     *
227     * @return array
228     */
229    public static function register_central_form_management_default( $features ) {
230        if ( ! isset( $features['central-form-management'] ) ) {
231            $features['central-form-management'] = true;
232        }
233
234        return $features;
235    }
236
237    /**
238     *  Find nested html block that reside in the contact form block.
239     *  We are using this to wrap the html block with div if it is nested inside contact form block. So that the elements render as expected.
240     *
241     *  @param array  $parsed_block - the parsed block.
242     *  @param array  $source_block - the source block.
243     *  @param object $parent_block - the parent WP_Block.
244     *
245     *  @return array
246     */
247    public static function find_nested_html_block( $parsed_block, $source_block, $parent_block ) {
248        if ( ! empty( $parsed_block['blockName'] ) && $parsed_block['blockName'] === 'core/html' && isset( $parent_block->parsed_block ) && $parent_block->parsed_block['blockName'] === 'jetpack/contact-form' ) {
249            $parsed_block['hasJPFormParent'] = true;
250        }
251        return $parsed_block;
252    }
253
254    /**
255     * Render wrapped html block that is inside the form block with a wrapped div so that the elements render as expected.
256     * The extra div is needed because the form block has a `flex: 0 0 100%;` applied to all the children of the form block.
257     * This cases all the elementes inside the block to render in a single line and make it not possible to add have inline elements.
258     *
259     * @param string $content - the content of the block.
260     * @param array  $parsed_block - the parsed block.
261     *
262     * @return string
263     */
264    public static function render_wrapped_html_block( $content, $parsed_block ) {
265        if ( ! empty( $parsed_block['hasJPFormParent'] ) ) {
266            return '<div>' . $content . '</div>';
267        }
268
269        return $content;
270    }
271
272    /**
273     * Register the Child blocks of Contact Form
274     * We are registering child blocks only when Contact Form plugin is Active
275     */
276    public static function register_child_blocks() {
277        // Bail early if the user cannot manage the block.
278        if ( ! self::can_manage_block() ) {
279            return;
280        }
281
282        // Keep the PHP-registered "visibility" support in sync with the JS
283        // registration (src/blocks/shared/settings/index.js and
284        // src/blocks/input/index.js), which disables it on fields and inputs.
285        add_filter( 'register_block_type_args', array( __CLASS__, 'disable_field_visibility_support' ), 10, 2 );
286
287        // Honor "hide everywhere" on fields ourselves, independent of core's
288        // version-dependent visibility filter. See drop_field_hidden_everywhere().
289        add_filter( 'render_block', array( __CLASS__, 'drop_field_hidden_everywhere' ), 10, 2 );
290
291        // Field inner block types.
292        Blocks::jetpack_register_block(
293            'jetpack/input',
294            array(
295                'supports'     => array(
296                    '__experimentalBorder' => array(
297                        'color'  => true,
298                        'radius' => true,
299                        'style'  => true,
300                        'width'  => true,
301                    ),
302                    'color'                => array(
303                        'text'       => true,
304                        'background' => true,
305                        'gradients'  => false,
306                    ),
307                    'typography'           => array(
308                        'fontSize'                     => true,
309                        'lineHeight'                   => true,
310                        '__experimentalFontFamily'     => true,
311                        '__experimentalFontWeight'     => true,
312                        '__experimentalFontStyle'      => true,
313                        '__experimentalTextTransform'  => true,
314                        '__experimentalTextDecoration' => true,
315                        '__experimentalLetterSpacing'  => true,
316                    ),
317                ),
318                'selectors'    => array(
319                    'border' => '.wp-block-jetpack-input, .is-style-outlined .notched-label:has(+ .wp-block-jetpack-input) > *,.is-style-outlined .wp-block-jetpack-input + .notched-label > *, .is-style-outlined .wp-block-jetpack-field-select .notched-label > *',
320                    'color'  => '.wp-block-jetpack-input, .is-style-outlined .notched-label:has(+ .wp-block-jetpack-input) > *,.is-style-outlined .wp-block-jetpack-input + .notched-label > *, .is-style-outlined .wp-block-jetpack-field-select .notched-label > *',
321                ),
322                'uses_context' => array( 'jetpack/field-default-value' ),
323            )
324        );
325        Blocks::jetpack_register_block(
326            'jetpack/label',
327            array(
328                'supports'     => array(
329                    'color'      => array(
330                        'text'       => true,
331                        'background' => false,
332                        'gradients'  => false,
333                    ),
334                    'typography' => array(
335                        'fontSize'                     => true,
336                        'lineHeight'                   => true,
337                        '__experimentalFontFamily'     => true,
338                        '__experimentalFontWeight'     => true,
339                        '__experimentalFontStyle'      => true,
340                        '__experimentalTextTransform'  => true,
341                        '__experimentalTextDecoration' => true,
342                        '__experimentalLetterSpacing'  => true,
343                    ),
344                    // The real support key is `visibility`, not `blockVisibility`
345                    // (that's the per-instance saved attribute). The label is the
346                    // only forms block that keeps the control â€” see FORMS-694.
347                    'visibility' => true,
348                ),
349                'uses_context' => array(
350                    'jetpack/field-required',
351                ),
352            )
353        );
354        Blocks::jetpack_register_block(
355            'jetpack/options',
356            array(
357                'supports'         => array(
358                    '__experimentalBorder' => array(
359                        'color'  => true,
360                        'radius' => true,
361                        'style'  => true,
362                        'width'  => true,
363                    ),
364                    'color'                => array(
365                        'text'       => false,
366                        'background' => true,
367                    ),
368                    'spacing'              => array(
369                        'blockGap' => false,
370                    ),
371                ),
372                'provides_context' => array(
373                    'jetpack/field-options-type' => 'type',
374                ),
375                'selectors'        => array(
376                    'border' => '.wp-block-jetpack-options, .is-style-outlined .notched-label:has(+ .wp-block-jetpack-options) > *',
377                    'color'  => '.wp-block-jetpack-options, .is-style-outlined .notched-label:has(+ .wp-block-jetpack-options) > *',
378                ),
379            )
380        );
381        Blocks::jetpack_register_block(
382            'jetpack/option',
383            array(
384                'supports'     => array(
385                    'color'      => array(
386                        'text'       => true,
387                        'background' => false,
388                        'gradients'  => false,
389                    ),
390                    'typography' => array(
391                        'fontSize'                     => true,
392                        'lineHeight'                   => true,
393                        '__experimentalFontFamily'     => true,
394                        '__experimentalFontWeight'     => true,
395                        '__experimentalFontStyle'      => true,
396                        '__experimentalTextTransform'  => true,
397                        '__experimentalTextDecoration' => true,
398                        '__experimentalLetterSpacing'  => true,
399                    ),
400                ),
401                'uses_context' => array(
402                    'jetpack/field-default-value',
403                    'jetpack/field-options-type',
404                    'jetpack/field-required',
405                ),
406            )
407        );
408
409        Blocks::jetpack_register_block(
410            'jetpack/phone-input',
411            array(
412                'supports'     => array(
413                    '__experimentalBorder' => array(
414                        'color'  => true,
415                        'radius' => true,
416                        'style'  => true,
417                        'width'  => true,
418                    ),
419                    'color'                => array(
420                        'text'       => true,
421                        'background' => true,
422                        'gradients'  => false,
423                    ),
424                    'typography'           => array(
425                        'fontSize'                     => true,
426                        'lineHeight'                   => true,
427                        '__experimentalFontFamily'     => true,
428                        '__experimentalFontWeight'     => true,
429                        '__experimentalFontStyle'      => true,
430                        '__experimentalTextTransform'  => true,
431                        '__experimentalTextDecoration' => true,
432                        '__experimentalLetterSpacing'  => true,
433                    ),
434                ),
435                'uses_context' => array(
436                    'jetpack/field-share-attributes',
437                    'jetpack/field-prefix-options',
438                    'jetpack/field-prefix-default',
439                    'jetpack/field-prefix-onChange',
440                    'jetpack/field-phone-country-toggle',
441                ),
442            )
443        );
444
445        Blocks::jetpack_register_block(
446            'jetpack/input-rating',
447            array(
448                'supports' => array(
449                    'color'      => array(
450                        'text'       => true,
451                        'background' => false,
452                    ),
453                    'typography' => array(
454                        'fontSize' => true,
455                    ),
456                ),
457            )
458        );
459
460        Blocks::jetpack_register_block(
461            'jetpack/input-range',
462            array(
463                'supports' => array(
464                    'color'      => array(
465                        'text'       => true,
466                        'background' => false,
467                    ),
468                    'typography' => array(
469                        'fontSize'                     => true,
470                        '__experimentalFontFamily'     => true,
471                        '__experimentalFontWeight'     => true,
472                        '__experimentalFontStyle'      => true,
473                        '__experimentalTextTransform'  => true,
474                        '__experimentalTextDecoration' => true,
475                        '__experimentalLetterSpacing'  => true,
476                    ),
477                ),
478            )
479        );
480
481        // Field render methods.
482        Blocks::jetpack_register_block(
483            'jetpack/field-text',
484            array(
485                'render_callback'  => array( Contact_Form_Plugin::class, 'gutenblock_render_field_text' ),
486                'provides_context' => array( 'jetpack/field-required' => 'required' ),
487            )
488        );
489        Blocks::jetpack_register_block(
490            'jetpack/field-name',
491            array(
492                'render_callback'  => array( Contact_Form_Plugin::class, 'gutenblock_render_field_name' ),
493                'provides_context' => array( 'jetpack/field-required' => 'required' ),
494            )
495        );
496        Blocks::jetpack_register_block(
497            'jetpack/field-email',
498            array(
499                'render_callback'  => array( Contact_Form_Plugin::class, 'gutenblock_render_field_email' ),
500                'provides_context' => array( 'jetpack/field-required' => 'required' ),
501            )
502        );
503        Blocks::jetpack_register_block(
504            'jetpack/field-url',
505            array(
506                'render_callback'  => array( Contact_Form_Plugin::class, 'gutenblock_render_field_url' ),
507                'provides_context' => array( 'jetpack/field-required' => 'required' ),
508            )
509        );
510        Blocks::jetpack_register_block(
511            'jetpack/field-date',
512            array(
513                'render_callback'  => array( Contact_Form_Plugin::class, 'gutenblock_render_field_date' ),
514                'provides_context' => array( 'jetpack/field-required' => 'required' ),
515            )
516        );
517        Blocks::jetpack_register_block(
518            'jetpack/field-telephone',
519            array(
520                'render_callback'  => array( Contact_Form_Plugin::class, 'gutenblock_render_field_telephone' ),
521                'attributes'       => array(
522                    'showCountrySelector' => array(
523                        'type' => 'boolean',
524                    ),
525                    'default'             => array(
526                        'type' => 'string',
527                        'role' => 'content',
528                    ),
529                    'searchPlaceholder'   => array(
530                        'type' => 'string',
531                        'role' => 'content',
532                    ),
533                ),
534                'supports'         => array(
535                    'interactivity' => true,
536                ),
537                'provides_context' => array(
538                    'jetpack/field-share-attributes'     => 'shareAttributes',
539                    'jetpack/field-required'             => 'required',
540                    'jetpack/field-prefix-default'       => 'default',
541                    'jetpack/field-phone-country-toggle' => 'showCountrySelector',
542                    'jetpack/field-phone-search-placeholder' => 'searchPlaceholder',
543                ),
544            )
545        );
546        Blocks::jetpack_register_block(
547            'jetpack/field-textarea',
548            array(
549                'render_callback'  => array( Contact_Form_Plugin::class, 'gutenblock_render_field_textarea' ),
550                'provides_context' => array( 'jetpack/field-required' => 'required' ),
551            )
552        );
553        Blocks::jetpack_register_block(
554            'jetpack/field-checkbox',
555            array(
556                'render_callback'  => array( Contact_Form_Plugin::class, 'gutenblock_render_field_checkbox' ),
557                'provides_context' => array(
558                    'jetpack/field-required'      => 'required',
559                    'jetpack/field-default-value' => 'defaultValue',
560                ),
561            )
562        );
563        Blocks::jetpack_register_block(
564            'jetpack/field-checkbox-multiple',
565            array(
566                'render_callback' => array( Contact_Form_Plugin::class, 'gutenblock_render_field_checkbox_multiple' ),
567            )
568        );
569
570        Blocks::jetpack_register_block(
571            'jetpack/field-option-checkbox',
572            array(
573                'render_callback' => array( Contact_Form_Plugin::class, 'gutenblock_render_field_option' ),
574            )
575        );
576
577        Blocks::jetpack_register_block(
578            'jetpack/field-radio',
579            array(
580                'render_callback' => array( Contact_Form_Plugin::class, 'gutenblock_render_field_radio' ),
581            )
582        );
583
584        Blocks::jetpack_register_block(
585            'jetpack/field-option-radio',
586            array(
587                'render_callback' => array( Contact_Form_Plugin::class, 'gutenblock_render_field_option' ),
588            )
589        );
590        Blocks::jetpack_register_block(
591            'jetpack/field-select',
592            array(
593                'render_callback'  => array( Contact_Form_Plugin::class, 'gutenblock_render_field_select' ),
594                'provides_context' => array( 'jetpack/field-required' => 'required' ),
595            )
596        );
597        Blocks::jetpack_register_block(
598            'jetpack/field-consent',
599            array(
600                'render_callback' => array( Contact_Form_Plugin::class, 'gutenblock_render_field_consent' ),
601            )
602        );
603
604        Blocks::jetpack_register_block(
605            'jetpack/field-number',
606            array(
607                'render_callback'  => array( Contact_Form_Plugin::class, 'gutenblock_render_field_number' ),
608                'provides_context' => array( 'jetpack/field-required' => 'required' ),
609            )
610        );
611
612        Blocks::jetpack_register_block(
613            'jetpack/field-file',
614            array(
615                'render_callback'  => array( Contact_Form_Plugin::class, 'gutenblock_render_field_file' ),
616                'provides_context' => array( 'jetpack/field-required' => 'required' ),
617                'plan_check'       => apply_filters( 'jetpack_unauth_file_upload_plan_check', true ),
618            )
619        );
620
621        Blocks::jetpack_register_block(
622            'jetpack/dropzone',
623            array(
624                'render_callback' => array( Contact_Form_Plugin::class, 'gutenblock_render_dropzone' ),
625            )
626        );
627
628        Blocks::jetpack_register_block(
629            'jetpack/field-hidden',
630            array(
631                'render_callback' => array( Contact_Form_Plugin::class, 'gutenblock_render_field_hidden' ),
632            )
633        );
634
635        Blocks::jetpack_register_block(
636            'jetpack/field-rating',
637            array(
638                'render_callback'  => array( Contact_Form_Plugin::class, 'gutenblock_render_field_rating' ),
639                'provides_context' => array(
640                    'jetpack/field-required' => 'required',
641                ),
642            )
643        );
644
645        Blocks::jetpack_register_block(
646            'jetpack/field-slider',
647            array(
648                'render_callback'  => array( Contact_Form_Plugin::class, 'gutenblock_render_field_slider' ),
649                'provides_context' => array( 'jetpack/field-required' => 'required' ),
650            )
651        );
652
653        Blocks::jetpack_register_block(
654            'jetpack/field-time',
655            array(
656                'render_callback'  => array( Contact_Form_Plugin::class, 'gutenblock_render_field_time' ),
657                'provides_context' => array( 'jetpack/field-required' => 'required' ),
658            )
659        );
660
661        // Paid file field block
662        add_action(
663            'jetpack_register_gutenberg_extensions',
664            array( __CLASS__, 'set_file_field_extension_available' )
665        );
666
667        /**
668         * The blocks 'jetpack/field-checkbox-multiple' and 'jetpack/field-radio' are wrapper blocks.
669         * Styles must be registered so that they are available to be overridden by the theme or global styles.
670         * Form field blocks define the block style via the settings in their index.js files.
671         * A follow up issue is to update them to use block.json files, which can be reused
672         * in both JS and PHP block registration.
673         */
674        register_block_style(
675            array( 'jetpack/field-checkbox-multiple', 'jetpack/field-radio' ),
676            array(
677                'name'       => 'list',
678                'label'      => __( 'List', 'jetpack-forms' ),
679                'is_default' => true,
680            )
681        );
682
683        register_block_style(
684            array( 'jetpack/field-checkbox-multiple', 'jetpack/field-radio' ),
685            array(
686                'name'  => 'button',
687                'label' => __( 'Button', 'jetpack-forms' ),
688            )
689        );
690
691        Blocks::jetpack_register_block(
692            'jetpack/form-step',
693            array(
694                'render_callback' => array( Contact_Form_Plugin::class, 'gutenblock_render_form_step' ),
695
696                /*
697                 * Deliberately not mirrored in the block's index.js: serialization is skipped
698                 * here so Contact_Form_Block::apply_background_support() can put the image on
699                 * the block's own element, and skipping it in JS too would drop the editor
700                 * preview.
701                 */
702                'supports'        => array(
703                    'background' => array(
704                        'backgroundImage'                 => true,
705                        'backgroundSize'                  => true,
706                        '__experimentalSkipSerialization' => true,
707                        '__experimentalDefaultControls'   => array(
708                            'backgroundImage' => true,
709                        ),
710                    ),
711                ),
712            )
713        );
714
715        Blocks::jetpack_register_block(
716            'jetpack/form-step-navigation',
717            array(
718                'render_callback' => array( Contact_Form_Plugin::class, 'gutenblock_render_form_step_navigation' ),
719            )
720        );
721
722        Blocks::jetpack_register_block(
723            'jetpack/form-progress-indicator',
724            array(
725                'render_callback' => array( Contact_Form_Plugin::class, 'gutenblock_render_form_progress_indicator' ),
726            )
727        );
728
729        Blocks::jetpack_register_block(
730            'jetpack/form-step-container'
731        );
732
733        Blocks::jetpack_register_block(
734            'jetpack/field-image-select',
735            array(
736                'render_callback'  => array( Contact_Form_Plugin::class, 'gutenblock_render_field_image_select' ),
737                'provides_context' => array(
738                    'jetpack/field-required' => 'required',
739                    'jetpack/field-image-select-show-labels' => 'showLabels',
740                    'jetpack/field-image-select-is-supersized' => 'isSupersized',
741                    'jetpack/field-image-select-is-multiple' => 'isMultiple',
742                    'jetpack/field-image-select-randomize-options' => 'randomizeOptions',
743                    'jetpack/field-image-select-show-other-option' => 'showOtherOption',
744                ),
745            )
746        );
747
748        Blocks::jetpack_register_block(
749            'jetpack/fieldset-image-options',
750            array(
751                'uses_context'     => array(
752                    'jetpack/field-image-select-is-supersized',
753                    'jetpack/field-image-select-is-multiple',
754                    'jetpack/field-share-attributes',
755                ),
756                'provides_context' => array(
757                    'jetpack/field-image-options-type' => 'type',
758                ),
759            )
760        );
761
762        Blocks::jetpack_register_block(
763            'jetpack/input-image-option',
764            array(
765                'supports'         => array(
766                    'color'                => array(
767                        'background'                    => true,
768                        'text'                          => true,
769                        'gradients'                     => false,
770                        '__experimentalDefaultControls' => array(
771                            'background' => true,
772                            'text'       => true,
773                        ),
774                    ),
775                    'typography'           => array(
776                        'fontSize'                      => true,
777                        'lineHeight'                    => true,
778                        '__experimentalFontFamily'      => true,
779                        '__experimentalFontWeight'      => true,
780                        '__experimentalFontStyle'       => true,
781                        '__experimentalTextTransform'   => true,
782                        '__experimentalTextDecoration'  => true,
783                        '__experimentalLetterSpacing'   => true,
784                        '__experimentalDefaultControls' => array(
785                            'fontSize' => true,
786                        ),
787                    ),
788                    '__experimentalBorder' => array(
789                        'color'                         => true,
790                        'radius'                        => true,
791                        'style'                         => true,
792                        'width'                         => true,
793                        '__experimentalDefaultControls' => array(
794                            'color'  => true,
795                            'radius' => true,
796                            'style'  => true,
797                            'width'  => true,
798                        ),
799                    ),
800                    'spacing'              => array(
801                        'margin'                        => true,
802                        'padding'                       => true,
803                        '__experimentalDefaultControls' => array(
804                            'margin'  => true,
805                            'padding' => true,
806                        ),
807                    ),
808                ),
809                'uses_context'     => array(
810                    'jetpack/field-image-select-is-supersized',
811                    'jetpack/field-image-select-show-labels',
812                    'jetpack/field-image-options-type',
813                    'jetpack/field-share-attributes',
814                ),
815                'provides_context' => array(
816                    'allowResize' => 'allowResize',
817                    'imageCrop'   => 'imageCrop',
818                    'fixedHeight' => 'fixedHeight',
819                ),
820            )
821        );
822    }
823
824    /**
825     * Set field-file extension available hook handler
826     */
827    public static function set_file_field_extension_available() {
828        if ( ! apply_filters( 'jetpack_unauth_file_upload_plan_check', true ) ) {
829            \Jetpack_Gutenberg::set_extension_available( 'field-file' );
830        }
831    }
832
833    /**
834     * Render the gutenblock form.
835     *
836     * @param array  $atts - the block attributes.
837     * @param string $content - html content.
838     *
839     * @return string
840     */
841    /**
842     * Static storage for form step count.
843     *
844     * @var int
845     */
846    private static $form_step_count = 1;
847
848    /**
849     * Hook into pre_render_block to count form steps before inner blocks render.
850     *
851     * @param string|null $pre_render   The pre-rendered content. Default null.
852     * @param array       $parsed_block The block being rendered.
853     * @return string|null
854     */
855    public static function pre_render_contact_form( $pre_render, $parsed_block ) {
856        // Only process contact form blocks
857        if ( ! isset( $parsed_block['blockName'] ) || $parsed_block['blockName'] !== 'jetpack/contact-form' ) {
858            return $pre_render;
859        }
860
861        // Count and store form steps
862        self::$form_step_count = self::count_form_steps_in_block( $parsed_block );
863
864        if ( isset( $parsed_block['attrs']['ref'] ) || null === $pre_render ) {
865            // This happends only important for lagacy reasons and when code is programatically generated.
866            // Since it can include the previous jetpack button.
867            // Which by default renders as a link instead of a button resulting in the form not submitting.
868            add_filter( 'jetpack_button_default_element', array( __CLASS__, 'submit_button_element' ) );
869        }
870
871        // For ref (synced) forms, render here via pre_render_block to short-circuit
872        // render_block(). This prevents wp_render_layout_support_flag from adding
873        // layout classes to the outer ref block's container div. The synced form's
874        // inner jetpack/contact-form block renders through the normal pipeline and
875        // gets layout classes on the correct element (wp-block-jetpack-contact-form).
876        if ( isset( $parsed_block['attrs']['ref'] ) ) {
877            return self::gutenblock_render_form( $parsed_block['attrs'], '' );
878        }
879
880        return $pre_render; // Don't actually pre-render, let normal rendering continue
881    }
882
883    /**
884     * Count form step blocks in a contact form block.
885     *
886     * @param array $block The contact form block.
887     * @return int Number of form steps found.
888     */
889    private static function count_form_steps_in_block( $block ) {
890        $step_count = 0;
891
892        if ( isset( $block['innerBlocks'] ) ) {
893            foreach ( $block['innerBlocks'] as $inner_block ) {
894                if ( $inner_block['blockName'] === 'jetpack/form-step' ) {
895                    ++$step_count;
896                }
897                // Also check nested blocks (like step containers)
898                $step_count += self::count_form_steps_in_block( $inner_block );
899            }
900        }
901
902        return $step_count;
903    }
904
905    /**
906     * Get the step count for forms (used by progress indicator).
907     *
908     * @return int The step count.
909     */
910    public static function get_form_step_count() {
911        return self::$form_step_count;
912    }
913
914    /**
915     * Render fallback for non-interactive contexts (email, feed, API, etc.).
916     *
917     * @param array $atts - the block attributes.
918     *
919     * @return string
920     */
921    private static function render_fallback( $atts ) {
922        return sprintf(
923            '<div class="%1$s"><a href="%2$s" target="_blank" rel="noopener noreferrer">%3$s</a></div>',
924            esc_attr( Blocks::classes( 'contact-form', $atts ) ),
925            esc_url( get_the_permalink() ),
926            esc_html__( 'Submit a form.', 'jetpack-forms' )
927        );
928    }
929
930    /**
931     * Render the contact form block for email contexts.
932     *
933     * This method is called by WordPress/WooCommerce email rendering system when a form block
934     * appears in email content. Forms are not interactive in email contexts, so we always return
935     * a fallback link that directs recipients to the form on the website.
936     *
937     * Note: The $block_content and $rendering_context parameters are required by the
938     * render_email_callback signature but are intentionally unused here. We only need the
939     * block attributes from $parsed_block to generate the fallback HTML, and we don't need
940     * to process the block content or use email-specific rendering context since we're
941     * always returning a simple fallback.
942     *
943     * @param string $block_content     The original block HTML content. Unused - we always return a fallback.
944     * @param array  $parsed_block      The parsed block data including attributes.
945     * @param object $rendering_context Email rendering context. Unused - not needed for fallback rendering.
946     *
947     * @return string HTML fallback with link to submit the form on the website.
948     */
949    public static function render_email( $block_content, array $parsed_block, $rendering_context ) { // phpcs:ignore VariableAnalysis.CodeAnalysis.VariableAnalysis.UnusedVariable
950        $atts = $parsed_block['attrs'] ?? array();
951
952        return self::render_fallback( $atts );
953    }
954
955    /**
956     * The element a Button block inside a form falls back to when it doesn't set one.
957     *
958     * Filters `jetpack_button_default_element` for the duration of a form's render.
959     *
960     * @return string
961     */
962    public static function submit_button_element() {
963        return 'button';
964    }
965
966    /**
967     * Render the gutenblock form.
968     *
969     * @param array  $atts - the block attributes.
970     * @param string $content - html content.
971     *
972     * @return string
973     */
974    public static function gutenblock_render_form( $atts, $content ) {
975        $form = self::render_form( $atts, $content );
976
977        // The form and its buttons are rendered by now, so stop defaulting Button blocks
978        // to `button` â€” any further button on the page is not a submit button.
979        remove_filter( 'jetpack_button_default_element', array( __CLASS__, 'submit_button_element' ) );
980
981        return $form;
982    }
983
984    /**
985     * Render the gutenblock form markup.
986     *
987     * @param array  $atts - the block attributes.
988     * @param string $content - html content.
989     *
990     * @return string
991     */
992    private static function render_form( $atts, $content ) {
993        // We should not render block if the module is disabled on a site using the Jetpack plugin.
994        // Exception: allow rendering in preview mode so form previews work.
995        if ( class_exists( 'Jetpack' ) && ! ( new Modules() )->is_active( 'contact-form' ) && ! Form_Preview::is_preview_mode() ) {
996            return '';
997        }
998        // Render fallback in other contexts than frontend (i.e. feed, emails, API, etc.), unless the form is being submitted.
999        // Exception: allow rendering in preview mode.
1000        if ( ! Request::is_frontend() && ! isset( $_POST['contact-form-id'] ) && ! Form_Preview::is_preview_mode() ) { // phpcs:ignore WordPress.Security.NonceVerification.Missing
1001            return self::render_fallback( $atts );
1002        }
1003
1004        self::load_view_scripts();
1005
1006        // Handle ref attribute - load form from jetpack_form post
1007        if ( isset( $atts['ref'] ) ) {
1008            $ref_id = absint( $atts['ref'] );
1009            if ( $ref_id > 0 ) {
1010                return self::apply_background_support( Contact_Form::render_synced_form( $ref_id ), $atts, self::FORM_BLOCK_CLASS );
1011            } else {
1012                return ''; // Invalid ref ID.
1013            }
1014        }
1015
1016        return self::apply_background_support( Contact_Form::parse( $atts, do_blocks( $content ) ), $atts, self::FORM_BLOCK_CLASS );
1017    }
1018
1019    /**
1020     * Apply a block's background image styles to the block's own element.
1021     *
1022     * Core's `wp_render_background_support()` targets the first tag of the rendered output.
1023     * Both form blocks wrap themselves at render time â€” the interactivity wrapper for a step,
1024     * the container div and any admin-only notices for a form â€” so the first tag is not the
1025     * element carrying the block's color, padding and radius, and for a synced form it varies
1026     * with who is viewing. Serialization is skipped in the block registration (PHP only: adding
1027     * it to block.json or index.js would kill the editor preview) and the styles are applied
1028     * here, on the element matching $target_class.
1029     *
1030     * Mirrors core's `wp_render_background_support()`, including its `cover` and `50% 50%`
1031     * defaults. Core 7.1 added gradient handling this does not implement, so `gradient` must
1032     * stay out of the JS supports until it is mirrored here too.
1033     *
1034     * @param string $html         Rendered HTML containing the block's own element.
1035     * @param array  $atts         Block attributes.
1036     * @param string $target_class Class of the element that should carry the background.
1037     *
1038     * @return string The HTML, with background styles applied when the block has an image.
1039     */
1040    public static function apply_background_support( $html, $atts, $target_class ) {
1041        $background = $atts['style']['background'] ?? null;
1042
1043        if ( ! is_array( $background ) || empty( $background['backgroundImage'] ) ) {
1044            return $html;
1045        }
1046
1047        $background_styles = array(
1048            'backgroundImage'      => $background['backgroundImage'],
1049            'backgroundSize'       => $background['backgroundSize'] ?? 'cover',
1050            'backgroundPosition'   => $background['backgroundPosition'] ?? null,
1051            'backgroundRepeat'     => $background['backgroundRepeat'] ?? null,
1052            'backgroundAttachment' => $background['backgroundAttachment'] ?? null,
1053        );
1054
1055        if ( 'contain' === $background_styles['backgroundSize'] && ! $background_styles['backgroundPosition'] ) {
1056            $background_styles['backgroundPosition'] = '50% 50%';
1057        }
1058
1059        // A step's styles are still ahead of the do_shortcode() pass in Contact_Form::parse(),
1060        // so percent-encode the brackets that would otherwise make this URL look like a
1061        // shortcode: unencoded they are stripped out of the attribute, taking the background
1062        // with them, and the shortcode's side effects run.
1063        if ( isset( $background_styles['backgroundImage']['url'] ) && is_string( $background_styles['backgroundImage']['url'] ) ) {
1064            $background_styles['backgroundImage']['url'] = strtr(
1065                $background_styles['backgroundImage']['url'],
1066                array(
1067                    '[' => '%5B',
1068                    ']' => '%5D',
1069                )
1070            );
1071        }
1072
1073        $styles = wp_style_engine_get_styles( array( 'background' => $background_styles ) );
1074
1075        if ( empty( $styles['css'] ) ) {
1076            return $html;
1077        }
1078
1079        $tags = new \WP_HTML_Tag_Processor( $html );
1080
1081        if ( ! $tags->next_tag( array( 'class_name' => $target_class ) ) ) {
1082            return $html;
1083        }
1084
1085        $existing_style = $tags->get_attribute( 'style' );
1086
1087        if ( is_string( $existing_style ) && '' !== $existing_style ) {
1088            $separator = str_ends_with( $existing_style, ';' ) ? '' : ';';
1089            $tags->set_attribute( 'style', $existing_style . $separator . $styles['css'] );
1090        } else {
1091            $tags->set_attribute( 'style', $styles['css'] );
1092        }
1093
1094        $tags->add_class( 'has-background' );
1095
1096        return $tags->get_updated_html();
1097    }
1098
1099    /**
1100     * Load editor styles for the block.
1101     *
1102     * @deprecated 7.5.0 This function is deprecated and will be removed in a future version.
1103     */
1104    public static function load_editor_styles() {
1105        _deprecated_function( __FUNCTION__, 'jetpack-7.5.0' );
1106    }
1107
1108    /**
1109     * Loads scripts
1110     */
1111    public static function load_editor_scripts() {
1112        global $post;
1113        // Bail early if the user cannot manage the block.
1114        if ( ! self::can_manage_block() ) {
1115            return;
1116        }
1117
1118        $handle = 'jp-forms-blocks';
1119
1120        // Ensure the jetpack-blocks-editor dependency exists. When the Blocks module
1121        // is inactive, nothing registers it, but the Forms editor JS needs the
1122        // Jetpack_Editor_Initial_State global (with availability data) it provides.
1123        self::maybe_register_blocks_editor_script();
1124
1125        Assets::register_script(
1126            $handle,
1127            '../../../dist/blocks/editor.js',
1128            __FILE__,
1129            array(
1130                'dependencies' => array( 'jetpack-blocks-editor' ),
1131                'in_footer'    => true,
1132                'textdomain'   => 'jetpack-forms',
1133                'enqueue'      => true,
1134                'css_path'     => '../../../dist/blocks/editor.css',
1135            )
1136        );
1137
1138        // Create a Contact_Form instance to get the default values
1139        $form_responses_url      = Forms_Dashboard::get_forms_admin_url();
1140        $akismet_active_with_key = Jetpack::is_akismet_active();
1141        $akismet_key_url         = admin_url( 'admin.php?page=akismet-key-config' );
1142
1143        $default_recipient = Contact_Form::get_default_to_with_source( $post );
1144
1145        $data = array(
1146            'defaults' => array(
1147                'to'                   => $default_recipient['to'],
1148                'toSource'             => $default_recipient['source'],
1149                // Decoded for display only: get_option( 'blogname' ) stores the site title HTML-encoded,
1150                // which would otherwise surface raw entities such as &#039; in the editor's placeholder.
1151                'subject'              => wp_specialchars_decode( Contact_Form::get_default_subject( array() ), ENT_QUOTES ),
1152                'formsResponsesUrl'    => $form_responses_url,
1153                'akismetActiveWithKey' => $akismet_active_with_key,
1154                'akismetUrl'           => $akismet_key_url,
1155                'assetsUrl'            => Jetpack_Forms::assets_url(),
1156                'isMailPoetEnabled'    => Jetpack_Forms::is_mailpoet_enabled(),
1157                // So the file field's "Maximum files" control offers exactly what the site will honour,
1158                // including when a filter has raised the ceiling.
1159                'maxFilesLimit'        => Contact_Form_Field::get_file_field_max_files_limit(),
1160            ),
1161        );
1162
1163        wp_add_inline_script( $handle, 'window.jpFormsBlocks = ' . wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_HEX_TAG | JSON_HEX_AMP ) . ';', 'before' );
1164    }
1165
1166    /**
1167     * Conditionally loads the AI form generation integration script.
1168     *
1169     * This script is only loaded when:
1170     * 1. The AI Assistant extension is available (ai-assistant-form-support)
1171     * 2. The central-form-management feature flag is enabled
1172     *
1173     * By checking these conditions in PHP, we ensure no JavaScript is loaded
1174     * when either the AI extension is disabled or central form management is off.
1175     *
1176     * This is hooked at priority 11 on enqueue_block_editor_assets to ensure
1177     * it runs after Jetpack_Gutenberg registers extensions at priority 10.
1178     */
1179    public static function maybe_load_ai_integration() {
1180        // Bail if the user cannot manage the block â€” jp-forms-blocks won't be registered.
1181        if ( ! self::can_manage_block() ) {
1182            return;
1183        }
1184
1185        // Check if central form management is enabled.
1186        if ( ! Contact_Form_Plugin::has_editor_feature_flag( 'central-form-management' ) ) {
1187            return;
1188        }
1189
1190        // Check if AI Assistant form support is available.
1191        // This extension is set as available when the AI Assistant block is registered.
1192        if ( ! class_exists( 'Jetpack_Gutenberg' ) ) {
1193            return;
1194        }
1195
1196        // Ensure extensions are registered by calling get_cached_availability().
1197        \Jetpack_Gutenberg::get_cached_availability();
1198        if ( ! \Jetpack_Gutenberg::is_available( 'ai-assistant-form-support' ) ) {
1199            return;
1200        }
1201
1202        Assets::register_script(
1203            'jp-forms-ai-plugin',
1204            '../../../dist/blocks/ai-form-plugin.js',
1205            __FILE__,
1206            array(
1207                'dependencies' => array( 'jp-forms-blocks' ),
1208                'in_footer'    => true,
1209                'textdomain'   => 'jetpack-forms',
1210                'enqueue'      => true,
1211            )
1212        );
1213    }
1214
1215    /**
1216     * Add REST API endpoints to the block editor preload list.
1217     *
1218     * @param array $paths Existing paths to preload.
1219     * @return array Updated paths to preload.
1220     */
1221    public static function preload_endpoints( $paths ) {
1222        $paths[] = array( '/wp/v2/feedback/config', 'GET' );
1223        $paths[] = array( '/wp/v2/feedback/config?_locale=user', 'GET' );
1224        return $paths;
1225    }
1226
1227    /**
1228     * Loads scripts
1229     */
1230    public static function load_view_scripts() {
1231        if ( is_admin() ) {
1232            // A block's view assets will not be required in wp-admin.
1233            return;
1234        }
1235
1236        Assets::register_script(
1237            'jp-forms-blocks',
1238            '../../../dist/blocks/view.js',
1239            __FILE__,
1240            array(
1241                'in_footer'  => true,
1242                'strategy'   => 'defer',
1243                'textdomain' => 'jetpack-forms',
1244                'enqueue'    => true,
1245            )
1246        );
1247    }
1248
1249    /**
1250     * Register a minimal jetpack-blocks-editor script when the Blocks module is inactive.
1251     *
1252     * The Forms editor JS depends on jetpack-blocks-editor for the
1253     * Jetpack_Editor_Initial_State global, which includes block availability data.
1254     * Normally the Blocks module registers this, but Forms needs to work independently.
1255     */
1256    private static function maybe_register_blocks_editor_script() {
1257        if ( wp_script_is( 'jetpack-blocks-editor', 'registered' ) ) {
1258            return;
1259        }
1260
1261        // When the Blocks module is active it will register the real script, so bail.
1262        if ( class_exists( 'Jetpack' ) && ( new Modules() )->is_active( 'blocks' ) ) {
1263            return;
1264        }
1265
1266        // Register a minimal script to satisfy the dependency.
1267        wp_register_script( 'jetpack-blocks-editor', '', array(), JETPACK__VERSION, true );
1268        wp_register_style( 'jetpack-blocks-editor', false, array(), JETPACK__VERSION );
1269
1270        // Provide the initial state the Forms editor JS expects:
1271        // - available_blocks: the JS block registration checks this before calling registerBlockType.
1272        // - modules: the useModuleStatus hook reads this to decide whether to show the block
1273        // or an "Activate Forms" placeholder.
1274        // - feature_flags: hasFeatureFlag() reads this for central-form-management,
1275        // form-webhooks, and multistep-form.
1276        wp_localize_script(
1277            Shared_Stores_Assets::SCRIPT_HANDLE,
1278            'Jetpack_Editor_Initial_State',
1279            array(
1280                'available_blocks' => array(
1281                    'contact-form' => array( 'available' => true ),
1282                ),
1283                'modules'          => array(
1284                    'contact-form' => array( 'activated' => true ),
1285                ),
1286                'feature_flags'    => apply_filters( 'jetpack_block_editor_feature_flags', array() ),
1287            )
1288        );
1289    }
1290
1291    /**
1292     * Check if the current user can view the block.
1293     * Every user can see it if the Contact Form module is active,
1294     * but if it is inactive, only admins can see it.
1295     *
1296     * This is only useful when the Contact Form package is used within the Jetpack plugin,
1297     * where the module logic exists.
1298     *
1299     * @since 0.49.0
1300     *
1301     * @return bool
1302     */
1303    public static function can_manage_block() {
1304        if (
1305            /**
1306             * Allow third-parties to override the form block's visibility.
1307             *
1308             * @since 0.49.0
1309             *
1310             * @module contact-form
1311             *
1312             * @param bool $can_manage_block Whether the current user can manage the block.
1313             */
1314            apply_filters( 'jetpack_contact_form_can_manage_block', false )
1315        ) {
1316            return true;
1317        }
1318
1319        if ( ! class_exists( 'Jetpack' ) ) {
1320            return true;
1321        }
1322
1323        return ( new Modules() )->is_active( 'contact-form' ) || current_user_can( 'jetpack_activate_modules' );
1324    }
1325}