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