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