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