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