PluginProbe
Jetpack – WP Security, Backup, Speed, & Growth / 16.2
Jetpack – WP Security, Backup, Speed, & Growth v16.2
16.3-a.5 16.3-a.7 16.3-a.3 16.3-a.1 16.2 16.2-beta 12.0.3 12.1.3 12.2.3 12.3.2 12.4.2 12.5.2 12.6.4 12.7.3 12.8.3 12.9.5 13.0.2 13.1.5 13.2.4 13.3.3 13.4.5 13.5.2 13.6.2 13.7.2 13.8.3 All 506 releases
jetpack / jetpack_vendor / automattic / jetpack-forms / src / contact-form / class-util.php

class-util.php in Jetpack – WP Security, Backup, Speed, & Growth 16.2, at jetpack_vendor/automattic/jetpack-forms/src/contact-form/class-util.php

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