Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
38.34% covered (danger)
38.34%
74 / 193
40.00% covered (danger)
40.00%
6 / 15
CRAP
0.00% covered (danger)
0.00%
0 / 1
Util
38.34% covered (danger)
38.34%
74 / 193
40.00% covered (danger)
40.00%
6 / 15
564.80
0.00% covered (danger)
0.00%
0 / 1
 init
100.00% covered (success)
100.00%
16 / 16
100.00% covered (success)
100.00%
1 / 1
1
 register_pattern
0.00% covered (danger)
0.00%
0 / 55
0.00% covered (danger)
0.00%
0 / 1
6
 grunion_contact_form_set_block_template_attribute
91.67% covered (success)
91.67%
11 / 12
0.00% covered (danger)
0.00%
0 / 1
4.01
 grunion_contact_form_set_block_template_part_id_global
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 grunion_contact_form_unset_block_template_part_id_global
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
20
 grunion_contact_form_suspend_block_template_id_in_post_content
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 grunion_contact_form_restore_block_template_id_after_post_content
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
5
 grunion_contact_form_filter_widget_block_content
0.00% covered (danger)
0.00%
0 / 7
0.00% covered (danger)
0.00%
0 / 1
2
 grunion_delete_old_spam
82.35% covered (warning)
82.35%
14 / 17
0.00% covered (danger)
0.00%
0 / 1
4.09
 grunion_delete_old_temp_feedback
0.00% covered (danger)
0.00%
0 / 16
0.00% covered (danger)
0.00%
0 / 1
20
 jetpack_tracks_record_grunion_pre_message_sent
0.00% covered (danger)
0.00%
0 / 21
0.00% covered (danger)
0.00%
0 / 1
42
 grunion_contact_form_apply_block_attribute
100.00% covered (success)
100.00%
7 / 7
100.00% covered (success)
100.00%
1 / 1
3
 modify_contact_form_blocks_recursive
100.00% covered (success)
100.00%
12 / 12
100.00% covered (success)
100.00%
1 / 1
4
 get_export_filename
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
6
 maybe_add_colon_to_label
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
1<?php
2/**
3 * Contact_Form_Util class.
4 *
5 * @package automattic/jetpack-forms
6 */
7
8namespace Automattic\Jetpack\Forms\ContactForm;
9
10/**
11 * This class serves as a container for what previously were standalone grunion functions.
12 * In the long term we should aim to move things to other classes and gradually get rid of this rather than adding more.
13 */
14class Util {
15
16    /**
17     * Saved values of the block_template global while core/post-content blocks render.
18     *
19     * A stack so nested or sequential post-content renders (e.g. a query loop) restore
20     * correctly. @see grunion_contact_form_suspend_block_template_id_in_post_content().
21     *
22     * @var array
23     */
24    private static $block_template_id_suspended = array();
25
26    /**
27     * Registers all relevant actions and filters for this class.
28     */
29    public static function init() {
30        add_filter( 'template_include', '\Automattic\Jetpack\Forms\ContactForm\Util::grunion_contact_form_set_block_template_attribute' );
31
32        add_action( 'render_block_core_template_part_post', '\Automattic\Jetpack\Forms\ContactForm\Util::grunion_contact_form_set_block_template_part_id_global' );
33        add_action( 'render_block_core_template_part_file', '\Automattic\Jetpack\Forms\ContactForm\Util::grunion_contact_form_set_block_template_part_id_global' );
34        add_action( 'render_block_core_template_part_none', '\Automattic\Jetpack\Forms\ContactForm\Util::grunion_contact_form_set_block_template_part_id_global' );
35        add_action( 'gutenberg_render_block_core_template_part_post', '\Automattic\Jetpack\Forms\ContactForm\Util::grunion_contact_form_set_block_template_part_id_global' );
36        add_action( 'gutenberg_render_block_core_template_part_file', '\Automattic\Jetpack\Forms\ContactForm\Util::grunion_contact_form_set_block_template_part_id_global' );
37        add_action( 'gutenberg_render_block_core_template_part_none', '\Automattic\Jetpack\Forms\ContactForm\Util::grunion_contact_form_set_block_template_part_id_global' );
38
39        add_filter( 'render_block', '\Automattic\Jetpack\Forms\ContactForm\Util::grunion_contact_form_unset_block_template_part_id_global', 10, 2 );
40        add_filter( 'widget_block_content', '\Automattic\Jetpack\Forms\ContactForm\Util::grunion_contact_form_filter_widget_block_content', 1, 3 );
41
42        // Suspend the block_template global while core/post-content renders, so forms in the post
43        // body are attributed to the post (and gated on the author), not to the template.
44        add_filter( 'pre_render_block', '\Automattic\Jetpack\Forms\ContactForm\Util::grunion_contact_form_suspend_block_template_id_in_post_content', 10, 2 );
45        add_filter( 'render_block', '\Automattic\Jetpack\Forms\ContactForm\Util::grunion_contact_form_restore_block_template_id_after_post_content', 10, 2 );
46
47        // Register the default for the `central-form-management` feature flag at bootstrap
48        // so that early callers of Contact_Form_Plugin::has_editor_feature_flag() — such as
49        // the Forms dashboard default-tab redirect, which runs before the `init` hook fires —
50        // see the correct value. Only the lightweight default is registered here; paid-plan
51        // flags stay in Contact_Form_Block::register_feature(), hooked later from
52        // Contact_Form_Block::register_block() on `init` priority 9.
53        add_filter( 'jetpack_block_editor_feature_flags', '\Automattic\Jetpack\Extensions\Contact_Form\Contact_Form_Block::register_central_form_management_default' );
54
55        add_action( 'init', '\Automattic\Jetpack\Forms\ContactForm\Contact_Form_Plugin::init', 9 );
56        add_action( 'grunion_scheduled_delete', '\Automattic\Jetpack\Forms\ContactForm\Util::grunion_delete_old_spam' );
57        add_action( 'grunion_scheduled_delete_temp', '\Automattic\Jetpack\Forms\ContactForm\Util::grunion_delete_old_temp_feedback' );
58        add_action( 'grunion_pre_message_sent', '\Automattic\Jetpack\Forms\ContactForm\Util::jetpack_tracks_record_grunion_pre_message_sent', 12, 3 );
59    }
60
61    /**
62     * Registers contact form block patterns.
63     */
64    public static function register_pattern() {
65        $category_slug = 'forms';
66        register_block_pattern_category( $category_slug, array( 'label' => __( 'Forms', 'jetpack-forms' ) ) );
67
68        $patterns = array(
69            'contact-form'         => array(
70                'title'      => __( 'Contact Form', 'jetpack-forms' ),
71                'blockTypes' => array( 'jetpack/contact-form' ),
72                'categories' => array( $category_slug ),
73                'content'    => '<!-- wp:jetpack/contact-form -->
74                    <div class="wp-block-jetpack-contact-form">
75                        <!-- wp:jetpack/field-name {"required":true} /-->
76                        <!-- wp:jetpack/field-email {"required":true} /-->
77                        <!-- wp:jetpack/field-textarea /-->
78                        <!-- wp:button {"tagName":"button","type":"submit"} -->
79                            <div class="wp-block-button"><button type="submit" class="wp-block-button__link wp-element-button">Contact us</button></div>
80                        <!-- /wp:button -->
81                    </div>
82                    <!-- /wp:jetpack/contact-form -->',
83            ),
84            'newsletter-form'      => array(
85                'title'      => __( 'Lead Capture Form', 'jetpack-forms' ),
86                'blockTypes' => array( 'jetpack/contact-form' ),
87                'categories' => array( $category_slug ),
88                'content'    => '<!-- wp:jetpack/contact-form -->
89                    <div class="wp-block-jetpack-contact-form">
90                        <!-- wp:jetpack/field-name {"required":true} /-->
91                        <!-- wp:jetpack/field-email {"required":true} /-->
92                        <!-- wp:jetpack/field-consent /-->
93                        <!-- wp:button {"tagName":"button","type":"submit"} -->
94                            <div class="wp-block-button"><button type="submit" class="wp-block-button__link wp-element-button">Subscribe</button></div>
95                        <!-- /wp:button -->
96                    </div>
97                    <!-- /wp:jetpack/contact-form -->',
98            ),
99            'rsvp-form'            => array(
100                'title'      => __( 'RSVP Form', 'jetpack-forms' ),
101                'blockTypes' => array( 'jetpack/contact-form' ),
102                'categories' => array( $category_slug ),
103                'content'    => '<!-- wp:jetpack/contact-form {"subject":"A new RSVP from your website"} -->
104                    <div class="wp-block-jetpack-contact-form">
105                        <!-- wp:jetpack/field-name {"required":true} /-->
106                        <!-- wp:jetpack/field-email {"required":true} /-->
107                        <!-- wp:jetpack/field-radio {"label":"Attending?","required":true,"options":["Yes","No"]} /-->
108                        <!-- wp:jetpack/field-textarea {"label":"Other Details"} /-->
109                        <!-- wp:button {"tagName":"button","type":"submit"} -->
110                            <div class="wp-block-button"><button type="submit" class="wp-block-button__link wp-element-button">Send RSVP</button></div>
111                        <!-- /wp:button -->
112                    </div>
113                    <!-- /wp:jetpack/contact-form -->',
114            ),
115            'registration-form'    => array(
116                'title'      => __( 'Registration Form', 'jetpack-forms' ),
117                'blockTypes' => array( 'jetpack/contact-form' ),
118                'categories' => array( $category_slug ),
119                'content'    => '<!-- wp:jetpack/contact-form {"subject":"A new registration from your website"} -->
120                    <div class="wp-block-jetpack-contact-form">
121                        <!-- wp:jetpack/field-name {"required":true} /-->
122                        <!-- wp:jetpack/field-email {"required":true} /-->
123                        <!-- wp:jetpack/field-telephone {"label":"Phone Number"} /-->
124                        <!-- wp:jetpack/field-select {"label":"How did you hear about us?","options":["Search Engine","Social Media","TV","Radio","Friend or Family"]} /-->
125                        <!-- wp:jetpack/field-textarea {"label":"Other Details"} /-->
126                        <!-- wp:button {"tagName":"button","type":"submit"} -->
127                            <div class="wp-block-button"><button type="submit" class="wp-block-button__link wp-element-button">Send</button></div>
128                        <!-- /wp:button -->
129                    </div>
130                    <!-- /wp:jetpack/contact-form -->',
131            ),
132            'appointment-form'     => array(
133                'title'      => __( 'Appointment Form', 'jetpack-forms' ),
134                'blockTypes' => array( 'jetpack/contact-form' ),
135                'categories' => array( $category_slug ),
136                'content'    => '<!-- wp:jetpack/contact-form {"subject":"A new appointment booked from your website"} -->
137                    <div class="wp-block-jetpack-contact-form">
138                        <!-- wp:jetpack/field-name {"required":true} /-->
139                        <!-- wp:jetpack/field-email {"required":true} /-->
140                        <!-- wp:jetpack/field-telephone {"required":true} /-->
141                        <!-- wp:jetpack/field-date {"label":"Date","required":true} /-->
142                        <!-- wp:jetpack/field-radio {"label":"Time","required":true,"options":["Morning","Afternoon"]} /-->
143                        <!-- wp:jetpack/field-textarea {"label":"Notes"} /-->
144                        <!-- wp:button {"tagName":"button","type":"submit"} -->
145                            <div class="wp-block-button"><button type="submit" class="wp-block-button__link wp-element-button">Book Appointment</button></div>
146                        <!-- /wp:button -->
147                    </div>
148                    <!-- /wp:jetpack/contact-form -->',
149            ),
150            'feedback-form'        => array(
151                'title'      => __( 'Feedback Form', 'jetpack-forms' ),
152                'blockTypes' => array( 'jetpack/contact-form' ),
153                'categories' => array( $category_slug ),
154                'content'    => '<!-- wp:jetpack/contact-form {"subject":"New feedback received from your website"} -->
155                    <div class="wp-block-jetpack-contact-form">
156                        <!-- wp:jetpack/field-name {"required":true} /-->
157                        <!-- wp:jetpack/field-email {"required":true} /-->
158                        <!-- wp:jetpack/field-rating {"required":true} -->
159                            <div><!-- wp:jetpack/label {"label":"Please rate our website"} /-->
160                        <!-- wp:jetpack/input-rating /--></div>
161                        <!-- /wp:jetpack/field-rating -->
162                        <!-- wp:jetpack/field-textarea {"label":"How could we improve?"} /-->
163                        <!-- wp:button {"tagName":"button","type":"submit"} -->
164                            <div class="wp-block-button"><button type="submit" class="wp-block-button__link wp-element-button">Send Feedback</button></div>
165                        <!-- /wp:button -->
166                    </div>
167                    <!-- /wp:jetpack/contact-form -->',
168            ),
169            'salesforce-lead-form' => array(
170                'title'      => __( 'Salesforce Lead Form', 'jetpack-forms' ),
171                'blockTypes' => array( 'jetpack/contact-form' ),
172                'categories' => array( $category_slug ),
173                'content'    => '<!-- wp:jetpack/contact-form {"formTitle":"Salesforce Lead Form"} -->
174                    <div class="wp-block-jetpack-contact-form">
175                        <!-- wp:jetpack/field-name {"label":"First Name","required":true,"id":"first_name"} /-->
176                        <!-- wp:jetpack/field-name {"label":"Last Name","required":true,"id":"last_name"} /-->
177                        <!-- wp:jetpack/field-email {"label":"Email","required":true,"id":"email"} /-->
178                        <!-- wp:jetpack/field-telephone {"label":"Phone","id":"phone"} /-->
179                        <!-- wp:jetpack/field-text {"label":"Company","id":"company"} /-->
180                        <!-- wp:jetpack/field-text {"label":"Job Title","id":"title"} /-->
181                        <!-- wp:button {"tagName":"button","type":"submit"} -->
182                            <div class="wp-block-button"><button type="submit" class="wp-block-button__link wp-element-button">Submit</button></div>
183                        <!-- /wp:button -->
184                    </div>
185                    <!-- /wp:jetpack/contact-form -->',
186            ),
187        );
188
189        foreach ( $patterns as $name => $pattern ) {
190            register_block_pattern( $name, $pattern );
191        }
192    }
193
194    /**
195     * Sets the 'block_template' attribute on all instances of wp:jetpack/contact-form in
196     * the $_wp_current_template_content global variable.
197     *
198     * The $_wp_current_template_content global variable is hydrated immediately prior to
199     * 'template_include' in wp-includes/template-loader.php.
200     *
201     * This fixes Contact Form Blocks added to FSE _templates_ (e.g. Single or 404).
202     *
203     * @param string $template Template to be loaded.
204     */
205    public static function grunion_contact_form_set_block_template_attribute( $template ) {
206        global $_wp_current_template_content;
207        if ( ! is_string( $template ) ) {
208            return $template;
209        }
210
211        if ( 'template-canvas.php' === basename( $template ) ) {
212            Contact_Form::style_on();
213            $_wp_current_template_content = self::grunion_contact_form_apply_block_attribute(
214                $_wp_current_template_content,
215                array(
216                    'block_template' => 'canvas',
217                )
218            );
219
220            // Mark that we are rendering a block template, so forms in the template chrome are
221            // attributed to it. This global is the trusted signal Feedback_Source::get_current()
222            // uses for the block_template source type (the content attribute is not trusted). It
223            // is suspended while core/post-content renders so a form in the post body is not
224            // mistaken for a template-authored form.
225            global $_wp_current_template_id;
226            $GLOBALS['grunion_block_template_id'] = ! empty( $_wp_current_template_id ) ? $_wp_current_template_id : 'canvas';
227        }
228        return $template;
229    }
230
231    /**
232     * Sets the $grunion_block_template_part_id global.
233     *
234     * This is part of the fix for Contact Form Blocks added to FSE _template parts_ (e.g footer).
235     * The global is processed in Contact_Form::parse().
236     *
237     * @param string $template_part_id ID for the currently rendered template part.
238     */
239    public static function grunion_contact_form_set_block_template_part_id_global( $template_part_id ) {
240        $GLOBALS['grunion_block_template_part_id'] = $template_part_id;
241    }
242
243    /**
244     * Unsets the global when block is done rendering.
245     *
246     * @param string $content Rendered block content.
247     * @param array  $block   The full block, including name and attributes.
248     * @return string
249     */
250    public static function grunion_contact_form_unset_block_template_part_id_global( $content, $block ) {
251        if ( isset( $block['blockName'] )
252            && 'core/template-part' === $block['blockName']
253            && isset( $GLOBALS['grunion_block_template_part_id'] ) ) {
254            unset( $GLOBALS['grunion_block_template_part_id'] );
255        }
256        return $content;
257    }
258
259    /**
260     * Suspends the block_template global while a core/post-content block renders.
261     *
262     * The core/post-content block renders the post body, which may contain a contact form
263     * authored by a user without edit_theme_options. Such a form must be attributed to the post
264     * (and gated on the post author), not to the surrounding template, so the trusted
265     * block_template signal is removed for the duration of the render and restored afterwards.
266     *
267     * Hooked on `pre_render_block`; returns its first argument unchanged so rendering proceeds.
268     *
269     * @param string|null $pre_render   The pre-rendered content. Default null.
270     * @param array       $parsed_block The block being rendered.
271     * @return string|null Unchanged $pre_render.
272     */
273    public static function grunion_contact_form_suspend_block_template_id_in_post_content( $pre_render, $parsed_block ) {
274        if ( isset( $parsed_block['blockName'] ) && 'core/post-content' === $parsed_block['blockName'] ) {
275            self::$block_template_id_suspended[] = $GLOBALS['grunion_block_template_id'] ?? null;
276            unset( $GLOBALS['grunion_block_template_id'] );
277        }
278        return $pre_render;
279    }
280
281    /**
282     * Restores the block_template global once a core/post-content block has finished rendering.
283     *
284     * Counterpart to grunion_contact_form_suspend_block_template_id_in_post_content(). Hooked on
285     * `render_block`; returns the block content unchanged.
286     *
287     * @param string $content Rendered block content.
288     * @param array  $block   The full block, including name and attributes.
289     * @return string Unchanged $content.
290     */
291    public static function grunion_contact_form_restore_block_template_id_after_post_content( $content, $block ) {
292        if ( isset( $block['blockName'] )
293            && 'core/post-content' === $block['blockName']
294            && ! empty( self::$block_template_id_suspended ) ) {
295            $restored = array_pop( self::$block_template_id_suspended );
296            if ( null !== $restored ) {
297                $GLOBALS['grunion_block_template_id'] = $restored;
298            }
299        }
300        return $content;
301    }
302
303    /**
304     * Sets the 'widget' attribute on all instances of the contact form in the widget block.
305     *
306     * @param string           $content  Existing widget block content.
307     * @param array            $instance Array of settings for the current widget.
308     * @param \WP_Widget_Block $widget   Current Block widget instance.
309     * @return string
310     */
311    public static function grunion_contact_form_filter_widget_block_content( $content, $instance, $widget ) {
312        Contact_Form::style_on();
313        // Inject 'block_template' => <widget-id> into all instances of the contact form block.
314        return self::grunion_contact_form_apply_block_attribute(
315            $content,
316            array(
317                'widget' => $widget->id,
318            )
319        );
320    }
321
322    /**
323     * Deletes old spam feedbacks to keep the posts table size under control.
324     */
325    public static function grunion_delete_old_spam() {
326        global $wpdb;
327
328        $grunion_delete_limit = 100;
329
330        $now_gmt = current_time( 'mysql', true );
331        // Use the spam status changed date if available, otherwise fall back to post_date_gmt for backward compatibility
332        $sql      = $wpdb->prepare(
333            "
334            SELECT p.`ID`
335            FROM $wpdb->posts p
336            LEFT JOIN $wpdb->postmeta pm ON p.`ID` = pm.`post_id` AND pm.`meta_key` = '_spam_status_changed_gmt'
337            WHERE DATE_SUB( %s, INTERVAL 15 DAY ) > COALESCE( pm.`meta_value`, p.`post_date_gmt` )
338                AND p.`post_type` = 'feedback'
339                AND p.`post_status` = 'spam'
340            LIMIT %d
341        ",
342            $now_gmt,
343            $grunion_delete_limit
344        );
345        $post_ids = $wpdb->get_col( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
346
347        foreach ( (array) $post_ids as $post_id ) {
348            // force a full delete, skip the trash
349            wp_delete_post( $post_id, true );
350        }
351
352        if (
353            /**
354             * Filter if the module run OPTIMIZE TABLE on the core WP tables.
355             *
356             * @module contact-form
357             *
358             * @since 1.3.1
359             * @since 6.4.0 Set to false by default.
360             *
361             * @param bool $filter Should Jetpack optimize the table, defaults to false.
362             */
363            apply_filters( 'grunion_optimize_table', false )
364        ) {
365            $wpdb->query( "OPTIMIZE TABLE $wpdb->posts" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
366        }
367
368        // if we hit the max then schedule another run
369        if ( count( $post_ids ) >= $grunion_delete_limit ) {
370            wp_schedule_single_event( time() + 700, 'grunion_scheduled_delete' );
371        }
372    }
373
374    /**
375     * Deletes old temp feedback to keep the posts table size under control.
376     *
377     * @since 6.5.0
378     */
379    public static function grunion_delete_old_temp_feedback() {
380        global $wpdb;
381
382        $grunion_delete_limit = 100;
383
384        $now_gmt = current_time( 'mysql', true );
385        $sql     = $wpdb->prepare(
386            "
387            SELECT `ID`
388            FROM $wpdb->posts
389            WHERE DATE_SUB( %s, INTERVAL 1 DAY ) > `post_date_gmt`
390                AND `post_type` = 'feedback'
391                AND `post_status` = 'jp-temp-feedback'
392            LIMIT %d
393        ",
394            $now_gmt,
395            $grunion_delete_limit
396        );
397
398        // The SQL query is already prepared with $wpdb->prepare() above, and direct query is needed for performance-critical cleanup operation
399        $post_ids = $wpdb->get_col( $sql ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared,WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
400
401        foreach ( (array) $post_ids as $post_id ) {
402            // force a full delete, skip the trash
403            wp_delete_post( $post_id, true );
404        }
405
406        if (
407            /**
408             * Filter if the module run OPTIMIZE TABLE on the core WP tables.
409             *
410             * @module contact-form
411             *
412             * @since 6.5.0
413             *
414             * @param bool $filter Should Jetpack optimize the table, defaults to false.
415             */
416            apply_filters( 'grunion_optimize_table', false )
417        ) {
418            // OPTIMIZE TABLE is a MySQL-specific maintenance command that cannot be prepared and is only run when explicitly enabled via filter
419            $wpdb->query( "OPTIMIZE TABLE $wpdb->posts" ); // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching
420        }
421
422        // if we hit the max then schedule another run
423        if ( count( $post_ids ) >= $grunion_delete_limit ) {
424            wp_schedule_single_event( time() + 700, 'grunion_scheduled_delete_temp' );
425        }
426    }
427
428    /**
429     * Send an event to Tracks on form submission.
430     *
431     * @param int   $post_id - the post_id for the CPT that is created.
432     * @param array $all_values - array containing all form fields.
433     * @param array $extra_values - array containing extra form metadata.
434     *
435     * @return null|void
436     */
437    public static function jetpack_tracks_record_grunion_pre_message_sent( $post_id, $all_values = array(), $extra_values = array() ) {
438        $post = get_post( $post_id );
439        if ( $post ) {
440            $extra = gmdate( 'Y-W', strtotime( $post->post_date_gmt ) );
441        } else {
442            $extra = 'no-post';
443        }
444
445        /** This action is documented in jetpack/modules/widgets/social-media-icons.php */
446        do_action( 'jetpack_bump_stats_extras', 'jetpack_forms_message_sent', $extra );
447
448        $form_type = isset( $extra_values['widget'] ) ? 'widget' : 'block';
449
450        $context = '';
451        if ( isset( $extra_values['block_template'] ) ) {
452            $context = 'template';
453        } elseif ( isset( $extra_values['block_template_part'] ) ) {
454            $context = 'template_part';
455        }
456
457        $plugin = Contact_Form_Plugin::init();
458
459        $plugin->record_tracks_event(
460            'jetpack_forms_message_sent',
461            array(
462                'post_id'     => $post_id,
463                'form_type'   => $form_type,
464                'context'     => $context,
465                'has_consent' => empty( $all_values['email_marketing_consent'] ) ? 0 : 1,
466            )
467        );
468    }
469
470    /**
471     * Adds a given attribute to all instances of the Contact Form block.
472     *
473     * @param string $content  Existing content to process.
474     * @param array  $new_attr New attributes to add.
475     * @return string
476     */
477    public static function grunion_contact_form_apply_block_attribute( $content, $new_attr ) {
478        if ( ! is_string( $content ) ) {
479            // If the content is not a string, we cannot process it.
480            return $content;
481        }
482
483        if ( false === stripos( $content, 'wp:jetpack/contact-form' ) ) {
484            return $content;
485        }
486
487        // Parse blocks using WordPress core function.
488        $blocks = parse_blocks( $content );
489
490        // Recursively modify contact form blocks.
491        $modified_blocks = self::modify_contact_form_blocks_recursive( $blocks, $new_attr );
492
493        // Serialize back to block markup.
494        return serialize_blocks( $modified_blocks );
495    }
496
497    /**
498     * Recursively modifies contact form blocks to add new attributes.
499     *
500     * @param array $blocks    Array of parsed blocks.
501     * @param array $new_attr  New attributes to add.
502     * @return array Modified blocks array.
503     */
504    private static function modify_contact_form_blocks_recursive( $blocks, $new_attr ) {
505        foreach ( $blocks as &$block ) {
506            // Check if this is a contact form block.
507            if ( 'jetpack/contact-form' === $block['blockName'] ) {
508                // Merge new attributes with existing ones.
509                $block['attrs'] = array_merge(
510                    $block['attrs'] ?? array(),
511                    $new_attr
512                );
513            }
514
515            // Recursively process inner blocks.
516            if ( ! empty( $block['innerBlocks'] ) ) {
517                $block['innerBlocks'] = self::modify_contact_form_blocks_recursive(
518                    $block['innerBlocks'],
519                    $new_attr
520                );
521            }
522        }
523
524        return $blocks;
525    }
526
527    /**
528     * Get a filename for export tasks
529     *
530     * @param string $source The filtered source for exported data.
531     * @return string The filename without source nor date suffix.
532     */
533    public static function get_export_filename( $source = '' ) {
534        return $source === ''
535            ? sprintf(
536                /* translators: Site title, used to craft the export filename, eg "MySite - Jetpack Form Responses" */
537                __( '%s - Jetpack Form Responses', 'jetpack-forms' ),
538                sanitize_file_name( get_bloginfo( 'name' ) )
539            )
540            : sprintf(
541                /* translators: 1: Site title; 2: post title. Used to craft the export filename, eg "MySite - Jetpack Form Responses - Contact" */
542                __( '%1$s - Jetpack Form Responses - %2$s', 'jetpack-forms' ),
543                sanitize_file_name( get_bloginfo( 'name' ) ),
544                sanitize_file_name( html_entity_decode( $source, ENT_QUOTES | ENT_HTML5, 'UTF-8' ) )
545            );
546    }
547
548    /**
549     * Ensures a field label ends with a colon, unless it ends with a question mark.
550     *
551     * @param string $label The field label.
552     * @return string The formatted label.
553     */
554    public static function maybe_add_colon_to_label( $label ) {
555        $formatted_label = $label ? $label : '';
556        // Special case for the Terms consent field block which a period after the label.
557        $formatted_label = str_ends_with( $formatted_label, '?' ) ? $formatted_label : rtrim( $formatted_label, ':.' ) . ':';
558
559        return $formatted_label;
560    }
561}