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