PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 1.13.0
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v1.13.0
2.12.6 2.12.5 2.12.4 2.12.3 2.12.2 2.12.1 2.12.0 2.11.1 2.11.0 2.10.1 2.10.0 2.9.1 2.9.0 2.8.2 2.8.1 2.7.0 2.7.1 2.8.0 trunk 0.0.10 0.0.11 0.0.12 0.0.13 0.0.2 0.0.3 All 96 releases
sureforms / inc / helper.php
helper.php
1,932 lines 65.6 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Sureforms Submit Class file.
4 *
5 * @package sureforms.
6 * @since 0.0.1
7 */
8
9 namespace SRFM\Inc;
10
11 use SRFM\Inc\Database\Tables\Entries;
12 use SRFM\Inc\Traits\Get_Instance;
13 use WP_Error;
14 use WP_Post;
15
16 if ( ! defined( 'ABSPATH' ) ) {
17 exit; // Exit if accessed directly.
18 }
19
20 /**
21 * Sureforms Helper Class.
22 *
23 * @since 0.0.1
24 */
25 class Helper {
26 use Get_Instance;
27
28 /**
29 * Allowed HTML tags for SVG.
30 *
31 * @var array<string, array<string, bool>>
32 */
33 public static $allowed_tags_svg = [
34 'span' => [
35 'class' => true,
36 'aria-hidden' => true,
37 ],
38 'svg' => [
39 'xmlns' => true,
40 'width' => true,
41 'height' => true,
42 'viewBox' => true,
43 'fill' => true,
44 ],
45 'path' => [
46 'd' => true,
47 'stroke' => true,
48 'stroke-opacity' => true,
49 'stroke-width' => true,
50 'stroke-linecap' => true,
51 'stroke-linejoin' => true,
52 ],
53 ];
54
55 /**
56 * Sureforms SVGs.
57 *
58 * @var mixed srfm_svgs
59 */
60 private static $srfm_svgs = null;
61
62 /**
63 * Get common error message.
64 *
65 * @since 0.0.2
66 * @return array<string>
67 */
68 public static function get_common_err_msg() {
69 return [
70 'required' => __( 'This field is required.', 'sureforms' ),
71 'unique' => __( 'Value needs to be unique.', 'sureforms' ),
72 ];
73 }
74
75 /**
76 * Convert a file URL to a file path.
77 *
78 * @param string $file_url The URL of the file.
79 *
80 * @since 1.3.0
81 * @return string The file path.
82 */
83 public static function convert_fileurl_to_filepath( $file_url ) {
84 static $upload_dir = null;
85 if ( ! $upload_dir ) {
86 // Internally cache the upload directory.
87 $upload_dir = wp_get_upload_dir();
88 }
89 return wp_normalize_path( str_replace( $upload_dir['baseurl'], $upload_dir['basedir'], $file_url ) );
90 }
91
92 /**
93 * Checks if current value is string or else returns default value
94 *
95 * @param mixed $data data which need to be checked if is string.
96 *
97 * @since 0.0.1
98 * @return string
99 */
100 public static function get_string_value( $data ) {
101 if ( is_scalar( $data ) ) {
102 return (string) $data;
103 }
104 if ( is_object( $data ) && method_exists( $data, '__toString' ) ) {
105 return $data->__toString();
106 }
107 if ( is_null( $data ) ) {
108 return '';
109 }
110 return '';
111 }
112 /**
113 * Checks if current value is number or else returns default value
114 *
115 * @param mixed $value data which need to be checked if is string.
116 * @param int $base value can be set is $data is not a string, defaults to empty string.
117 *
118 * @since 0.0.1
119 * @return int
120 */
121 public static function get_integer_value( $value, $base = 10 ) {
122 if ( is_numeric( $value ) ) {
123 return (int) $value;
124 }
125 if ( is_string( $value ) ) {
126 $trimmed_value = trim( $value );
127 return intval( $trimmed_value, $base );
128 }
129 return 0;
130 }
131
132 /**
133 * Checks if current value is an array or else returns default value
134 *
135 * @param mixed $data Data which needs to be checked if it is an array.
136 *
137 * @since 0.0.3
138 * @return array
139 */
140 public static function get_array_value( $data ) {
141 if ( is_array( $data ) ) {
142 return $data;
143 }
144 if ( is_null( $data ) ) {
145 return [];
146 }
147 return (array) $data;
148 }
149
150 /**
151 * Extracts the field type from the dynamic field key ( or field slug ).
152 *
153 * @param string $field_key Dynamic field key.
154 * @since 0.0.6
155 * @return string Extracted field type.
156 */
157 public static function get_field_type_from_key( $field_key ) {
158
159 if ( false === strpos( $field_key, '-lbl-' ) ) {
160 return '';
161 }
162
163 return trim( explode( '-', $field_key )[1] );
164 }
165
166 /**
167 * Extracts the field label from the dynamic field key ( or field slug ).
168 *
169 * @param string $field_key Dynamic field key.
170 * @since 1.1.1
171 * @return string Extracted field label.
172 */
173 public static function get_field_label_from_key( $field_key ) {
174 if ( false === strpos( $field_key, '-lbl-' ) ) {
175 return '';
176 }
177
178 $label = explode( '-lbl-', $field_key )[1];
179 // Getting the encrypted label. we are removing the block slug here.
180 $label = explode( '-', $label )[0];
181
182 return $label ? html_entity_decode( self::decrypt( $label ) ) : '';
183 }
184
185 /**
186 * Extracts the block ID from the dynamic field key ( or field slug ).
187 *
188 * @param string $field_key Dynamic field key.
189 * @since 1.6.1
190 * @return string Extracted block ID.
191 */
192 public static function get_block_id_from_key( $field_key ) {
193 // Check if the key contains the block ID identifier.
194 if ( strpos( $field_key, 'srfm-' ) === 0 && strpos( $field_key, '-lbl-' ) === false ) {
195 return ''; // Return empty if the key format is invalid.
196 }
197
198 $parts = explode( '-lbl-', $field_key );
199 if ( isset( $parts[0] ) ) {
200 $block_id = explode( '-', $parts[0] );
201 if ( is_array( $block_id ) && ! empty( $block_id ) ) {
202 return end( $block_id );
203 }
204 }
205 return '';
206 }
207
208 /**
209 * Returns the proper sanitize callback functions according to the field type.
210 *
211 * @param string $field_type HTML field type.
212 * @since 0.0.6
213 * @return callable Returns sanitize callbacks according to the provided field type.
214 */
215 public static function get_field_type_sanitize_function( $field_type ) {
216 $callbacks = apply_filters(
217 'srfm_field_type_sanitize_functions',
218 [
219 'url' => 'esc_url_raw',
220 'input' => 'sanitize_text_field',
221 'number' => [ self::class, 'sanitize_number' ],
222 'email' => 'sanitize_email',
223 'textarea' => [ self::class, 'sanitize_textarea' ],
224 ]
225 );
226
227 return $callbacks[ $field_type ] ?? 'sanitize_text_field';
228 }
229
230 /**
231 * Sanitizes a numeric value.
232 *
233 * This function checks if the input value is numeric. If it is numeric, it sanitizes
234 * the value to ensure it's a float or integer, allowing for fractions and thousand separators.
235 * If the value is not numeric, it sanitizes it as a text field.
236 *
237 * @param mixed $value The value to be sanitized.
238 * @since 0.0.6
239 * @return int|float|string The sanitized value.
240 */
241 public static function sanitize_number( $value ) {
242 if ( ! is_numeric( $value ) ) {
243 // phpcs:ignore /** @phpstan-ignore-next-line */
244 return sanitize_text_field( $value ); // If it is not numeric, then let user get some sanitized data to view.
245 }
246
247 // phpcs:ignore /** @phpstan-ignore-next-line */
248 return sanitize_text_field( filter_var( $value, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION | FILTER_FLAG_ALLOW_THOUSAND ) );
249 }
250
251 /**
252 * This function sanitizes the submitted form data according to the field type.
253 *
254 * @param array<mixed> $form_data $form_data User submitted form data.
255 * @since 0.0.6
256 * @return array<mixed> $result Sanitized form data.
257 */
258 public static function sanitize_by_field_type( $form_data ) {
259 $result = [];
260
261 if ( empty( $form_data ) || ! is_array( $form_data ) ) {
262 return $result;
263 }
264
265 foreach ( $form_data as $field_key => &$value ) {
266 $field_type = self::get_field_type_from_key( $field_key );
267 $sanitize_function = self::get_field_type_sanitize_function( $field_type );
268 $sanitized_data = is_array( $value ) ? self::sanitize_by_field_type( $value ) : call_user_func( $sanitize_function, $value );
269
270 $result[ $field_key ] = $sanitized_data;
271 }
272
273 return $result;
274 }
275
276 /**
277 * This function performs array_map for multi dimensional array
278 *
279 * @param string $function function name to be applied on each element on array.
280 * @param array<mixed> $data_array array on which function needs to be performed.
281 * @return array<mixed>
282 * @since 0.0.1
283 */
284 public static function sanitize_recursively( $function, $data_array ) {
285 $response = [];
286 if ( is_array( $data_array ) ) {
287 if ( ! is_callable( $function ) ) {
288 return $data_array;
289 }
290 foreach ( $data_array as $key => $data ) {
291 $val = is_array( $data ) ? self::sanitize_recursively( $function, $data ) : $function( $data );
292 $response[ $key ] = $val;
293 }
294 }
295
296 return $response;
297 }
298
299 /**
300 * Generates common markup liked label, etc
301 *
302 * @param int|string $form_id form id.
303 * @param string $type Type of form markup.
304 * @param string $label Label for the form markup.
305 * @param string $slug Slug for the form markup.
306 * @param string $block_id Block id for the form markup.
307 * @param bool $required If field is required or not.
308 * @param string $help Help for the form markup.
309 * @param string $error_msg Error message for the form markup.
310 * @param bool $is_unique Check if the field is unique.
311 * @param string $duplicate_msg Duplicate message for field.
312 * @param bool $override Override for error markup.
313 * @return string
314 * @since 0.0.1
315 */
316 public static function generate_common_form_markup( $form_id, $type, $label = '', $slug = '', $block_id = '', $required = false, $help = '', $error_msg = '', $is_unique = false, $duplicate_msg = '', $override = false ) {
317 $duplicate_msg = $duplicate_msg ? ' data-unique-msg="' . esc_attr( $duplicate_msg ) . '"' : '';
318
319 $markup = '';
320 $show_labels_as_placeholder = get_post_meta( self::get_integer_value( $form_id ), '_srfm_use_label_as_placeholder', true );
321 $show_labels_as_placeholder = $show_labels_as_placeholder ? self::get_string_value( $show_labels_as_placeholder ) : false;
322
323 $required_sign = apply_filters( 'srfm_value_after_label_placeholder', ' *' );
324
325 if ( ! is_string( $required_sign ) ) {
326 $required_sign = ' *';
327 }
328
329 switch ( $type ) {
330 case 'label':
331 if ( $label ) {
332 ob_start();
333 ?>
334 <label id="srfm-label-<?php echo esc_attr( $block_id ); ?>" for="srfm-<?php echo esc_attr( $slug ); ?>-<?php echo esc_attr( $block_id ); ?>" class="srfm-block-label">
335 <?php echo wp_kses_post( $label ); ?>
336 <?php if ( $required ) { ?>
337 <span class="srfm-required" aria-hidden="true"> *</span>
338 <?php } ?>
339 </label>
340 <?php
341 $markup = ob_get_clean();
342 }
343 break;
344 case 'help':
345 if ( $help ) {
346 ob_start();
347 ?>
348 <div class="srfm-description" id="srfm-description-<?php echo esc_attr( $block_id ); ?>">
349 <?php echo wp_kses_post( $help ); ?>
350 </div>
351 <?php
352 $markup = ob_get_clean();
353 }
354 break;
355 case 'error':
356 if ( $required || $override ) {
357 ob_start();
358 ?>
359 <div class="srfm-error-message" data-srfm-id="srfm-error-<?php echo esc_attr( $block_id ); ?>" data-error-msg="<?php echo esc_attr( $error_msg ); ?>"<?php echo $duplicate_msg; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
360 <?php echo esc_html( $error_msg ); ?>
361 </div>
362 <?php
363 $markup = ob_get_clean();
364 }
365 break;
366 case 'is_unique':
367 if ( $is_unique ) {
368 ob_start();
369 ?>
370 <div class="srfm-error">
371 <?php echo esc_html( $duplicate_msg ); ?>
372 </div>
373 <?php
374 $markup = ob_get_clean();
375 }
376 break;
377 case 'placeholder':
378 $markup = $label && '1' === $show_labels_as_placeholder ? wp_kses_post( $label ) . ( $required ? esc_attr( $required_sign ) : '' ) : '';
379 break;
380 case 'label_text':
381 // This has been added for generating label text for the form markup instead of adding it in the label tag.
382 if ( $label ) {
383 ob_start();
384 ?>
385 <?php echo wp_kses_post( $label ); ?>
386 <?php if ( $required ) { ?>
387 <span class="srfm-required" aria-hidden="true"> *</span>
388 <?php } ?>
389 <?php
390 $markup = ob_get_clean();
391 }
392 break;
393 default:
394 $markup = '';
395 }
396
397 return is_string( $markup ) ? $markup : '';
398 }
399
400 /**
401 * Get an SVG Icon
402 *
403 * @since 0.0.1
404 * @param string $icon the icon name.
405 * @param string $class if the baseline class should be added.
406 * @param string $html Custom attributes inside svg wrapper.
407 * @return string
408 */
409 public static function fetch_svg( $icon = '', $class = '', $html = '' ) {
410 $class = $class ? ' ' . $class : '';
411
412 if ( ! self::$srfm_svgs ) {
413 ob_start();
414
415 include_once SRFM_DIR . 'assets/svg/svgs.json';
416 self::$srfm_svgs = json_decode( self::get_string_value( ob_get_clean() ), true );
417 self::$srfm_svgs = apply_filters( 'srfm_svg_icons', self::$srfm_svgs );
418 }
419
420 ob_start();
421 ?>
422 <span class="srfm-icon<?php echo esc_attr( $class ); ?>" <?php echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
423 <?php echo self::$srfm_svgs[ $icon ] ?? ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
424 </span>
425 <?php
426 $output = ob_get_clean();
427 return is_string( $output ) ? $output : '';
428 }
429
430 /**
431 * Encrypt data using base64.
432 *
433 * @param string $input The input string which needs to be encrypted.
434 * @since 0.0.1
435 * @return string The encrypted string.
436 */
437 public static function encrypt( $input ) {
438 // If the input is empty or not a string, then abandon ship.
439 if ( empty( $input ) || ! is_string( $input ) ) {
440 return '';
441 }
442
443 // Encrypt the input and return it.
444 $base_64 = base64_encode( $input ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
445 return rtrim( $base_64, '=' );
446 }
447
448 /**
449 * Decrypt data using base64.
450 *
451 * @param string $input The input string which needs to be decrypted.
452 * @since 0.0.1
453 * @return string The decrypted string.
454 */
455 public static function decrypt( $input ) {
456 // If the input is empty or not a string, then abandon ship.
457 if ( empty( $input ) || ! is_string( $input ) ) {
458 return '';
459 }
460
461 // Decrypt the input and return it.
462 $base_64 = $input . str_repeat( '=', strlen( $input ) % 4 );
463 return base64_decode( $base_64 ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
464 }
465
466 /**
467 * Update an option from the database.
468 *
469 * @param string $key The option key.
470 * @param mixed $value The value to update.
471 * @param bool $network_override Whether to allow the network_override admin setting to be overridden on subsites.
472 * @since 0.0.1
473 * @return bool True if the option was updated, false otherwise.
474 */
475 public static function update_admin_settings_option( $key, $value, $network_override = false ) {
476 // Update the site-wide option if we're in the network admin, and return the updated status.
477 return $network_override && is_multisite() ? update_site_option( $key, $value ) : update_option( $key, $value );
478 }
479
480 /**
481 * Update an option from the database.
482 *
483 * @param int|string $post_id post id / form id.
484 * @param string $key meta key name.
485 * @param bool $single single or multiple.
486 * @param mixed $default default value.
487 *
488 * @since 0.0.1
489 * @return string Meta value.
490 */
491 public static function get_meta_value( $post_id, $key, $single = true, $default = '' ) {
492 $srfm_live_mode_data = self::get_instant_form_live_data();
493
494 if ( isset( $srfm_live_mode_data[ $key ] ) ) {
495 // Give priority to live mode data if we have one set from the Instant Form.
496 return self::get_string_value( $srfm_live_mode_data[ $key ] );
497 }
498
499 return get_post_meta( self::get_integer_value( $post_id ), $key, $single ) ? self::get_string_value( get_post_meta( self::get_integer_value( $post_id ), $key, $single ) ) : self::get_string_value( $default );
500 }
501
502 /**
503 * Wrapper for the WordPress's get_post_meta function with the support for default values.
504 *
505 * @param int|string $post_id Post ID.
506 * @param string $key The meta key to retrieve.
507 * @param mixed $default Default value.
508 * @param bool $single Optional. Whether to return a single value.
509 * @since 0.0.8
510 * @return mixed Meta value.
511 */
512 public static function get_post_meta( $post_id, $key, $default = null, $single = true ) {
513 $meta_value = get_post_meta( self::get_integer_value( $post_id ), $key, $single );
514 return $meta_value ? $meta_value : $default;
515 }
516
517 /**
518 * Returns query params data for instant form live preview.
519 *
520 * @since 0.0.8
521 * @return array<mixed> Live preview data.
522 */
523 public static function get_instant_form_live_data() {
524 $srfm_live_mode_data = isset( $_GET['live_mode'] ) && self::current_user_can() ? self::sanitize_recursively( 'sanitize_text_field', wp_unslash( $_GET ) ) : []; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not needed here.
525
526 return $srfm_live_mode_data ? array_map(
527 // Normalize falsy values.
528 static function( $live_data ) {
529 return 'false' === $live_data ? false : $live_data;
530 },
531 $srfm_live_mode_data
532 ) : [];
533 }
534
535 /**
536 * Default dynamic block value.
537 *
538 * @since 0.0.1
539 * @return array<string> Meta value.
540 */
541 public static function default_dynamic_block_option() {
542
543 $common_err_msg = self::get_common_err_msg();
544
545 $default_values = [
546 'srfm_url_block_required_text' => $common_err_msg['required'],
547 'srfm_input_block_required_text' => $common_err_msg['required'],
548 'srfm_input_block_unique_text' => $common_err_msg['unique'],
549 'srfm_address_block_required_text' => $common_err_msg['required'],
550 'srfm_phone_block_required_text' => $common_err_msg['required'],
551 'srfm_phone_block_unique_text' => $common_err_msg['unique'],
552 'srfm_number_block_required_text' => $common_err_msg['required'],
553 'srfm_textarea_block_required_text' => $common_err_msg['required'],
554 'srfm_multi_choice_block_required_text' => $common_err_msg['required'],
555 'srfm_checkbox_block_required_text' => $common_err_msg['required'],
556 'srfm_gdpr_block_required_text' => $common_err_msg['required'],
557 'srfm_email_block_required_text' => $common_err_msg['required'],
558 'srfm_email_block_unique_text' => $common_err_msg['unique'],
559 'srfm_dropdown_block_required_text' => $common_err_msg['required'],
560 'srfm_rating_block_required_text' => $common_err_msg['required'],
561 ];
562
563 $default_values = array_merge( $default_values, Translatable::dynamic_validation_messages() );
564
565 return apply_filters( 'srfm_default_dynamic_block_option', $default_values, $common_err_msg );
566 }
567
568 /**
569 * Get default dynamic block value.
570 *
571 * @param string $key meta key name.
572 * @since 0.0.1
573 * @return string Meta value.
574 */
575 public static function get_default_dynamic_block_option( $key ) {
576 $default_dynamic_values = self::default_dynamic_block_option();
577 $option = get_option( 'srfm_default_dynamic_block_option', $default_dynamic_values );
578
579 if ( is_array( $option ) && array_key_exists( $key, $option ) ) {
580 return $option[ $key ];
581 }
582 return '';
583 }
584
585 /**
586 * Checks whether a given request has appropriate permissions.
587 *
588 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
589 * @since 0.0.1
590 */
591 public static function get_items_permissions_check() {
592 if ( self::current_user_can() ) {
593 return true;
594 }
595
596 return new WP_Error(
597 'rest_cannot_view',
598 __( 'Sorry, you are not allowed to perform this action.', 'sureforms' ),
599 [ 'status' => \rest_authorization_required_code() ]
600 );
601 }
602
603 /**
604 * Check if the current user has a given capability.
605 *
606 * @param string $capability The capability to check.
607 * @param array<mixed> $args Optional. Additional arguments to pass to the capability check.
608 *
609 * @since 0.0.3
610 * @return bool Whether the current user has the given capability or role.
611 */
612 public static function current_user_can( $capability = '', $args = [] ) {
613 if ( ! function_exists( 'current_user_can' ) ) {
614 return false;
615 }
616
617 if ( ! is_string( $capability ) || empty( $capability ) ) {
618 $capability = 'manage_options';
619 }
620
621 return ! empty( $args ) && is_array( $args ) && count( $args ) > 0
622 ? current_user_can( $capability, ...$args )
623 : current_user_can( $capability );
624 }
625
626 /**
627 * Get all the entries for the given form ids. The entries are older than the given days_old.
628 *
629 * @param int $days_old The number of days old the entries should be.
630 * @param array<int> $sf_form_ids The form ids for which the entries need to be fetched.
631 * @since 0.0.2
632 * @return array<mixed> the entries matching the criteria.
633 */
634 public static function get_entries_from_form_ids( $days_old = 0, $sf_form_ids = [] ) {
635
636 $entries = [];
637 $days_old_date = ( new \DateTime() )->modify( "-{$days_old} days" )->format( 'Y-m-d H:i:s' );
638
639 foreach ( $sf_form_ids as $form_id ) {
640 // args according to the get_all() function in the Entries class.
641 $args = [
642 'where' => [
643 [
644 [
645 'key' => 'form_id',
646 'value' => $form_id,
647 'compare' => '=',
648 ],
649 [
650 'key' => 'created_at',
651 'value' => $days_old_date,
652 'compare' => '<=',
653 ],
654 ],
655 ],
656 ];
657
658 // store all the entries in a single array.
659 $entries = array_merge( $entries, Entries::get_all( $args, false ) );
660 }
661 return $entries;
662 }
663
664 /**
665 * Decode block attributes.
666 * The function reverses the effect of serialize_block_attributes()
667 *
668 * @link https://developer.wordpress.org/reference/functions/serialize_block_attributes/
669 * @param string $encoded_data the encoded block attribute.
670 * @since 0.0.2
671 * @return string decoded block attribute
672 */
673 public static function decode_block_attribute( $encoded_data = '' ) {
674 $decoded_data = preg_replace( '/\\\\u002d\\\\u002d/', '--', self::get_string_value( $encoded_data ) );
675 $decoded_data = preg_replace( '/\\\\u003c/', '<', self::get_string_value( $decoded_data ) );
676 $decoded_data = preg_replace( '/\\\\u003e/', '>', self::get_string_value( $decoded_data ) );
677 $decoded_data = preg_replace( '/\\\\u0026/', '&', self::get_string_value( $decoded_data ) );
678 $decoded_data = preg_replace( '/\\\\\\\\"/', '"', self::get_string_value( $decoded_data ) );
679 return self::get_string_value( $decoded_data );
680 }
681
682 /**
683 * Map slugs to submission data.
684 *
685 * @param array<mixed> $submission_data submission_data.
686 * @since 0.0.3
687 * @return array<mixed>
688 */
689 public static function map_slug_to_submission_data( $submission_data = [] ) {
690 $mapped_data = [];
691 foreach ( $submission_data as $key => $value ) {
692 if ( false === strpos( $key, '-lbl-' ) ) {
693 continue;
694 }
695 $label = explode( '-lbl-', $key )[1];
696 $slug = implode( '-', array_slice( explode( '-', $label ), 1 ) );
697
698 // Check if value is array to handle external package field functionality.
699 // like repeater fields that need special processing.
700 if ( is_array( $value ) && ! empty( $value ) ) {
701 // Apply filter to allow external packages to process array values.
702 // Returns processed data with 'is_processed' flag if successfully handled.
703 $filtered_submission_data = apply_filters(
704 'srfm_map_slug_to_submission_data_array',
705 [
706 'value' => $value,
707 'key' => $key,
708 'slug' => $slug,
709 ]
710 );
711 if ( isset( $filtered_submission_data['is_processed'] ) && true === $filtered_submission_data['is_processed'] ) {
712 $mapped_data[ $slug ] = $filtered_submission_data['value'];
713 continue;
714 }
715 }
716
717 $mapped_data[ $slug ] = is_string( $value ) ? html_entity_decode( esc_attr( $value ) ) : $value;
718 }
719 return $mapped_data;
720 }
721
722 /**
723 * Get forms options. Shows all the available forms in the dropdown.
724 *
725 * @since 0.0.5
726 * @param string $key Determines the type of data to return.
727 * @return array<mixed>
728 */
729 public static function get_sureforms( $key = '' ) {
730 $forms = get_posts(
731 apply_filters(
732 'srfm_get_sureforms_query_args',
733 [
734 'post_type' => SRFM_FORMS_POST_TYPE,
735 'posts_per_page' => -1,
736 'post_status' => 'publish',
737 ]
738 )
739 );
740
741 $options = [];
742
743 foreach ( $forms as $form ) {
744 if ( $form instanceof WP_Post ) {
745 if ( 'all' === $key ) {
746 $options[ $form->ID ] = $form;
747 } elseif ( ! empty( $key ) && is_string( $key ) && isset( $form->$key ) ) {
748 $options[ $form->ID ] = $form->$key;
749 } else {
750 $options[ $form->ID ] = $form->post_title;
751 }
752 }
753 }
754
755 return $options;
756 }
757
758 /**
759 * Get all the forms.
760 *
761 * @since 0.0.5
762 * @return array<mixed>
763 */
764 public static function get_sureforms_title_with_ids() {
765 $form_options = self::get_sureforms();
766
767 foreach ( $form_options as $key => $value ) {
768 $form_options[ $key ] = $value . ' #' . $key;
769 }
770
771 return $form_options;
772 }
773
774 /**
775 * Get the CSS variables based on different field spacing sizes.
776 *
777 * @param string|null $field_spacing The field spacing size or boolean false to return complete sizes array.
778 *
779 * @since 0.0.7
780 * @return array<string|mixed>
781 */
782 public static function get_css_vars( $field_spacing = null ) {
783 /**
784 * $sizes - Field Spacing Sizes Variables.
785 * The array contains the CSS variables for different field spacing sizes.
786 * Each key corresponds to the field spacing size, and the value is an array of CSS variables.
787 *
788 * For future variables depending on the field spacing size, add the variable to the array respectively.
789 */
790 $sizes = apply_filters(
791 'srfm_css_vars_sizes',
792 [
793 'small' => [
794 '--srfm-row-gap-between-blocks' => '16px',
795 // Address block gap and spacing variables.
796 '--srfm-address-label-font-size' => '14px',
797 '--srfm-address-label-line-height' => '20px',
798 '--srfm-address-description-font-size' => '12px',
799 '--srfm-address-description-line-height' => '16px',
800 '--srfm-col-gap-between-fields' => '12px',
801 '--srfm-row-gap-between-fields' => '12px',
802 '--srfm-gap-below-address-label' => '12px',
803 // Dropdown Variables.
804 '--srfm-dropdown-font-size' => '14px',
805 '--srfm-dropdown-gap-between-input-menu' => '4px',
806 '--srfm-dropdown-badge-padding' => '2px 6px',
807 '--srfm-dropdown-multiselect-font-size' => '12px',
808 '--srfm-dropdown-multiselect-line-height' => '16px',
809 '--srfm-dropdown-padding-right' => '12px',
810 // initial padding and from 20px - 12px for dropdown arrow width and 8px for gap before dropdown arrow.
811 '--srfm-dropdown-padding-right-icon' => 'calc( var( --srfm-dropdown-padding-right ) + 20px )',
812 '--srfm-dropdown-multiselect-padding' => '8px var( --srfm-dropdown-padding-right-icon ) 8px 8px',
813 // Input Field Variables.
814 '--srfm-input-height' => '40px',
815 '--srfm-input-field-padding' => '10px 12px',
816 '--srfm-input-field-font-size' => '14px',
817 '--srfm-input-field-line-height' => '20px',
818 '--srfm-input-field-margin-top' => '4px',
819 '--srfm-input-field-margin-bottom' => '4px',
820 // Checkbox and GDPR Variables.
821 '--srfm-checkbox-label-font-size' => '14px',
822 '--srfm-checkbox-label-line-height' => '20px',
823 '--srfm-checkbox-description-font-size' => '12px',
824 '--srfm-checkbox-description-line-height' => '16px',
825 '--srfm-check-ctn-width' => '16px',
826 '--srfm-check-ctn-height' => '16px',
827 '--srfm-check-svg-size' => '10px',
828 '--srfm-checkbox-margin-top-frontend' => '2px',
829 '--srfm-checkbox-margin-top-editor' => '3px',
830 '--srfm-check-gap' => '8px',
831 '--srfm-checkbox-description-margin-left' => '24px',
832 // Phone Number field variables.
833 '--srfm-flag-section-padding' => '10px 0 10px 12px',
834 '--srfm-gap-between-icon-text' => '8px',
835 // Label Variables.
836 '--srfm-label-font-size' => '14px',
837 '--srfm-label-line-height' => '20px',
838 // Description Variables.
839 '--srfm-description-font-size' => '12px',
840 '--srfm-description-line-height' => '16px',
841 // Button Variables.
842 '--srfm-btn-padding' => '8px 14px',
843 '--srfm-btn-font-size' => '14px',
844 '--srfm-btn-line-height' => '20px',
845 // Multi Choice Variables.
846 '--srfm-multi-choice-horizontal-padding' => '16px',
847 '--srfm-multi-choice-vertical-padding' => '16px',
848 '--srfm-multi-choice-internal-option-gap' => '8px',
849 '--srfm-multi-choice-vertical-svg-size' => '32px',
850 '--srfm-multi-choice-horizontal-image-size' => '20px',
851 '--srfm-multi-choice-vertical-image-size' => '100px',
852 '--srfm-multi-choice-outer-padding' => '0',
853 ],
854 'medium' => [
855 '--srfm-row-gap-between-blocks' => '18px',
856 // Address block gap and spacing variables.
857 '--srfm-address-label-font-size' => '16px',
858 '--srfm-address-label-line-height' => '24px',
859 '--srfm-address-description-font-size' => '14px',
860 '--srfm-address-description-line-height' => '20px',
861 '--srfm-col-gap-between-fields' => '16px',
862 '--srfm-row-gap-between-fields' => '16px',
863 '--srfm-gap-below-address-label' => '14px',
864 // Input Field Variables.
865 '--srfm-input-height' => '44px',
866 '--srfm-input-field-font-size' => '16px',
867 '--srfm-input-field-line-height' => '24px',
868 '--srfm-input-field-margin-top' => '6px',
869 '--srfm-input-field-margin-bottom' => '6px',
870 // Checkbox and GDPR Variables.
871 '--srfm-checkbox-label-font-size' => '16px',
872 '--srfm-checkbox-label-line-height' => '24px',
873 '--srfm-checkbox-description-font-size' => '14px',
874 '--srfm-checkbox-description-line-height' => '20px',
875 '--srfm-checkbox-margin-top-frontend' => '4px',
876 '--srfm-checkbox-margin-top-editor' => '6px',
877 '--srfm-checkbox-description-margin-left' => '24px',
878 // Label Variables.
879 '--srfm-label-font-size' => '16px',
880 '--srfm-label-line-height' => '24px',
881 // Description Variables.
882 '--srfm-description-font-size' => '14px',
883 '--srfm-description-line-height' => '20px',
884 // Button Variables.
885 '--srfm-btn-padding' => '10px 14px',
886 '--srfm-btn-font-size' => '16px',
887 '--srfm-btn-line-height' => '24px',
888 // Multi Choice Variables.
889 '--srfm-multi-choice-horizontal-padding' => '20px',
890 '--srfm-multi-choice-vertical-padding' => '20px',
891 '--srfm-multi-choice-vertical-svg-size' => '40px',
892 '--srfm-multi-choice-horizontal-image-size' => '24px',
893 '--srfm-multi-choice-vertical-image-size' => '120px',
894 '--srfm-multi-choice-outer-padding' => '2px',
895 ],
896 'large' => [
897 '--srfm-row-gap-between-blocks' => '20px',
898 // Address Block Gap and Spacing Variables.
899 '--srfm-address-label-font-size' => '18px',
900 '--srfm-address-label-line-height' => '28px',
901 '--srfm-address-description-font-size' => '16px',
902 '--srfm-address-description-line-height' => '24px',
903 '--srfm-col-gap-between-fields' => '16px',
904 '--srfm-row-gap-between-fields' => '20px',
905 '--srfm-gap-below-address-label' => '16px',
906 // Dropdown Variables.
907 '--srfm-dropdown-font-size' => '16px',
908 '--srfm-dropdown-gap-between-input-menu' => '6px',
909 '--srfm-dropdown-badge-padding' => '6px 6px',
910 '--srfm-dropdown-multiselect-font-size' => '14px',
911 '--srfm-dropdown-multiselect-line-height' => '20px',
912 '--srfm-dropdown-padding-right' => '14px',
913 // Input Field Variables.
914 '--srfm-input-height' => '48px',
915 '--srfm-input-field-padding' => '10px 14px',
916 '--srfm-input-field-font-size' => '18px',
917 '--srfm-input-field-line-height' => '28px',
918 '--srfm-input-field-margin-top' => '8px',
919 '--srfm-input-field-margin-bottom' => '8px',
920 // Checkbox and GDPR Variables.
921 '--srfm-checkbox-label-font-size' => '18px',
922 '--srfm-checkbox-label-line-height' => '28px',
923 '--srfm-checkbox-description-font-size' => '16px',
924 '--srfm-checkbox-description-line-height' => '24px',
925 '--srfm-check-ctn-width' => '20px',
926 '--srfm-check-ctn-height' => '20px',
927 '--srfm-check-svg-size' => '14px',
928 '--srfm-check-gap' => '10px',
929 '--srfm-checkbox-margin-top-frontend' => '4px',
930 '--srfm-checkbox-margin-top-editor' => '5px',
931 '--srfm-checkbox-description-margin-left' => '30px',
932 // Label Variables.
933 '--srfm-label-font-size' => '18px',
934 '--srfm-label-line-height' => '28px',
935 // Description Variables.
936 '--srfm-description-font-size' => '16px',
937 '--srfm-description-line-height' => '24px',
938 // Button Variables.
939 '--srfm-btn-padding' => '10px 14px',
940 '--srfm-btn-font-size' => '18px',
941 '--srfm-btn-line-height' => '28px',
942 // Multi Choice Variables.
943 '--srfm-multi-choice-horizontal-padding' => '24px',
944 '--srfm-multi-choice-vertical-padding' => '24px',
945 '--srfm-multi-choice-internal-option-gap' => '12px',
946 '--srfm-multi-choice-vertical-svg-size' => '48px',
947 '--srfm-multi-choice-horizontal-image-size' => '28px',
948 '--srfm-multi-choice-vertical-image-size' => '140px',
949 '--srfm-multi-choice-outer-padding' => '4px',
950 ],
951 ]
952 );
953 // Return complete sizes array if field_spacing is false. Required in case of JS for Editor changes.
954 if ( ! $field_spacing ) {
955 return $sizes;
956 }
957
958 $selected_size = $sizes['small'];
959 if ( 'small' !== $field_spacing && isset( $sizes[ $field_spacing ] ) ) {
960 $selected_size = array_merge( $selected_size, $sizes[ $field_spacing ] );
961 }
962
963 return $selected_size;
964 }
965
966 /**
967 * Array of SureForms blocks which get have user input.
968 *
969 * @since 0.0.10
970 * @return array<string>
971 */
972 public static function get_sureforms_blocks() {
973 return apply_filters(
974 'srfm_blocks',
975 [
976 'srfm/input',
977 'srfm/email',
978 'srfm/textarea',
979 'srfm/number',
980 'srfm/checkbox',
981 'srfm/gdpr',
982 'srfm/phone',
983 'srfm/address',
984 'srfm/dropdown',
985 'srfm/multi-choice',
986 'srfm/radio',
987 'srfm/submit',
988 'srfm/url',
989 ]
990 );
991 }
992
993 /**
994 * Render a site key missing error message.
995 *
996 * @param string $provider_name Name of the captcha provider (e.g., HCaptcha, Google reCAPTCHA, Turnstile).
997 * @since 1.7.0
998 * @since 1.7.1 moved to inc/helper.php from inc/generate-form-markup.php
999 * @return void
1000 */
1001 public static function render_missing_sitekey_error( $provider_name ) {
1002 $icon = self::fetch_svg( 'info_circle', '', 'aria-hidden="true"' );
1003 ?>
1004 <p id="sitekey-error" class="srfm-common-error-message srfm-error-message">
1005 <?php echo wp_kses( $icon, self::$allowed_tags_svg ); ?>
1006 <span class="srfm-error-content">
1007 <?php
1008 echo esc_html(
1009 sprintf(
1010 /* translators: %s: Provider name like HCaptcha, Google reCAPTCHA, Turnstile */
1011 __( '%s sitekey is missing. Please contact your site administrator.', 'sureforms' ),
1012 $provider_name
1013 )
1014 );
1015 ?>
1016 </span>
1017 </p>
1018 <?php
1019 }
1020
1021 /**
1022 * Process blocks and inner blocks.
1023 *
1024 * @param array<mixed> $blocks The block data.
1025 * @param array<string> $slugs The array of existing slugs.
1026 * @param bool $updated The array of existing slugs.
1027 * @param string $prefix The array of existing slugs.
1028 * @param bool $skip_checking_existing_slug Skips the checking of existing slug if passed true. More information documented inside this function.
1029 * @since 0.0.10
1030 * @return array
1031 */
1032 public static function process_blocks( $blocks, &$slugs, &$updated, $prefix = '', $skip_checking_existing_slug = false ) {
1033
1034 if ( ! is_array( $blocks ) ) {
1035 return [ $blocks, $slugs, $updated ];
1036 }
1037
1038 foreach ( $blocks as $index => $block ) {
1039
1040 if ( ! is_array( $block ) ) {
1041 continue;
1042 }
1043 // Checking only for SureForms blocks which can have user input.
1044 if ( empty( $block['blockName'] ) || ! in_array( $block['blockName'], self::get_sureforms_blocks(), true ) ) {
1045 continue;
1046 }
1047
1048 /**
1049 * Lets continue if slug already exists.
1050 * This will ensure that we don't update already existing slugs.
1051 */
1052 if ( isset( $block['attrs'] ) && ! empty( $block['attrs']['slug'] ) && ! in_array( $block['attrs']['slug'], $slugs, true ) ) {
1053
1054 // Made it associative array, so that we can directly check it using block_id rather than mapping or using "in_array" for the checks.
1055 $slugs[ $block['attrs']['block_id'] ] = self::get_string_value( $block['attrs']['slug'] );
1056
1057 if ( is_array( $block['innerBlocks'] ) && ! empty( $block['innerBlocks'] ) ) {
1058 [ $blocks[ $index ]['innerBlocks'], $slugs, $updated ] = self::process_blocks( $block['innerBlocks'], $slugs, $updated, '' );
1059 }
1060 continue;
1061 }
1062
1063 if ( $skip_checking_existing_slug && empty( $block['innerBlocks'] ) && isset( $slugs[ $block['attrs']['block_id'] ] ) ) {
1064 /**
1065 * Skip re-processing of the already process or existing slugs if above parameter "$skip_checking_existing_slug" is passed as true.
1066 * This is helpful in the scenarios where we need to compare and verify between already saved blocks and new unsaved blocks parsed
1067 * from the contents.
1068 *
1069 * However, it is also necessary to make sure if that current block is not a parent / wrapper block
1070 * by checking "$block['innerBlocks']" empty.
1071 *
1072 * And finally, checking if the block-id "$block['attrs']['block_id']" is already set in the list of "$slugs",
1073 * making sure that we are only processing the new blocks.
1074 */
1075 continue;
1076 }
1077
1078 if ( is_array( $blocks[ $index ]['attrs'] ) ) {
1079
1080 $blocks[ $index ]['attrs']['slug'] = self::generate_unique_block_slug( $block, $slugs, $prefix );
1081 $slugs[ $block['attrs']['block_id'] ] = $blocks[ $index ]['attrs']['slug']; // Made it associative array, so that we can directly check it using block_id rather than mapping or using "in_array" for the checks.
1082 $updated = true;
1083 if ( is_array( $block['innerBlocks'] ) && ! empty( $block['innerBlocks'] ) ) {
1084
1085 [ $blocks[ $index ]['innerBlocks'], $slugs, $updated ] = self::process_blocks( $block['innerBlocks'], $slugs, $updated, $blocks[ $index ]['attrs']['slug'] );
1086
1087 }
1088 }
1089 }
1090 return [ $blocks, $slugs, $updated ];
1091 }
1092
1093 /**
1094 * Generates slug based on the provided block and existing slugs.
1095 *
1096 * @param array<mixed> $block The block data.
1097 * @param array<string> $slugs The array of existing slugs.
1098 * @param string $prefix The array of existing slugs.
1099 * @since 0.0.10
1100 * @return string The generated unique block slug.
1101 */
1102 public static function generate_unique_block_slug( $block, $slugs, $prefix ) {
1103 $slug = is_string( $block['blockName'] ) ? $block['blockName'] : '';
1104
1105 if ( ! empty( $block['attrs']['label'] ) && is_string( $block['attrs']['label'] ) ) {
1106 $slug = sanitize_title( $block['attrs']['label'] );
1107 }
1108
1109 if ( ! empty( $prefix ) ) {
1110 $slug = $prefix . '-' . $slug;
1111 }
1112
1113 return self::generate_slug( $slug, $slugs );
1114 }
1115
1116 /**
1117 * This function ensures that the slug is unique.
1118 * If the slug is already taken, it appends a number to the slug to make it unique.
1119 *
1120 * @param string $slug test to be converted to slug.
1121 * @param array<string> $slugs An array of existing slugs.
1122 * @since 0.0.10
1123 * @return string The unique slug.
1124 */
1125 public static function generate_slug( $slug, $slugs ) {
1126 $slug = sanitize_title( $slug );
1127
1128 if ( ! in_array( $slug, $slugs, true ) ) {
1129 return $slug;
1130 }
1131
1132 $index = 1;
1133
1134 while ( in_array( $slug . '-' . $index, $slugs, true ) ) {
1135 $index++;
1136 }
1137
1138 return $slug . '-' . $index;
1139 }
1140
1141 /**
1142 * Encode data to JSON. This function will encode the data with JSON_UNESCAPED_SLASHES and JSON_UNESCAPED_UNICODE.
1143 *
1144 * @since 0.0.11
1145 * @param array<mixed> $data The data to encode.
1146 * @return string|false The JSON representation of the value on success or false on failure.
1147 */
1148 public static function encode_json( $data ) {
1149 return wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
1150 }
1151
1152 /**
1153 * Returns true if SureTriggers plugin is ready for the custom app.
1154 *
1155 * @since 1.0.3
1156 * @return bool Returns true if SureTriggers plugin is ready for the custom app.
1157 */
1158 public static function is_suretriggers_ready() {
1159 if ( ! defined( 'SURE_TRIGGERS_FILE' ) ) {
1160 // Probably plugin is de-activated or not installed at all.
1161 return false;
1162 }
1163
1164 $suretriggers_data = get_option( 'suretrigger_options', [] );
1165 if ( ! is_array( $suretriggers_data ) || empty( $suretriggers_data['secret_key'] ) || ! is_string( $suretriggers_data['secret_key'] ) ) {
1166 // SureTriggers is not authenticated yet.
1167 return false;
1168 }
1169
1170 return true;
1171 }
1172
1173 /**
1174 * Registers script translations for a specific handle.
1175 *
1176 * This function sets the script translations for a given script handle, allowing
1177 * localization of JavaScript strings using the specified text domain and path.
1178 *
1179 * @param string $handle The script handle to apply translations to.
1180 * @param string $domain Optional. The text domain for translations. Default is 'sureforms'.
1181 * @param string $path Optional. The path to the translation files. Default is the 'languages' folder in the SureForms directory.
1182 *
1183 * @since 1.0.5
1184 * @return void
1185 */
1186 public static function register_script_translations( $handle, $domain = 'sureforms', $path = SRFM_DIR . 'languages' ) {
1187 wp_set_script_translations( $handle, $domain, $path );
1188 }
1189
1190 /**
1191 * Validates whether the specified conditions or a single key-value pair exist in the request context.
1192 *
1193 * - If `$conditions` is provided as an array, it will validate all key-value pairs in `$conditions`
1194 * against the `$_REQUEST` superglobal.
1195 * - If `$conditions` is empty, it validates a single key-value pair from `$key` and `$value`.
1196 *
1197 * @param string $value The expected value to match in the request if `$conditions` is not used.
1198 * @param string $key The key to check for in the request if `$conditions` is not used.
1199 * @param array<string, string> $conditions An optional associative array of key-value pairs to validate.
1200 * @since 1.1.1
1201 * @return bool Returns true if all conditions are met or the single key-value pair is valid, otherwise false.
1202 */
1203 public static function validate_request_context( $value, $key = 'post_type', array $conditions = [] ) {
1204 // If conditions are provided, validate all key-value pairs in the conditions array.
1205 if ( ! empty( $conditions ) ) {
1206 foreach ( $conditions as $condition_key => $condition_value ) {
1207 if ( ! isset( $_REQUEST[ $condition_key ] ) || $_REQUEST[ $condition_key ] !== $condition_value ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a controlled comparison of request values.
1208 // Return false if any condition is not satisfied.
1209 return false;
1210 }
1211 }
1212 // Return true if all conditions are satisfied.
1213 return true;
1214 }
1215
1216 // Validate $value and $key when no conditions are provided.
1217 if ( empty( $key ) || empty( $value ) ) {
1218 return false;
1219 }
1220
1221 // Validate a single key-value pair when no conditions are provided.
1222 return isset( $_REQUEST[ $key ] ) && $_REQUEST[ $key ] === $value; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Nonce verification is not needed here. Input is validated via strict comparison.
1223 }
1224
1225 /**
1226 * Retrieve the list of excluded fields for form data processing.
1227 *
1228 * This method returns an array of field keys that should be excluded when
1229 * processing form data.
1230 *
1231 * @since 1.1.1
1232 * @return array<string> Returns the string array of excluded fields.
1233 */
1234 public static function get_excluded_fields() {
1235 $excluded_fields = [ 'srfm-honeypot-field', 'g-recaptcha-response', 'srfm-sender-email-field', 'form-id' ];
1236
1237 return apply_filters( 'srfm_excluded_fields', $excluded_fields );
1238 }
1239
1240 /**
1241 * Check whether the current page is a SureForms admin page.
1242 *
1243 * @since 1.2.2
1244 * @return bool Returns true if the current page is a SureForms admin page, otherwise false.
1245 */
1246 public static function is_sureforms_admin_page() {
1247 $current_screen = get_current_screen();
1248 $is_screen_sureforms_menu = self::validate_request_context( 'sureforms_menu', 'page' );
1249 $is_screen_add_new_form = self::validate_request_context( 'add-new-form', 'page' );
1250 $is_screen_sureforms_form_settings = self::validate_request_context( 'sureforms_form_settings', 'page' );
1251 $is_screen_sureforms_entries = self::validate_request_context( SRFM_ENTRIES, 'page' );
1252 $is_post_type_sureforms_form = $current_screen && SRFM_FORMS_POST_TYPE === $current_screen->post_type;
1253
1254 return $is_screen_sureforms_menu || $is_screen_add_new_form || $is_screen_sureforms_form_settings || $is_screen_sureforms_entries || $is_post_type_sureforms_form;
1255 }
1256
1257 /**
1258 * Filters and concatenates valid class names from an array.
1259 *
1260 * @param array<string> $class_names The array containing potential class names.
1261 * @since 1.4.0
1262 * @return string The concatenated string of valid class names separated by spaces.
1263 */
1264 public static function join_strings( $class_names ) {
1265 // Filter the array to include only valid class names.
1266 $valid_class_names = array_filter(
1267 $class_names,
1268 static function ( $value ) {
1269 return is_string( $value ) && '' !== $value && false !== $value;
1270 }
1271 );
1272
1273 // Concatenate the valid class names with spaces and return.
1274 return implode( ' ', $valid_class_names );
1275 }
1276 /**
1277 * Get SureForms Website URL.
1278 *
1279 * @param string $trail The URL trail to append to SureForms website URL. The parameter should not include a leading slash as the base URL already ends with a trailing slash.
1280 * @param array<string, string> $utm_args Optional. An associative array of UTM parameters to append to the URL. Default empty array. Example: [ 'utm_medium' => 'dashboard'].
1281 * @since 0.0.7
1282 * @return string
1283 */
1284 public static function get_sureforms_website_url( $trail, $utm_args = [] ) {
1285 $url = SRFM_WEBSITE;
1286 if ( ! empty( $trail ) && is_string( $trail ) ) {
1287 $url = SRFM_WEBSITE . $trail;
1288 }
1289
1290 if ( ! is_array( $utm_args ) ) {
1291 $utm_args = [];
1292 }
1293
1294 if ( class_exists( 'BSF_UTM_Analytics' ) ) {
1295 $url = \BSF_UTM_Analytics::get_utm_ready_link( $url, 'sureforms', $utm_args );
1296 }
1297
1298 return esc_url( $url );
1299 }
1300
1301 /**
1302 * Validates if the given string is a valid CSS class name.
1303 *
1304 * A valid CSS class name:
1305 * - Does not start with a digit, hyphen, or underscore.
1306 * - Can contain alphanumeric characters, underscores, hyphens, and Unicode letters.
1307 *
1308 * @param string $class_name The class name to validate.
1309 *
1310 * @since 1.3.1
1311 * @return bool True if the class name is valid, otherwise false.
1312 */
1313 public static function is_valid_css_class_name( $class_name ) {
1314 // Regular expression to validate a Unicode-aware CSS class name.
1315 $class_name_regex = '/^[^\d\-_][\w\p{L}\p{N}\-_]*$/u';
1316
1317 // Check if the className matches the pattern.
1318 return preg_match( $class_name_regex, $class_name ) === 1;
1319 }
1320
1321 /**
1322 * Get the gradient css for given gradient parameters.
1323 *
1324 * @param string $type The type of gradient. Default 'linear'.
1325 * @param string $color1 The first color of the gradient. Default '#FFC9B2'.
1326 * @param string $color2 The second color of the gradient. Default '#C7CBFF'.
1327 * @param int $loc1 The location of the first color. Default 0.
1328 * @param int $loc2 The location of the second color. Default 100.
1329 * @param int $angle The angle of the gradient. Default 90.
1330 *
1331 * @since 1.4.4
1332 * @return string The gradient css.
1333 */
1334 public static function get_gradient_css( $type = 'linear', $color1 = '#FFC9B2', $color2 = '#C7CBFF', $loc1 = 0, $loc2 = 100, $angle = 90 ) {
1335 if ( 'linear' === $type ) {
1336 return "linear-gradient({$angle}deg, {$color1} {$loc1}%, {$color2} {$loc2}%)";
1337 }
1338 return "radial-gradient({$color1} {$loc1}%, {$color2} {$loc2}%)";
1339 }
1340
1341 /**
1342 * Return the classes based on background and overlay type to add to the form container.
1343 *
1344 * @param string $background_type The background type.
1345 * @param string $overlay_type The overlay type.
1346 * @param string $bg_image The background image url.
1347 *
1348 * @since 1.4.4
1349 * @return string The classes to add to the form container.
1350 */
1351 public static function get_background_classes( $background_type, $overlay_type, $bg_image = '' ) {
1352 if ( empty( $background_type ) ) {
1353 $background_type = 'color';
1354 }
1355
1356 $background_type_class = '';
1357 $overlay_class = 'image' === $background_type && ! empty( $bg_image ) && $overlay_type ? "srfm-overlay-{$overlay_type}" : '';
1358
1359 // Set the class based on the background type.
1360 switch ( $background_type ) {
1361 case 'image':
1362 $background_type_class = 'srfm-bg-image';
1363 break;
1364 case 'gradient':
1365 $background_type_class = 'srfm-bg-gradient';
1366 break;
1367 default:
1368 $background_type_class = 'srfm-bg-color';
1369 break;
1370 }
1371
1372 return self::join_strings( [ $background_type_class, $overlay_class ] );
1373 }
1374
1375 /**
1376 * Custom escape function for the textarea with rich text support.
1377 *
1378 * @param string $content The content submitted by the user in the textarea block.
1379 * @since 1.7.1
1380 *
1381 * @return string Escaped content.
1382 */
1383 public static function esc_textarea( $content ) {
1384 $content = wpautop( self::sanitize_textarea( $content ) );
1385
1386 return trim( str_replace( [ "\r\n", "\r", "\n" ], '', $content ) );
1387 }
1388
1389 /**
1390 * Custom sanitization function for the textarea with rich text support.
1391 *
1392 * @param string $content The content submitted by the user in the textarea block.
1393 * @since 1.7.1
1394 *
1395 * @return string Sanitized content.
1396 */
1397 public static function sanitize_textarea( $content ) {
1398 $count = 1;
1399 $content = convert_invalid_entities( $content );
1400
1401 // Remove the 'script' and 'style' tags recursively from the content.
1402 while ( $count ) {
1403 $content = preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', self::get_string_value( $content ), - 1, $count );
1404 }
1405
1406 // Disable the safe style attribute parsing for the textarea block.
1407 add_filter( 'safe_style_css', [ self::class, 'disable_style_attr_parsing' ], 10, 1 );
1408 $content = wp_kses_post( self::get_string_value( $content ) );
1409
1410 // Remove the filter after sanitization to avoid affecting other blocks.
1411 remove_filter( 'safe_style_css', [ self::class, 'disable_style_attr_parsing' ], 10 );
1412
1413 // Ensure all tags are balanced.
1414 return force_balance_tags( $content );
1415 }
1416
1417 /**
1418 * Disable parsing of style attributes for the textarea block.
1419 *
1420 * @param array<string> $allowed_styles The allowed styles.
1421 * @since 1.7.1
1422 *
1423 * @return array An empty array to disable style attribute parsing.
1424 */
1425 public static function disable_style_attr_parsing( $allowed_styles ) {
1426 unset( $allowed_styles );
1427 // Disable parsing of style attributes.
1428 return [];
1429 }
1430 /**
1431 * Strips JavaScript attributes from HTML content.
1432 *
1433 * @param string $html The HTML content to process.
1434 * @since 1.7.1
1435 * @return string The cleaned HTML content without JavaScript attributes.
1436 */
1437 public static function strip_js_attributes( $html ) {
1438 $dom = new \DOMDocument();
1439
1440 // Suppress warnings due to malformed HTML.
1441 libxml_use_internal_errors( true );
1442 $loaded = $dom->loadHTML( '<?xml encoding="utf-8" ?>' . $html );
1443 libxml_clear_errors();
1444
1445 if ( ! $loaded ) {
1446 return $html; // Return original HTML if loading fails.
1447 }
1448
1449 $xpath = new \DOMXPath( $dom );
1450
1451 // 1. Remove all <script> tags.
1452 $script_nodes = $xpath->query( '//script' );
1453 if ( $script_nodes instanceof \DOMNodeList ) {
1454 foreach ( $script_nodes as $script ) {
1455 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- This is a DOM element.
1456 $parent_node = $script->parentNode;
1457 if ( $parent_node instanceof \DOMNode ) {
1458 $parent_node->removeChild( $script );
1459 }
1460 }
1461 }
1462
1463 // 2. Remove all attributes that start with "on" (like onclick, onmouseover, etc.).
1464 $elements_with_on_attrs = $xpath->query( '//*[@*[starts-with(name(), "on")]]' );
1465 if ( $elements_with_on_attrs instanceof \DOMNodeList ) {
1466 foreach ( $elements_with_on_attrs as $element ) {
1467 if ( $element instanceof \DOMElement && $element->hasAttributes() ) {
1468 foreach ( iterator_to_array( $element->attributes ) as $attr ) {
1469 if ( $attr instanceof \DOMAttr && stripos( $attr->name, 'on' ) === 0 ) {
1470 $element->removeAttribute( $attr->name );
1471 }
1472 }
1473 }
1474 }
1475 }
1476
1477 // Return cleaned HTML.
1478 $body = $dom->getElementsByTagName( 'body' )->item( 0 );
1479 if ( $body instanceof \DOMNode ) {
1480 $cleaned_html = $dom->saveHTML( $body );
1481 return is_string( $cleaned_html ) ? $cleaned_html : '';
1482 }
1483 return '';
1484 }
1485
1486 /**
1487 * Encodes the given string with base64.
1488 * Moved from admin class to here.
1489 *
1490 * @param string $logo contains svg's.
1491 * @return string
1492 */
1493 public static function encode_svg( $logo ) {
1494 return 'data:image/svg+xml;base64,' . base64_encode( $logo ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
1495 }
1496
1497 /**
1498 * Get plugin status
1499 *
1500 * @since 0.0.1
1501 * @since 1.7.0 moved to inc/helper.php from inc/admin-ajax.php
1502 *
1503 * @param string $plugin_init_file Plugin init file.
1504 * @return string
1505 */
1506 public static function get_plugin_status( $plugin_init_file ) {
1507
1508 $installed_plugins = get_plugins();
1509
1510 if ( ! isset( $installed_plugins[ $plugin_init_file ] ) ) {
1511 return 'Install';
1512 }
1513 if ( is_plugin_active( $plugin_init_file ) ) {
1514 return 'Activated';
1515 }
1516 return 'Installed';
1517 }
1518
1519 /**
1520 * Check if the starter template premium plugin is installed and return its file path.
1521 *
1522 * @since 1.7.3
1523 *
1524 * @return string The plugin file path if premium is installed, otherwise the default starter sites plugin file path.
1525 */
1526 public static function check_starter_template_plugin() {
1527 if ( ! function_exists( 'get_plugins' ) ) {
1528 require_once ABSPATH . 'wp-admin/includes/plugin.php';
1529 }
1530 $plugins = get_plugins();
1531
1532 $premium = 'astra-pro-sites/astra-pro-sites.php';
1533
1534 return isset( $plugins[ $premium ] ) ? $premium : 'astra-sites/astra-sites.php';
1535 }
1536
1537 /**
1538 * Get sureforms recommended integrations.
1539 *
1540 * @since 0.0.1
1541 * @since 1.7.0 moved to inc/helper.php from inc/admin-ajax.php
1542 *
1543 * @return array<mixed>
1544 */
1545 public static function sureforms_get_integration() {
1546 $suretrigger_connected = apply_filters( 'suretriggers_is_user_connected', '' );
1547 $logo_sure_triggers = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/suretriggers.svg' );
1548 $logo_full = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/suretriggers_full.svg' );
1549 $logo_sure_mails = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/suremails.svg' );
1550 $logo_uae = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/uae.svg' );
1551 $logo_starter_templates = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/starterTemplates.svg' );
1552 return apply_filters(
1553 'srfm_integrated_plugins',
1554 [
1555 'sure_mails' => [
1556 'title' => __( 'SureMail', 'sureforms' ),
1557 'subtitle' => __( 'Free and easy SMTP mails plugin.', 'sureforms' ),
1558 'status' => self::get_plugin_status( 'suremails/suremails.php' ),
1559 'slug' => 'suremails',
1560 'path' => 'suremails/suremails.php',
1561 'logo' => self::encode_svg( is_string( $logo_sure_mails ) ? $logo_sure_mails : '' ),
1562 ],
1563 'sure_triggers' => [
1564 'title' => __( 'OttoKit', 'sureforms' ),
1565 'subtitle' => __( 'No-code automation tool for WordPress.', 'sureforms' ),
1566 'description' => __( 'OttoKit is a powerful automation platform that helps you connect your various plugins and apps together. It allows you to automate repetitive tasks, so you can focus on more important work.', 'sureforms' ),
1567 'status' => self::get_plugin_status( 'suretriggers/suretriggers.php' ),
1568 'slug' => 'suretriggers',
1569 'path' => 'suretriggers/suretriggers.php',
1570 'logo' => self::encode_svg( is_string( $logo_sure_triggers ) ? $logo_sure_triggers : '' ),
1571 'logo_full' => self::encode_svg( is_string( $logo_full ) ? $logo_full : '' ),
1572 'connected' => $suretrigger_connected,
1573 'connection_url' => admin_url( 'admin.php?page=suretriggers' ),
1574 ],
1575 'uae' => [
1576 'title' => __( 'Ultimate Addons for Elementor', 'sureforms' ),
1577 'subtitle' => __( 'Build modern websites with elementor addons.', 'sureforms' ),
1578 'status' => self::get_plugin_status( 'header-footer-elementor/header-footer-elementor.php' ),
1579 'slug' => 'header-footer-elementor',
1580 'path' => 'header-footer-elementor/header-footer-elementor.php',
1581 'logo' => self::encode_svg( is_string( $logo_uae ) ? $logo_uae : '' ),
1582 ],
1583 'starter_templates' => [
1584 'title' => __( 'Starter Templates', 'sureforms' ),
1585 'subtitle' => __( 'Build your dream website in minutes with AI.', 'sureforms' ),
1586 'status' => self::get_plugin_status( self::check_starter_template_plugin() ),
1587 'slug' => 'astra-sites',
1588 'path' => self::check_starter_template_plugin(),
1589 'logo' => self::encode_svg( is_string( $logo_starter_templates ) ? $logo_starter_templates : '' ),
1590 ],
1591 ]
1592 );
1593 }
1594
1595 /**
1596 * Get a value from the srfm_options array.
1597 *
1598 * @param string $key The key to retrieve.
1599 * @param mixed $default The default value to return if the key does not exist.
1600 * @since 1.8.0
1601 * @return mixed
1602 */
1603 public static function get_srfm_option( $key, $default = null ) {
1604 $options = get_option( 'srfm_options', [] );
1605 if ( ! is_array( $options ) ) {
1606 $options = [];
1607 }
1608 return array_key_exists( $key, $options ) ? $options[ $key ] : $default;
1609 }
1610
1611 /**
1612 * Update a value in the srfm_options array.
1613 *
1614 * @param string $key The key to update.
1615 * @param mixed $value The value to set.
1616 * @since 1.8.0
1617 * @return void
1618 */
1619 public static function update_srfm_option( $key, $value ) {
1620 $options = get_option( 'srfm_options', [] );
1621 if ( ! is_array( $options ) ) {
1622 $options = [];
1623 }
1624 $options[ $key ] = $value;
1625 update_option( 'srfm_options', $options );
1626 }
1627
1628 /**
1629 * Get the WordPress file types.
1630 *
1631 * @since 1.7.4
1632 * @return array<string,mixed> An associative array representing the file types.
1633 */
1634 public static function get_wp_file_types() {
1635 $formats = [];
1636 $mimes = get_allowed_mime_types();
1637 $maxsize = wp_max_upload_size() / 1048576;
1638 if ( ! empty( $mimes ) ) {
1639 foreach ( $mimes as $type => $mime ) {
1640 $multiple = explode( '|', $type );
1641 foreach ( $multiple as $single ) {
1642 $formats[] = $single;
1643 }
1644 }
1645 }
1646
1647 return [
1648 'formats' => $formats,
1649 'maxsize' => $maxsize,
1650 ];
1651 }
1652
1653 /**
1654 * Determines if the SureForms Pro plugin is installed and active.
1655 *
1656 * Checks for the presence of the SRFM_PRO_VER constant.
1657 *
1658 * @since 1.8.0
1659 *
1660 * @return bool True if the Pro plugin is active; false otherwise.
1661 */
1662 public static function has_pro() {
1663 return defined( 'SRFM_PRO_VER' );
1664 }
1665
1666 /**
1667 * Verifies the request by checking the nonce and user capabilities.
1668 *
1669 * @param string $request_type The type of request, either 'rest' or 'ajax'.
1670 * @param string $nonce_action The action name for the nonce.
1671 * @param string $nonce_name The name of the nonce field.
1672 * @param string $capability The capability required to perform the action. Default is 'manage_options'.
1673 *
1674 * @since 1.10.0
1675 * @return void
1676 */
1677 public static function verify_nonce_and_capabilities( $request_type, $nonce_action, $nonce_name, $capability = 'manage_options' ) {
1678
1679 if ( ! is_string( $nonce_action ) || ! is_string( $nonce_name ) || empty( $nonce_action ) || empty( $nonce_name ) ) {
1680 wp_send_json_error(
1681 [ 'message' => __( 'Invalid nonce action or name.', 'sureforms' ) ],
1682 400
1683 );
1684 }
1685
1686 // Verify nonce for security.
1687 if ( 'rest' === $request_type ) {
1688 // For REST API requests, use the WP_REST_Request object to verify the nonce.
1689 if ( ! wp_verify_nonce( $nonce_action, $nonce_name ) ) {
1690 wp_send_json_error(
1691 [ 'message' => __( 'Invalid security token.', 'sureforms' ) ],
1692 403
1693 );
1694 }
1695 } elseif ( 'ajax' === $request_type ) {
1696 // For non-REST requests, use the standard nonce verification.
1697 if ( ! check_ajax_referer( $nonce_action, $nonce_name, false ) ) {
1698 wp_send_json_error(
1699 [ 'message' => __( 'Invalid security token.', 'sureforms' ) ],
1700 403
1701 );
1702 }
1703 } else {
1704 // If the request type is not recognized, return an error.
1705 wp_send_json_error(
1706 [ 'message' => __( 'Invalid request type.', 'sureforms' ) ],
1707 400
1708 );
1709 }
1710
1711 // Check user capabilities.
1712 if ( ! current_user_can( $capability ) ) {
1713 wp_send_json_error(
1714 [ 'message' => esc_html__( 'You do not have permission to perform this action.', 'sureforms' ) ],
1715 403
1716 );
1717 }
1718 }
1719
1720 /**
1721 * Get the block name from a field name by extracting the first two parts.
1722 *
1723 * @param string $field_name The full field name (e.g., 'srfm-text-lbl-123').
1724 *
1725 * @since 1.11.0
1726 * @return string The block name (e.g., 'srfm-text').
1727 */
1728 public static function get_block_name_from_field( $field_name ) {
1729 return implode( '-', array_slice( explode( '-', explode( '-lbl-', $field_name )[0] ), 0, 2 ) );
1730 }
1731
1732 /**
1733 * Check if any of the top 10 popular WordPress SMTP plugins is active using array_intersect.
1734 *
1735 * @since 1.9.1
1736 * @return bool True if any SMTP plugin is active, false otherwise.
1737 */
1738 public static function is_any_smtp_plugin_active() {
1739 $smtp_plugins = [
1740 'wp-mail-smtp/wp_mail_smtp.php',
1741 'post-smtp/postman-smtp.php',
1742 'easy-wp-smtp/easy-wp-smtp.php',
1743 'wp-smtp/wp-smtp.php',
1744 'newsletter/plugin.php',
1745 'fluent-smtp/fluent-smtp.php',
1746 'pepipost-smtp/pepipost-smtp.php',
1747 'mail-bank/wp-mail-bank.php',
1748 'smtp-mailer/smtp-mailer.php',
1749 'suremails/suremails.php',
1750 'site-mailer/site-mailer.php',
1751 ];
1752
1753 $active_plugins = (array) get_option( 'active_plugins', [] );
1754 // For multisite, merge sitewide active plugins.
1755 if ( is_multisite() ) {
1756 $network_plugins = (array) get_site_option( 'active_sitewide_plugins', [] );
1757 $active_plugins = array_merge( $active_plugins, array_keys( $network_plugins ) );
1758 }
1759
1760 return (bool) array_intersect( $smtp_plugins, $active_plugins );
1761 }
1762
1763 /**
1764 * Apply a filter and return the filtered value only if it's a non-empty array.
1765 * Otherwise, return the default array.
1766 *
1767 * @param string $filter_name The name of the filter to apply.
1768 * @param mixed $default The default array to return if the filtered result is invalid.
1769 * @param mixed ...$args Additional arguments to pass to the filter.
1770 *
1771 * @return array The filtered array if valid, otherwise the default.
1772 */
1773 public static function apply_filters_as_array( $filter_name, $default, ...$args ) {
1774 // Ensure $default is an array.
1775 if ( ! is_array( $default ) ) {
1776 $default = [];
1777 }
1778
1779 // Validate the filter name.
1780 if ( ! is_string( $filter_name ) || empty( $filter_name ) ) {
1781 return $default;
1782 }
1783
1784 // Apply the filter with additional arguments.
1785 $filtered = apply_filters( $filter_name, $default, ...$args );
1786
1787 // Return filtered result if it's a non-empty array.
1788 return is_array( $filtered ) && ! empty( $filtered ) ? $filtered : $default;
1789 }
1790
1791 /**
1792 * Get forms with entry counts for a specific time period.
1793 *
1794 * @param int $timestamp The timestamp to get entries after.
1795 * @param int $limit Maximum number of forms to return (0 for all).
1796 * @param bool $sort Whether to sort by entry count descending.
1797 * @return array Array of form data with entry counts.
1798 * @since 1.9.1
1799 */
1800 public static function get_forms_with_entry_counts( $timestamp, $limit = 0, $sort = true ) {
1801 // Get all published forms with post objects for bulk title access.
1802 $args = [
1803 'post_type' => SRFM_FORMS_POST_TYPE,
1804 'posts_per_page' => -1,
1805 'post_status' => 'publish',
1806 'orderby' => 'ID',
1807 'order' => 'DESC',
1808 'no_found_rows' => true,
1809 'update_post_term_cache' => false,
1810 'update_post_meta_cache' => false,
1811 ];
1812
1813 $query = new \WP_Query( $args );
1814
1815 if ( ! $query->have_posts() ) {
1816 return [];
1817 }
1818
1819 $all_forms = [];
1820
1821 // Process posts directly from the query results without touching global $post.
1822 foreach ( $query->posts as $form ) {
1823 // Ensure we have a valid post object.
1824 if ( ! $form instanceof \WP_Post ) {
1825 continue;
1826 }
1827
1828 $form_id = (int) $form->ID;
1829 if ( $form_id <= 0 ) {
1830 continue;
1831 }
1832
1833 // Get entries count after the timestamp for this specific form.
1834 $entry_count = Entries::get_entries_count_after( $timestamp, $form_id );
1835
1836 // Get form title directly from post object, use "Blank Form" if empty.
1837 $form_title = $form->post_title;
1838 if ( empty( trim( self::get_string_value( $form_title ) ) ) ) {
1839 $form_title = __( 'Blank Form', 'sureforms' );
1840 }
1841
1842 $all_forms[] = [
1843 'form_id' => $form_id,
1844 'title' => $form_title,
1845 'count' => $entry_count,
1846 ];
1847 }
1848
1849 // Sort by count descending, then by form_id descending for consistency.
1850 if ( $sort ) {
1851 usort(
1852 $all_forms,
1853 static function( $a, $b ) {
1854 if ( $a['count'] === $b['count'] ) {
1855 return $b['form_id'] - $a['form_id'];
1856 }
1857 return $b['count'] - $a['count'];
1858 }
1859 );
1860 }
1861
1862 // Return limited results if specified.
1863 if ( $limit > 0 ) {
1864 return array_slice( $all_forms, 0, $limit );
1865 }
1866
1867 return $all_forms;
1868 }
1869
1870 /**
1871 * Check if the given form ID is valid SureForms form ID.
1872 * A valid form ID is a numeric value that corresponds to an existing SureForms form in the database.
1873 *
1874 * @since 1.9.1
1875 *
1876 * @param int|string|mixed $form_id The form ID to validate.
1877 * @return bool True if the form ID is valid, false otherwise.
1878 */
1879 public static function is_valid_form( $form_id ) {
1880
1881 // Check for a valid form ID.
1882 if ( empty( $form_id ) || ! is_numeric( $form_id ) ) {
1883 return false;
1884 }
1885
1886 // Check if the form ID exists in the database.
1887 $form = get_post( self::get_integer_value( $form_id ) );
1888
1889 // If the form does not exist or is not of the correct post type, return false.
1890 if ( ! $form || ! is_a( $form, 'WP_Post' ) || SRFM_FORMS_POST_TYPE !== $form->post_type ) {
1891 return false;
1892 }
1893
1894 return true;
1895 }
1896
1897 /**
1898 * Get the timestamp from a string.
1899 *
1900 * @param string $date The date in a specific format (e.g., '2025.10.01').
1901 * @param string $hours The hours in a specific format (e.g., '12').
1902 * @param string $minutes The minutes in a specific format (e.g., '00').
1903 * @param string $meridiem The meridiem in a specific format (e.g., 'AM' or 'PM').
1904 *
1905 * @since 1.10.1
1906 * @return int|false The timestamp if successful, false otherwise.
1907 */
1908 public static function get_timestamp_from_string( $date, $hours = '12', $minutes = '00', $meridiem = 'AM' ) {
1909
1910 if ( empty( $date ) || ! is_string( $date ) ) {
1911 return false; // Invalid input.
1912 }
1913
1914 // Ensure the date is in a valid format of YYYY-MM-DD.
1915 if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) {
1916 return false; // Invalid date format.
1917 }
1918
1919 $time_string = $date . ' ' . $hours . ':' . $minutes . ' ' . $meridiem;
1920
1921 // Convert to timestamp.
1922 $timestamp = strtotime( $time_string );
1923
1924 if ( false !== $timestamp && is_int( $timestamp ) && $timestamp > 0 ) {
1925 return $timestamp;
1926 }
1927
1928 // If conversion fails, return false.
1929 return false;
1930 }
1931 }
1932