PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 1.4.1
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v1.4.1
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,148 lines 40.4 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 use WP_Post_Type;
16
17 if ( ! defined( 'ABSPATH' ) ) {
18 exit; // Exit if accessed directly.
19 }
20
21 /**
22 * Sureforms Helper Class.
23 *
24 * @since 0.0.1
25 */
26 class Helper {
27 use Get_Instance;
28
29 /**
30 * Sureforms SVGs.
31 *
32 * @var mixed srfm_svgs
33 */
34 private static $srfm_svgs = null;
35
36 /**
37 * Get common error message.
38 *
39 * @since 0.0.2
40 * @return array<string>
41 */
42 public static function get_common_err_msg() {
43 return [
44 'required' => __( 'This field is required.', 'sureforms' ),
45 'unique' => __( 'Value needs to be unique.', 'sureforms' ),
46 ];
47 }
48
49 /**
50 * Convert a file URL to a file path.
51 *
52 * @param string $file_url The URL of the file.
53 *
54 * @since 1.3.0
55 * @return string The file path.
56 */
57 public static function convert_fileurl_to_filepath( $file_url ) {
58 static $upload_dir = null;
59 if ( ! $upload_dir ) {
60 // Internally cache the upload directory.
61 $upload_dir = wp_get_upload_dir();
62 }
63 return wp_normalize_path( str_replace( $upload_dir['baseurl'], $upload_dir['basedir'], $file_url ) );
64 }
65
66 /**
67 * Checks if current value is string or else returns default value
68 *
69 * @param mixed $data data which need to be checked if is string.
70 *
71 * @since 0.0.1
72 * @return string
73 */
74 public static function get_string_value( $data ) {
75 if ( is_scalar( $data ) ) {
76 return (string) $data;
77 }
78 if ( is_object( $data ) && method_exists( $data, '__toString' ) ) {
79 return $data->__toString();
80 }
81 if ( is_null( $data ) ) {
82 return '';
83 }
84 return '';
85 }
86 /**
87 * Checks if current value is number or else returns default value
88 *
89 * @param mixed $value data which need to be checked if is string.
90 * @param int $base value can be set is $data is not a string, defaults to empty string.
91 *
92 * @since 0.0.1
93 * @return int
94 */
95 public static function get_integer_value( $value, $base = 10 ) {
96 if ( is_numeric( $value ) ) {
97 return (int) $value;
98 }
99 if ( is_string( $value ) ) {
100 $trimmed_value = trim( $value );
101 return intval( $trimmed_value, $base );
102 }
103 return 0;
104 }
105
106 /**
107 * Checks if current value is an array or else returns default value
108 *
109 * @param mixed $data Data which needs to be checked if it is an array.
110 *
111 * @since 0.0.3
112 * @return array
113 */
114 public static function get_array_value( $data ) {
115 if ( is_array( $data ) ) {
116 return $data;
117 }
118 if ( is_null( $data ) ) {
119 return [];
120 }
121 return (array) $data;
122 }
123
124 /**
125 * Extracts the field type from the dynamic field key ( or field slug ).
126 *
127 * @param string $field_key Dynamic field key.
128 * @since 0.0.6
129 * @return string Extracted field type.
130 */
131 public static function get_field_type_from_key( $field_key ) {
132
133 if ( false === strpos( $field_key, '-lbl-' ) ) {
134 return '';
135 }
136
137 return trim( explode( '-', $field_key )[1] );
138 }
139
140 /**
141 * Extracts the field label from the dynamic field key ( or field slug ).
142 *
143 * @param string $field_key Dynamic field key.
144 * @since 1.1.1
145 * @return string Extracted field label.
146 */
147 public static function get_field_label_from_key( $field_key ) {
148 if ( false === strpos( $field_key, '-lbl-' ) ) {
149 return '';
150 }
151
152 $label = explode( '-lbl-', $field_key )[1];
153 // Getting the encrypted label. we are removing the block slug here.
154 $label = explode( '-', $label )[0];
155
156 return $label ? html_entity_decode( self::decrypt( $label ) ) : '';
157 }
158
159 /**
160 * Returns the proper sanitize callback functions according to the field type.
161 *
162 * @param string $field_type HTML field type.
163 * @since 0.0.6
164 * @return callable Returns sanitize callbacks according to the provided field type.
165 */
166 public static function get_field_type_sanitize_function( $field_type ) {
167 $callbacks = apply_filters(
168 'srfm_field_type_sanitize_functions',
169 [
170 'url' => 'esc_url_raw',
171 'input' => 'sanitize_text_field',
172 'number' => [ self::class, 'sanitize_number' ],
173 'email' => 'sanitize_email',
174 'textarea' => 'sanitize_textarea_field',
175 ]
176 );
177
178 return $callbacks[ $field_type ] ?? 'sanitize_text_field';
179 }
180
181 /**
182 * Sanitizes a numeric value.
183 *
184 * This function checks if the input value is numeric. If it is numeric, it sanitizes
185 * the value to ensure it's a float or integer, allowing for fractions and thousand separators.
186 * If the value is not numeric, it sanitizes it as a text field.
187 *
188 * @param mixed $value The value to be sanitized.
189 * @since 0.0.6
190 * @return int|float|string The sanitized value.
191 */
192 public static function sanitize_number( $value ) {
193 if ( ! is_numeric( $value ) ) {
194 // phpcs:ignore /** @phpstan-ignore-next-line */
195 return sanitize_text_field( $value ); // If it is not numeric, then let user get some sanitized data to view.
196 }
197
198 // phpcs:ignore /** @phpstan-ignore-next-line */
199 return sanitize_text_field( filter_var( $value, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION | FILTER_FLAG_ALLOW_THOUSAND ) );
200 }
201
202 /**
203 * This function sanitizes the submitted form data according to the field type.
204 *
205 * @param array<mixed> $form_data $form_data User submitted form data.
206 * @since 0.0.6
207 * @return array<mixed> $result Sanitized form data.
208 */
209 public static function sanitize_by_field_type( $form_data ) {
210 $result = [];
211
212 if ( empty( $form_data ) || ! is_array( $form_data ) ) {
213 return $result;
214 }
215
216 foreach ( $form_data as $field_key => &$value ) {
217 $field_type = self::get_field_type_from_key( $field_key );
218 $sanitize_function = self::get_field_type_sanitize_function( $field_type );
219 $sanitized_data = is_array( $value ) ? self::sanitize_by_field_type( $value ) : call_user_func( $sanitize_function, $value );
220
221 $result[ $field_key ] = $sanitized_data;
222 }
223
224 return $result;
225 }
226
227 /**
228 * This function performs array_map for multi dimensional array
229 *
230 * @param string $function function name to be applied on each element on array.
231 * @param array<mixed> $data_array array on which function needs to be performed.
232 * @return array<mixed>
233 * @since 0.0.1
234 */
235 public static function sanitize_recursively( $function, $data_array ) {
236 $response = [];
237 if ( is_array( $data_array ) ) {
238 if ( ! is_callable( $function ) ) {
239 return $data_array;
240 }
241 foreach ( $data_array as $key => $data ) {
242 $val = is_array( $data ) ? self::sanitize_recursively( $function, $data ) : $function( $data );
243 $response[ $key ] = $val;
244 }
245 }
246
247 return $response;
248 }
249
250 /**
251 * Generates common markup liked label, etc
252 *
253 * @param int|string $form_id form id.
254 * @param string $type Type of form markup.
255 * @param string $label Label for the form markup.
256 * @param string $slug Slug for the form markup.
257 * @param string $block_id Block id for the form markup.
258 * @param bool $required If field is required or not.
259 * @param string $help Help for the form markup.
260 * @param string $error_msg Error message for the form markup.
261 * @param bool $is_unique Check if the field is unique.
262 * @param string $duplicate_msg Duplicate message for field.
263 * @param bool $override Override for error markup.
264 * @return string
265 * @since 0.0.1
266 */
267 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 ) {
268 $duplicate_msg = $duplicate_msg ? ' data-unique-msg="' . esc_attr( $duplicate_msg ) . '"' : '';
269
270 $markup = '';
271 $show_labels_as_placeholder = get_post_meta( self::get_integer_value( $form_id ), '_srfm_use_label_as_placeholder', true );
272 $show_labels_as_placeholder = $show_labels_as_placeholder ? self::get_string_value( $show_labels_as_placeholder ) : false;
273
274 switch ( $type ) {
275 case 'label':
276 $markup = $label ? '<label id="srfm-label-' . esc_attr( $block_id ) . '" for="srfm-' . $slug . '-' . esc_attr( $block_id ) . '" class="srfm-block-label">' . htmlspecialchars_decode( esc_html( $label ) ) . ( $required ? '<span class="srfm-required" aria-label="' . esc_attr__( 'Required', 'sureforms' ) . '"><span aria-hidden="true"> *</span></span>' : '' ) . '</label>' : '';
277 break;
278 case 'help':
279 $markup = $help ? '<div class="srfm-description" id="srfm-description-' . esc_attr( $block_id ) . '">' . wp_kses_post( htmlspecialchars_decode( $help ) ) . '</div>' : '';
280 break;
281 case 'error':
282 $markup = $required || $override ? '<div class="srfm-error-message" data-srfm-id="srfm-error-' . esc_attr( $block_id ) . '" data-error-msg="' . esc_attr( $error_msg ) . '"' . $duplicate_msg . '>' . esc_html( $error_msg ) . '</div>' : '';
283 break;
284 case 'is_unique':
285 $markup = $is_unique ? '<div class="srfm-error">' . esc_html( $duplicate_msg ) . '</div>' : '';
286 break;
287 case 'placeholder':
288 $markup = $label && '1' === $show_labels_as_placeholder ? htmlspecialchars_decode( esc_html( $label ) ) . ( $required ? ' *' : '' ) : '';
289 break;
290 case 'label_text':
291 // This has been added for generating label text for the form markup instead of adding it in the label tag.
292 $markup = $label ? htmlspecialchars_decode( esc_html( $label ) ) . ( $required ? '<span class="srfm-required" aria-label=",' . esc_attr__( 'Required', 'sureforms' ) . ',"><span aria-hidden="true"> *</span></span>' : '' ) . '</label>' : '';
293 break;
294 default:
295 $markup = '';
296 }
297
298 return $markup;
299 }
300
301 /**
302 * Get an SVG Icon
303 *
304 * @since 0.0.1
305 * @param string $icon the icon name.
306 * @param string $class if the baseline class should be added.
307 * @param string $html Custom attributes inside svg wrapper.
308 * @return string
309 */
310 public static function fetch_svg( $icon = '', $class = '', $html = '' ) {
311 $class = $class ? ' ' . $class : '';
312
313 $output = '<span class="srfm-icon' . $class . '" ' . $html . '>';
314 if ( ! self::$srfm_svgs ) {
315 ob_start();
316
317 include_once SRFM_DIR . 'assets/svg/svgs.json';
318 self::$srfm_svgs = json_decode( self::get_string_value( ob_get_clean() ), true );
319 self::$srfm_svgs = apply_filters( 'srfm_svg_icons', self::$srfm_svgs );
320 }
321
322 $output .= self::$srfm_svgs[ $icon ] ?? '';
323 $output .= '</span>';
324
325 return $output;
326 }
327
328 /**
329 * Encrypt data using base64.
330 *
331 * @param string $input The input string which needs to be encrypted.
332 * @since 0.0.1
333 * @return string The encrypted string.
334 */
335 public static function encrypt( $input ) {
336 // If the input is empty or not a string, then abandon ship.
337 if ( empty( $input ) || ! is_string( $input ) ) {
338 return '';
339 }
340
341 // Encrypt the input and return it.
342 $base_64 = base64_encode( $input ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
343 return rtrim( $base_64, '=' );
344 }
345
346 /**
347 * Decrypt data using base64.
348 *
349 * @param string $input The input string which needs to be decrypted.
350 * @since 0.0.1
351 * @return string The decrypted string.
352 */
353 public static function decrypt( $input ) {
354 // If the input is empty or not a string, then abandon ship.
355 if ( empty( $input ) || ! is_string( $input ) ) {
356 return '';
357 }
358
359 // Decrypt the input and return it.
360 $base_64 = $input . str_repeat( '=', strlen( $input ) % 4 );
361 return base64_decode( $base_64 ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
362 }
363
364 /**
365 * Update an option from the database.
366 *
367 * @param string $key The option key.
368 * @param mixed $value The value to update.
369 * @param bool $network_override Whether to allow the network_override admin setting to be overridden on subsites.
370 * @since 0.0.1
371 * @return bool True if the option was updated, false otherwise.
372 */
373 public static function update_admin_settings_option( $key, $value, $network_override = false ) {
374 // Update the site-wide option if we're in the network admin, and return the updated status.
375 return $network_override && is_multisite() ? update_site_option( $key, $value ) : update_option( $key, $value );
376 }
377
378 /**
379 * Update an option from the database.
380 *
381 * @param int|string $post_id post id / form id.
382 * @param string $key meta key name.
383 * @param bool $single single or multiple.
384 * @param mixed $default default value.
385 *
386 * @since 0.0.1
387 * @return string Meta value.
388 */
389 public static function get_meta_value( $post_id, $key, $single = true, $default = '' ) {
390 $srfm_live_mode_data = self::get_instant_form_live_data();
391
392 if ( isset( $srfm_live_mode_data[ $key ] ) ) {
393 // Give priority to live mode data if we have one set from the Instant Form.
394 return self::get_string_value( $srfm_live_mode_data[ $key ] );
395 }
396
397 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 );
398 }
399
400 /**
401 * Wrapper for the WordPress's get_post_meta function with the support for default values.
402 *
403 * @param int|string $post_id Post ID.
404 * @param string $key The meta key to retrieve.
405 * @param mixed $default Default value.
406 * @param bool $single Optional. Whether to return a single value.
407 * @since 0.0.8
408 * @return mixed Meta value.
409 */
410 public static function get_post_meta( $post_id, $key, $default = null, $single = true ) {
411 $meta_value = get_post_meta( self::get_integer_value( $post_id ), $key, $single );
412 return $meta_value ? $meta_value : $default;
413 }
414
415 /**
416 * Returns query params data for instant form live preview.
417 *
418 * @since 0.0.8
419 * @return array<mixed> Live preview data.
420 */
421 public static function get_instant_form_live_data() {
422 $srfm_live_mode_data = isset( $_GET['live_mode'] ) && current_user_can( 'edit_posts' ) ? self::sanitize_recursively( 'sanitize_text_field', wp_unslash( $_GET ) ) : []; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
423
424 return $srfm_live_mode_data ? array_map(
425 // Normalize falsy values.
426 static function( $live_data ) {
427 return 'false' === $live_data ? false : $live_data;
428 },
429 $srfm_live_mode_data
430 ) : [];
431 }
432
433 /**
434 * Default dynamic block value.
435 *
436 * @since 0.0.1
437 * @return array<string> Meta value.
438 */
439 public static function default_dynamic_block_option() {
440
441 $common_err_msg = self::get_common_err_msg();
442
443 $default_values = [
444 'srfm_url_block_required_text' => $common_err_msg['required'],
445 'srfm_input_block_required_text' => $common_err_msg['required'],
446 'srfm_input_block_unique_text' => $common_err_msg['unique'],
447 'srfm_address_block_required_text' => $common_err_msg['required'],
448 'srfm_phone_block_required_text' => $common_err_msg['required'],
449 'srfm_phone_block_unique_text' => $common_err_msg['unique'],
450 'srfm_number_block_required_text' => $common_err_msg['required'],
451 'srfm_textarea_block_required_text' => $common_err_msg['required'],
452 'srfm_multi_choice_block_required_text' => $common_err_msg['required'],
453 'srfm_checkbox_block_required_text' => $common_err_msg['required'],
454 'srfm_gdpr_block_required_text' => $common_err_msg['required'],
455 'srfm_email_block_required_text' => $common_err_msg['required'],
456 'srfm_email_block_unique_text' => $common_err_msg['unique'],
457 'srfm_dropdown_block_required_text' => $common_err_msg['required'],
458 'srfm_rating_block_required_text' => $common_err_msg['required'],
459 ];
460
461 $default_values = array_merge( $default_values, Translatable::dynamic_validation_messages() );
462
463 return apply_filters( 'srfm_default_dynamic_block_option', $default_values, $common_err_msg );
464 }
465
466 /**
467 * Get default dynamic block value.
468 *
469 * @param string $key meta key name.
470 * @since 0.0.1
471 * @return string Meta value.
472 */
473 public static function get_default_dynamic_block_option( $key ) {
474 $default_dynamic_values = self::default_dynamic_block_option();
475 $option = get_option( 'srfm_default_dynamic_block_option', $default_dynamic_values );
476
477 if ( is_array( $option ) && array_key_exists( $key, $option ) ) {
478 return $option[ $key ];
479 }
480 return '';
481 }
482
483 /**
484 * Checks whether a given request has appropriate permissions.
485 *
486 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
487 * @since 0.0.1
488 */
489 public static function get_items_permissions_check() {
490 if ( current_user_can( 'edit_posts' ) ) {
491 return true;
492 }
493
494 foreach ( get_post_types( [ 'show_in_rest' => true ], 'objects' ) as $post_type ) {
495 /**
496 * The post type.
497 *
498 * @var WP_Post_Type $post_type
499 */
500 if ( current_user_can( $post_type->cap->edit_posts ) ) {
501 return true;
502 }
503 }
504
505 return new WP_Error(
506 'rest_cannot_view',
507 __( 'Sorry, you are not allowed to perform this action.', 'sureforms' ),
508 [ 'status' => \rest_authorization_required_code() ]
509 );
510 }
511
512 /**
513 * Check if the current user has a given capability.
514 *
515 * @param string $capability The capability to check.
516 * @since 0.0.3
517 * @return bool Whether the current user has the given capability or role.
518 */
519 public static function current_user_can( $capability = '' ) {
520
521 if ( ! function_exists( 'current_user_can' ) ) {
522 return false;
523 }
524
525 if ( ! is_string( $capability ) || empty( $capability ) ) {
526 $capability = 'edit_posts';
527 }
528
529 return current_user_can( $capability );
530 }
531
532 /**
533 * Get all the entries for the given form ids. The entries are older than the given days_old.
534 *
535 * @param int $days_old The number of days old the entries should be.
536 * @param array<int> $sf_form_ids The form ids for which the entries need to be fetched.
537 * @since 0.0.2
538 * @return array<mixed> the entries matching the criteria.
539 */
540 public static function get_entries_from_form_ids( $days_old = 0, $sf_form_ids = [] ) {
541
542 $entries = [];
543 $days_old_date = ( new \DateTime() )->modify( "-{$days_old} days" )->format( 'Y-m-d H:i:s' );
544
545 foreach ( $sf_form_ids as $form_id ) {
546 // args according to the get_all() function in the Entries class.
547 $args = [
548 'where' => [
549 [
550 [
551 'key' => 'form_id',
552 'value' => $form_id,
553 'compare' => '=',
554 ],
555 [
556 'key' => 'created_at',
557 'value' => $days_old_date,
558 'compare' => '<=',
559 ],
560 ],
561 ],
562 ];
563
564 // store all the entries in a single array.
565 $entries = array_merge( $entries, Entries::get_all( $args, false ) );
566 }
567 return $entries;
568 }
569
570 /**
571 * Decode block attributes.
572 * The function reverses the effect of serialize_block_attributes()
573 *
574 * @link https://developer.wordpress.org/reference/functions/serialize_block_attributes/
575 * @param string $encoded_data the encoded block attribute.
576 * @since 0.0.2
577 * @return string decoded block attribute
578 */
579 public static function decode_block_attribute( $encoded_data = '' ) {
580 $decoded_data = preg_replace( '/\\\\u002d\\\\u002d/', '--', self::get_string_value( $encoded_data ) );
581 $decoded_data = preg_replace( '/\\\\u003c/', '<', self::get_string_value( $decoded_data ) );
582 $decoded_data = preg_replace( '/\\\\u003e/', '>', self::get_string_value( $decoded_data ) );
583 $decoded_data = preg_replace( '/\\\\u0026/', '&', self::get_string_value( $decoded_data ) );
584 $decoded_data = preg_replace( '/\\\\\\\\"/', '"', self::get_string_value( $decoded_data ) );
585 return self::get_string_value( $decoded_data );
586 }
587
588 /**
589 * Map slugs to submission data.
590 *
591 * @param array<mixed> $submission_data submission_data.
592 * @since 0.0.3
593 * @return array<mixed>
594 */
595 public static function map_slug_to_submission_data( $submission_data = [] ) {
596 $mapped_data = [];
597 foreach ( $submission_data as $key => $value ) {
598 if ( false === strpos( $key, '-lbl-' ) ) {
599 continue;
600 }
601 $label = explode( '-lbl-', $key )[1];
602 $slug = implode( '-', array_slice( explode( '-', $label ), 1 ) );
603 $mapped_data[ $slug ] = $value;
604 }
605 return $mapped_data;
606 }
607
608 /**
609 * Get forms options. Shows all the available forms in the dropdown.
610 *
611 * @since 0.0.5
612 * @param string $key Determines the type of data to return.
613 * @return array<mixed>
614 */
615 public static function get_sureforms( $key = '' ) {
616 $forms = get_posts(
617 apply_filters(
618 'srfm_get_sureforms_query_args',
619 [
620 'post_type' => SRFM_FORMS_POST_TYPE,
621 'posts_per_page' => -1,
622 'post_status' => 'publish',
623 ]
624 )
625 );
626
627 $options = [];
628
629 foreach ( $forms as $form ) {
630 if ( $form instanceof WP_Post ) {
631 if ( 'all' === $key ) {
632 $options[ $form->ID ] = $form;
633 } elseif ( ! empty( $key ) && is_string( $key ) && isset( $form->$key ) ) {
634 $options[ $form->ID ] = $form->$key;
635 } else {
636 $options[ $form->ID ] = $form->post_title;
637 }
638 }
639 }
640
641 return $options;
642 }
643
644 /**
645 * Get all the forms.
646 *
647 * @since 0.0.5
648 * @return array<mixed>
649 */
650 public static function get_sureforms_title_with_ids() {
651 $form_options = self::get_sureforms();
652
653 foreach ( $form_options as $key => $value ) {
654 $form_options[ $key ] = $value . ' #' . $key;
655 }
656
657 return $form_options;
658 }
659
660 /**
661 * Get the CSS variables based on different field spacing sizes.
662 *
663 * @param string|null $field_spacing The field spacing size or boolean false to return complete sizes array.
664 *
665 * @since 0.0.7
666 * @return array<string|mixed>
667 */
668 public static function get_css_vars( $field_spacing = null ) {
669 /**
670 * $sizes - Field Spacing Sizes Variables.
671 * The array contains the CSS variables for different field spacing sizes.
672 * Each key corresponds to the field spacing size, and the value is an array of CSS variables.
673 *
674 * For future variables depending on the field spacing size, add the variable to the array respectively.
675 */
676 $sizes = apply_filters(
677 'srfm_css_vars_sizes',
678 [
679 'small' => [
680 '--srfm-row-gap-between-blocks' => '16px',
681 // Address block gap and spacing variables.
682 '--srfm-col-gap-between-fields' => '12px',
683 '--srfm-row-gap-between-fields' => '12px',
684 '--srfm-gap-below-address-label' => '12px',
685 // Dropdown Variables.
686 '--srfm-dropdown-font-size' => '14px',
687 '--srfm-dropdown-gap-between-input-menu' => '4px',
688 '--srfm-dropdown-badge-padding' => '2px 6px',
689 '--srfm-dropdown-multiselect-font-size' => '12px',
690 '--srfm-dropdown-multiselect-line-height' => '16px',
691 '--srfm-dropdown-padding-right' => '12px',
692 // initial padding and from 20px - 12px for dropdown arrow width and 8px for gap before dropdown arrow.
693 '--srfm-dropdown-padding-right-icon' => 'calc( var( --srfm-dropdown-padding-right ) + 20px )',
694 '--srfm-dropdown-multiselect-padding' => '8px var( --srfm-dropdown-padding-right-icon ) 8px 8px',
695 // Input Field Variables.
696 '--srfm-input-height' => '40px',
697 '--srfm-input-field-padding' => '10px 12px',
698 '--srfm-input-field-font-size' => '14px',
699 '--srfm-input-field-line-height' => '20px',
700 '--srfm-input-field-margin' => '4px 0',
701 // Checkbox and GDPR Variables.
702 '--srfm-check-ctn-width' => '16px',
703 '--srfm-check-ctn-height' => '16px',
704 '--srfm-check-svg-size' => '10px',
705 '--srfm-checkbox-margin-top-frontend' => '2px',
706 '--srfm-checkbox-margin-top-editor' => '3px',
707 '--srfm-check-gap' => '8px',
708 '--srfm-checkbox-description-margin-left' => '24px',
709 // Phone Number field variables.
710 '--srfm-flag-section-padding' => '10px 0 10px 12px',
711 '--srfm-gap-between-icon-text' => '8px',
712 // Label Variables.
713 '--srfm-label-font-size' => '14px',
714 '--srfm-label-line-height' => '20px',
715 // Description Variables.
716 '--srfm-description-font-size' => '12px',
717 '--srfm-description-line-height' => '16px',
718 // Button Variables.
719 '--srfm-btn-padding' => '8px 14px',
720 '--srfm-btn-font-size' => '14px',
721 '--srfm-btn-line-height' => '20px',
722 // Multi Choice Variables.
723 '--srfm-multi-choice-horizontal-padding' => '16px',
724 '--srfm-multi-choice-vertical-padding' => '16px',
725 '--srfm-multi-choice-internal-option-gap' => '8px',
726 '--srfm-multi-choice-vertical-svg-size' => '32px',
727 '--srfm-multi-choice-horizontal-image-size' => '20px',
728 '--srfm-multi-choice-vertical-image-size' => '100px',
729 '--srfm-multi-choice-outer-padding' => '0',
730 ],
731 'medium' => [
732 '--srfm-row-gap-between-blocks' => '18px',
733 // Address block gap and spacing variables.
734 '--srfm-col-gap-between-fields' => '16px',
735 '--srfm-row-gap-between-fields' => '16px',
736 '--srfm-gap-below-address-label' => '14px',
737 // Input Field Variables.
738 '--srfm-input-height' => '44px',
739 '--srfm-input-field-font-size' => '16px',
740 '--srfm-input-field-line-height' => '24px',
741 '--srfm-input-field-margin' => '6px 0',
742 // Checkbox and GDPR Variables.
743 '--srfm-checkbox-margin-top-frontend' => '4px',
744 '--srfm-checkbox-margin-top-editor' => '6px',
745 '--srfm-checkbox-description-margin-left' => '24px',
746 // Label Variables.
747 '--srfm-label-font-size' => '16px',
748 '--srfm-label-line-height' => '24px',
749 // Description Variables.
750 '--srfm-description-font-size' => '14px',
751 '--srfm-description-line-height' => '20px',
752 // Button Variables.
753 '--srfm-btn-padding' => '10px 14px',
754 '--srfm-btn-font-size' => '16px',
755 '--srfm-btn-line-height' => '24px',
756 // Multi Choice Variables.
757 '--srfm-multi-choice-horizontal-padding' => '20px',
758 '--srfm-multi-choice-vertical-padding' => '20px',
759 '--srfm-multi-choice-vertical-svg-size' => '40px',
760 '--srfm-multi-choice-horizontal-image-size' => '24px',
761 '--srfm-multi-choice-vertical-image-size' => '120px',
762 '--srfm-multi-choice-outer-padding' => '2px',
763 ],
764 'large' => [
765 '--srfm-row-gap-between-blocks' => '20px',
766 // Address Block Gap and Spacing Variables.
767 '--srfm-col-gap-between-fields' => '16px',
768 '--srfm-row-gap-between-fields' => '20px',
769 '--srfm-gap-below-address-label' => '16px',
770 // Dropdown Variables.
771 '--srfm-dropdown-font-size' => '16px',
772 '--srfm-dropdown-gap-between-input-menu' => '6px',
773 '--srfm-dropdown-badge-padding' => '6px 6px',
774 '--srfm-dropdown-multiselect-font-size' => '14px',
775 '--srfm-dropdown-multiselect-line-height' => '20px',
776 '--srfm-dropdown-padding-right' => '14px',
777 // Input Field Variables.
778 '--srfm-input-height' => '48px',
779 '--srfm-input-field-padding' => '10px 14px',
780 '--srfm-input-field-font-size' => '18px',
781 '--srfm-input-field-line-height' => '28px',
782 '--srfm-input-field-margin' => '8px 0',
783 // Checkbox and GDPR Variables.
784 '--srfm-check-ctn-width' => '20px',
785 '--srfm-check-ctn-height' => '20px',
786 '--srfm-check-svg-size' => '14px',
787 '--srfm-check-gap' => '10px',
788 '--srfm-checkbox-margin-top-frontend' => '4px',
789 '--srfm-checkbox-margin-top-editor' => '5px',
790 '--srfm-checkbox-description-margin-left' => '30px',
791 // Label Variables.
792 '--srfm-label-font-size' => '18px',
793 '--srfm-label-line-height' => '28px',
794 // Description Variables.
795 '--srfm-description-font-size' => '16px',
796 '--srfm-description-line-height' => '24px',
797 // Button Variables.
798 '--srfm-btn-padding' => '10px 14px',
799 '--srfm-btn-font-size' => '18px',
800 '--srfm-btn-line-height' => '28px',
801 // Multi Choice Variables.
802 '--srfm-multi-choice-horizontal-padding' => '24px',
803 '--srfm-multi-choice-vertical-padding' => '24px',
804 '--srfm-multi-choice-internal-option-gap' => '12px',
805 '--srfm-multi-choice-vertical-svg-size' => '48px',
806 '--srfm-multi-choice-horizontal-image-size' => '28px',
807 '--srfm-multi-choice-vertical-image-size' => '140px',
808 '--srfm-multi-choice-outer-padding' => '4px',
809 ],
810 ]
811 );
812 // Return complete sizes array if field_spacing is false. Required in case of JS for Editor changes.
813 if ( ! $field_spacing ) {
814 return $sizes;
815 }
816
817 $selected_size = $sizes['small'];
818 if ( 'small' !== $field_spacing && isset( $sizes[ $field_spacing ] ) ) {
819 $selected_size = array_merge( $selected_size, $sizes[ $field_spacing ] );
820 }
821
822 return $selected_size;
823 }
824
825 /**
826 * Array of SureForms blocks which get have user input.
827 *
828 * @since 0.0.10
829 * @return array<string>
830 */
831 public static function get_sureforms_blocks() {
832 return apply_filters(
833 'srfm_blocks',
834 [
835 'srfm/input',
836 'srfm/email',
837 'srfm/textarea',
838 'srfm/number',
839 'srfm/checkbox',
840 'srfm/gdpr',
841 'srfm/phone',
842 'srfm/address',
843 'srfm/dropdown',
844 'srfm/multi-choice',
845 'srfm/radio',
846 'srfm/submit',
847 'srfm/url',
848 ]
849 );
850 }
851
852 /**
853 * Process blocks and inner blocks.
854 *
855 * @param array<mixed> $blocks The block data.
856 * @param array<string> $slugs The array of existing slugs.
857 * @param bool $updated The array of existing slugs.
858 * @param string $prefix The array of existing slugs.
859 * @param bool $skip_checking_existing_slug Skips the checking of existing slug if passed true. More information documented inside this function.
860 * @since 0.0.10
861 * @return array
862 */
863 public static function process_blocks( $blocks, &$slugs, &$updated, $prefix = '', $skip_checking_existing_slug = false ) {
864
865 if ( ! is_array( $blocks ) ) {
866 return [ $blocks, $slugs, $updated ];
867 }
868
869 foreach ( $blocks as $index => $block ) {
870
871 if ( ! is_array( $block ) ) {
872 continue;
873 }
874 // Checking only for SureForms blocks which can have user input.
875 if ( empty( $block['blockName'] ) || ! in_array( $block['blockName'], self::get_sureforms_blocks(), true ) ) {
876 continue;
877 }
878
879 /**
880 * Lets continue if slug already exists.
881 * This will ensure that we don't update already existing slugs.
882 */
883 if ( isset( $block['attrs'] ) && ! empty( $block['attrs']['slug'] ) && ! in_array( $block['attrs']['slug'], $slugs, true ) ) {
884
885 // Made it associative array, so that we can directly check it using block_id rather than mapping or using "in_array" for the checks.
886 $slugs[ $block['attrs']['block_id'] ] = self::get_string_value( $block['attrs']['slug'] );
887 continue;
888 }
889
890 if ( $skip_checking_existing_slug && empty( $block['innerBlocks'] ) && isset( $slugs[ $block['attrs']['block_id'] ] ) ) {
891 /**
892 * Skip re-processing of the already process or existing slugs if above parameter "$skip_checking_existing_slug" is passed as true.
893 * This is helpful in the scenarios where we need to compare and verify between already saved blocks and new unsaved blocks parsed
894 * from the contents.
895 *
896 * However, it is also necessary to make sure if that current block is not a parent / wrapper block
897 * by checking "$block['innerBlocks']" empty.
898 *
899 * And finally, checking if the block-id "$block['attrs']['block_id']" is already set in the list of "$slugs",
900 * making sure that we are only processing the new blocks.
901 */
902 continue;
903 }
904
905 if ( is_array( $blocks[ $index ]['attrs'] ) ) {
906
907 $blocks[ $index ]['attrs']['slug'] = self::generate_unique_block_slug( $block, $slugs, $prefix );
908 $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.
909 $updated = true;
910 if ( is_array( $block['innerBlocks'] ) && ! empty( $block['innerBlocks'] ) ) {
911
912 [ $blocks[ $index ]['innerBlocks'], $slugs, $updated ] = self::process_blocks( $block['innerBlocks'], $slugs, $updated, $blocks[ $index ]['attrs']['slug'] );
913
914 }
915 }
916 }
917 return [ $blocks, $slugs, $updated ];
918 }
919
920 /**
921 * Generates slug based on the provided block and existing slugs.
922 *
923 * @param array<mixed> $block The block data.
924 * @param array<string> $slugs The array of existing slugs.
925 * @param string $prefix The array of existing slugs.
926 * @since 0.0.10
927 * @return string The generated unique block slug.
928 */
929 public static function generate_unique_block_slug( $block, $slugs, $prefix ) {
930 $slug = is_string( $block['blockName'] ) ? $block['blockName'] : '';
931
932 if ( ! empty( $block['attrs']['label'] ) && is_string( $block['attrs']['label'] ) ) {
933 $slug = sanitize_title( $block['attrs']['label'] );
934 }
935
936 if ( ! empty( $prefix ) ) {
937 $slug = $prefix . '-' . $slug;
938 }
939
940 return self::generate_slug( $slug, $slugs );
941 }
942
943 /**
944 * This function ensures that the slug is unique.
945 * If the slug is already taken, it appends a number to the slug to make it unique.
946 *
947 * @param string $slug test to be converted to slug.
948 * @param array<string> $slugs An array of existing slugs.
949 * @since 0.0.10
950 * @return string The unique slug.
951 */
952 public static function generate_slug( $slug, $slugs ) {
953 $slug = sanitize_title( $slug );
954
955 if ( ! in_array( $slug, $slugs, true ) ) {
956 return $slug;
957 }
958
959 $index = 1;
960
961 while ( in_array( $slug . '-' . $index, $slugs, true ) ) {
962 $index++;
963 }
964
965 return $slug . '-' . $index;
966 }
967
968 /**
969 * Encode data to JSON. This function will encode the data with JSON_UNESCAPED_SLASHES and JSON_UNESCAPED_UNICODE.
970 *
971 * @since 0.0.11
972 * @param array<mixed> $data The data to encode.
973 * @return string|false The JSON representation of the value on success or false on failure.
974 */
975 public static function encode_json( $data ) {
976 return wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
977 }
978
979 /**
980 * Returns true if SureTriggers plugin is ready for the custom app.
981 *
982 * @since 1.0.3
983 * @return bool Returns true if SureTriggers plugin is ready for the custom app.
984 */
985 public static function is_suretriggers_ready() {
986 if ( ! defined( 'SURE_TRIGGERS_FILE' ) ) {
987 // Probably plugin is de-activated or not installed at all.
988 return false;
989 }
990
991 $suretriggers_data = get_option( 'suretrigger_options', [] );
992 if ( ! is_array( $suretriggers_data ) || empty( $suretriggers_data['secret_key'] ) || ! is_string( $suretriggers_data['secret_key'] ) ) {
993 // SureTriggers is not authenticated yet.
994 return false;
995 }
996
997 return true;
998 }
999
1000 /**
1001 * Registers script translations for a specific handle.
1002 *
1003 * This function sets the script translations for a given script handle, allowing
1004 * localization of JavaScript strings using the specified text domain and path.
1005 *
1006 * @param string $handle The script handle to apply translations to.
1007 * @param string $domain Optional. The text domain for translations. Default is 'sureforms'.
1008 * @param string $path Optional. The path to the translation files. Default is the 'languages' folder in the SureForms directory.
1009 *
1010 * @since 1.0.5
1011 * @return void
1012 */
1013 public static function register_script_translations( $handle, $domain = 'sureforms', $path = SRFM_DIR . 'languages' ) {
1014 wp_set_script_translations( $handle, $domain, $path );
1015 }
1016
1017 /**
1018 * Validates whether the specified conditions or a single key-value pair exist in the request context.
1019 *
1020 * - If `$conditions` is provided as an array, it will validate all key-value pairs in `$conditions`
1021 * against the `$_REQUEST` superglobal.
1022 * - If `$conditions` is empty, it validates a single key-value pair from `$key` and `$value`.
1023 *
1024 * @param string $value The expected value to match in the request if `$conditions` is not used.
1025 * @param string $key The key to check for in the request if `$conditions` is not used.
1026 * @param array<string, string> $conditions An optional associative array of key-value pairs to validate.
1027 * @since 1.1.1
1028 * @return bool Returns true if all conditions are met or the single key-value pair is valid, otherwise false.
1029 */
1030 public static function validate_request_context( $value, $key = 'post_type', array $conditions = [] ) {
1031 // If conditions are provided, validate all key-value pairs in the conditions array.
1032 if ( ! empty( $conditions ) ) {
1033 foreach ( $conditions as $condition_key => $condition_value ) {
1034 if ( ! isset( $_REQUEST[ $condition_key ] ) || $_REQUEST[ $condition_key ] !== $condition_value ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a controlled comparison of request values.
1035 // Return false if any condition is not satisfied.
1036 return false;
1037 }
1038 }
1039 // Return true if all conditions are satisfied.
1040 return true;
1041 }
1042
1043 // Validate $value and $key when no conditions are provided.
1044 if ( empty( $key ) || empty( $value ) ) {
1045 return false;
1046 }
1047
1048 // Validate a single key-value pair when no conditions are provided.
1049 return isset( $_REQUEST[ $key ] ) && $_REQUEST[ $key ] === $value; // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Input is validated via strict comparison.
1050 }
1051
1052 /**
1053 * Retrieve the list of excluded fields for form data processing.
1054 *
1055 * This method returns an array of field keys that should be excluded when
1056 * processing form data.
1057 *
1058 * @since 1.1.1
1059 * @return array<string> Returns the string array of excluded fields.
1060 */
1061 public static function get_excluded_fields() {
1062 $excluded_fields = [ 'srfm-honeypot-field', 'g-recaptcha-response', 'srfm-sender-email-field', 'form-id' ];
1063
1064 return apply_filters( 'srfm_excluded_fields', $excluded_fields );
1065 }
1066
1067 /**
1068 * Check whether the current page is a SureForms admin page.
1069 *
1070 * @since 1.2.2
1071 * @return bool Returns true if the current page is a SureForms admin page, otherwise false.
1072 */
1073 public static function is_sureforms_admin_page() {
1074 $current_screen = get_current_screen();
1075 $is_screen_sureforms_menu = self::validate_request_context( 'sureforms_menu', 'page' );
1076 $is_screen_add_new_form = self::validate_request_context( 'add-new-form', 'page' );
1077 $is_screen_sureforms_form_settings = self::validate_request_context( 'sureforms_form_settings', 'page' );
1078 $is_screen_sureforms_entries = self::validate_request_context( SRFM_ENTRIES, 'page' );
1079 $is_post_type_sureforms_form = $current_screen && SRFM_FORMS_POST_TYPE === $current_screen->post_type;
1080
1081 return $is_screen_sureforms_menu || $is_screen_add_new_form || $is_screen_sureforms_form_settings || $is_screen_sureforms_entries || $is_post_type_sureforms_form;
1082 }
1083
1084 /**
1085 * Filters and concatenates valid class names from an array.
1086 *
1087 * @param array<string> $class_names The array containing potential class names.
1088 * @since 1.4.0
1089 * @return string The concatenated string of valid class names separated by spaces.
1090 */
1091 public static function join_strings( $class_names ) {
1092 // Filter the array to include only valid class names.
1093 $valid_class_names = array_filter(
1094 $class_names,
1095 static function ( $value ) {
1096 return is_string( $value ) && '' !== $value && false !== $value;
1097 }
1098 );
1099
1100 // Concatenate the valid class names with spaces and return.
1101 return implode( ' ', $valid_class_names );
1102 }
1103 /**
1104 * Get SureForms Website URL.
1105 *
1106 * @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.
1107 * @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'].
1108 * @since 0.0.7
1109 * @return string
1110 */
1111 public static function get_sureforms_website_url( $trail, $utm_args = [] ) {
1112 $url = SRFM_WEBSITE;
1113 if ( ! empty( $trail ) && is_string( $trail ) ) {
1114 $url = SRFM_WEBSITE . $trail;
1115 }
1116
1117 if ( ! is_array( $utm_args ) ) {
1118 $utm_args = [];
1119 }
1120
1121 if ( class_exists( '\BSF_UTM_Analytics\Inc\Utils' ) ) {
1122 $url = \BSF_UTM_Analytics\Inc\Utils::get_utm_ready_link( $url, 'sureforms', $utm_args );
1123 }
1124
1125 return esc_url( $url );
1126 }
1127
1128 /**
1129 * Validates if the given string is a valid CSS class name.
1130 *
1131 * A valid CSS class name:
1132 * - Does not start with a digit, hyphen, or underscore.
1133 * - Can contain alphanumeric characters, underscores, hyphens, and Unicode letters.
1134 *
1135 * @param string $class_name The class name to validate.
1136 *
1137 * @since 1.3.1
1138 * @return bool True if the class name is valid, otherwise false.
1139 */
1140 public static function is_valid_css_class_name( $class_name ) {
1141 // Regular expression to validate a Unicode-aware CSS class name.
1142 $class_name_regex = '/^[^\d\-_][\w\p{L}\p{N}\-_]*$/u';
1143
1144 // Check if the className matches the pattern.
1145 return preg_match( $class_name_regex, $class_name ) === 1;
1146 }
1147 }
1148