PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.9.0
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.9.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
2,438 lines 82.8 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 * Validate a date string in Y-m-d format.
134 *
135 * @param string $date The date string to validate.
136 * @since 2.6.0
137 * @return bool
138 */
139 public static function validate_date( string $date ): bool {
140 $d = \DateTime::createFromFormat( 'Y-m-d', $date );
141 return $d && $d->format( 'Y-m-d' ) === $date;
142 }
143
144 /**
145 * Returns a boolean representation of the given value.
146 *
147 * @param mixed $data Data which needs to be converted to boolean.
148 *
149 * @since 2.5.2
150 * @return bool
151 */
152 public static function get_boolean_value( $data ) {
153 return (bool) $data;
154 }
155
156 /**
157 * Checks if current value is an array or else returns default value
158 *
159 * @param mixed $data Data which needs to be checked if it is an array.
160 *
161 * @since 0.0.3
162 * @return array
163 */
164 public static function get_array_value( $data ) {
165 if ( is_array( $data ) ) {
166 return $data;
167 }
168 if ( is_null( $data ) ) {
169 return [];
170 }
171 return (array) $data;
172 }
173
174 /**
175 * Extracts the field type from the dynamic field key ( or field slug ).
176 *
177 * @param string $field_key Dynamic field key.
178 * @since 0.0.6
179 * @return string Extracted field type.
180 */
181 public static function get_field_type_from_key( $field_key ) {
182
183 if ( false === strpos( $field_key, '-lbl-' ) ) {
184 return '';
185 }
186
187 return trim( explode( '-', $field_key )[1] );
188 }
189
190 /**
191 * Extracts the field label from the dynamic field key ( or field slug ).
192 *
193 * @param string $field_key Dynamic field key.
194 * @since 1.1.1
195 * @return string Extracted field label.
196 */
197 public static function get_field_label_from_key( $field_key ) {
198 if ( false === strpos( $field_key, '-lbl-' ) ) {
199 return '';
200 }
201
202 $label = explode( '-lbl-', $field_key )[1];
203 // Getting the encrypted label. we are removing the block slug here.
204 $label = explode( '-', $label )[0];
205
206 return $label ? html_entity_decode( self::decrypt( $label ) ) : '';
207 }
208
209 /**
210 * Extracts the block ID from the dynamic field key ( or field slug ).
211 *
212 * @param string $field_key Dynamic field key.
213 * @since 1.6.1
214 * @return string Extracted block ID.
215 */
216 public static function get_block_id_from_key( $field_key ) {
217 // Check if the key contains the block ID identifier.
218 if ( strpos( $field_key, 'srfm-' ) === 0 && strpos( $field_key, '-lbl-' ) === false ) {
219 return ''; // Return empty if the key format is invalid.
220 }
221
222 $parts = explode( '-lbl-', $field_key );
223 if ( isset( $parts[0] ) ) {
224 $block_id = explode( '-', $parts[0] );
225 if ( is_array( $block_id ) && ! empty( $block_id ) ) {
226 return end( $block_id );
227 }
228 }
229 return '';
230 }
231
232 /**
233 * Returns the proper sanitize callback functions according to the field type.
234 *
235 * @param string $field_type HTML field type.
236 * @since 0.0.6
237 * @return callable Returns sanitize callbacks according to the provided field type.
238 */
239 public static function get_field_type_sanitize_function( $field_type ) {
240 $callbacks = apply_filters(
241 'srfm_field_type_sanitize_functions',
242 [
243 'url' => 'esc_url_raw',
244 'input' => 'sanitize_text_field',
245 'number' => [ self::class, 'sanitize_number' ],
246 'email' => 'sanitize_email',
247 'textarea' => [ self::class, 'sanitize_textarea' ],
248 ]
249 );
250
251 return $callbacks[ $field_type ] ?? 'sanitize_text_field';
252 }
253
254 /**
255 * Sanitizes a numeric value.
256 *
257 * This function checks if the input value is numeric. If it is numeric, it sanitizes
258 * the value to ensure it's a float or integer, allowing for fractions and thousand separators.
259 * If the value is not numeric, it sanitizes it as a text field.
260 *
261 * @param mixed $value The value to be sanitized.
262 * @since 0.0.6
263 * @return int|float|string The sanitized value.
264 */
265 public static function sanitize_number( $value ) {
266 if ( ! is_numeric( $value ) ) {
267 // phpcs:ignore /** @phpstan-ignore-next-line */
268 return sanitize_text_field( $value ); // If it is not numeric, then let user get some sanitized data to view.
269 }
270
271 // phpcs:ignore /** @phpstan-ignore-next-line */
272 return sanitize_text_field( filter_var( $value, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION | FILTER_FLAG_ALLOW_THOUSAND ) );
273 }
274
275 /**
276 * Sanitize a CSS value to prevent injection.
277 *
278 * Strips characters that can break out of a CSS property value context
279 * and removes dangerous CSS functions while preserving safe ones
280 * (rgb, hsl, linear-gradient, etc.).
281 *
282 * @param mixed $value Raw CSS value.
283 * @return string Sanitized CSS value.
284 * @since 2.7.0
285 */
286 public static function sanitize_css_value( $value ) {
287 $value = self::get_string_value( $value );
288 // Strip characters that can break out of a CSS property value context.
289 $value = preg_replace( '/[{}<>;\\\\"\'`]/', '', $value ) ?? '';
290 // Remove dangerous CSS functions (url, expression, import, etc.) while preserving safe ones (rgb, hsl, linear-gradient, etc.).
291 return preg_replace( '/\b(url|expression|import|javascript)\s*\(/i', '(', $value ) ?? '';
292 }
293
294 /**
295 * This function sanitizes the submitted form data according to the field type.
296 *
297 * @param array<mixed> $form_data $form_data User submitted form data.
298 * @since 0.0.6
299 * @return array<mixed> $result Sanitized form data.
300 */
301 public static function sanitize_by_field_type( $form_data ) {
302 $result = [];
303
304 if ( empty( $form_data ) || ! is_array( $form_data ) ) {
305 return $result;
306 }
307
308 foreach ( $form_data as $field_key => &$value ) {
309 $field_type = self::get_field_type_from_key( $field_key );
310 $sanitize_function = self::get_field_type_sanitize_function( $field_type );
311 $sanitized_data = is_array( $value ) ? self::sanitize_by_field_type( $value ) : call_user_func( $sanitize_function, $value );
312
313 $result[ $field_key ] = $sanitized_data;
314 }
315
316 return $result;
317 }
318
319 /**
320 * Sanitize a value based on its PHP type.
321 *
322 * Recursively sanitizes arrays while preserving native PHP types (bool, int, float).
323 * Use this for complex object metas with many properties where per-field callbacks are impractical.
324 *
325 * @param mixed $value The value to sanitize.
326 * @param int $depth Current recursion depth. Values nested beyond 10 levels are discarded.
327 * @since 2.8.0
328 * @return mixed The sanitized value.
329 */
330 public static function sanitize_by_type( $value, int $depth = 0 ) {
331 if ( $depth > 10 ) {
332 return '';
333 }
334 if ( is_array( $value ) ) {
335 $sanitized = [];
336 foreach ( $value as $key => $val ) {
337 $sanitized[ sanitize_text_field( (string) $key ) ] = self::sanitize_by_type( $val, $depth + 1 );
338 }
339 return $sanitized;
340 }
341 if ( is_bool( $value ) ) {
342 return $value;
343 }
344 if ( is_int( $value ) ) {
345 return intval( $value );
346 }
347 if ( is_float( $value ) ) {
348 return floatval( $value );
349 }
350 if ( is_string( $value ) ) {
351 return sanitize_text_field( $value );
352 }
353 return '';
354 }
355
356 /**
357 * This function performs array_map for multi dimensional array
358 *
359 * @param string $function function name to be applied on each element on array.
360 * @param array<mixed> $data_array array on which function needs to be performed.
361 * @return array<mixed>
362 * @since 0.0.1
363 */
364 public static function sanitize_recursively( $function, $data_array ) {
365 $response = [];
366 if ( is_array( $data_array ) ) {
367 if ( ! is_callable( $function ) ) {
368 return $data_array;
369 }
370 foreach ( $data_array as $key => $data ) {
371 $val = is_array( $data ) ? self::sanitize_recursively( $function, $data ) : $function( $data );
372 $response[ $key ] = $val;
373 }
374 }
375
376 return $response;
377 }
378
379 /**
380 * Generates common markup liked label, etc
381 *
382 * @param int|string $form_id form id.
383 * @param string $type Type of form markup.
384 * @param string $label Label for the form markup.
385 * @param string $slug Slug for the form markup.
386 * @param string $block_id Block id for the form markup.
387 * @param bool $required If field is required or not.
388 * @param string $help Help for the form markup.
389 * @param string $error_msg Error message for the form markup.
390 * @param bool $is_unique Check if the field is unique.
391 * @param string $duplicate_msg Duplicate message for field.
392 * @param bool $override Override for error markup.
393 * @return string
394 * @since 0.0.1
395 */
396 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 ) {
397 $duplicate_msg = $duplicate_msg ? ' data-unique-msg="' . esc_attr( $duplicate_msg ) . '"' : '';
398
399 $markup = '';
400 $show_labels_as_placeholder = get_post_meta( self::get_integer_value( $form_id ), '_srfm_use_label_as_placeholder', true );
401 $show_labels_as_placeholder = $show_labels_as_placeholder ? self::get_string_value( $show_labels_as_placeholder ) : false;
402
403 $required_sign = apply_filters( 'srfm_value_after_label_placeholder', ' *' );
404
405 if ( ! is_string( $required_sign ) ) {
406 $required_sign = ' *';
407 }
408
409 switch ( $type ) {
410 case 'label':
411 if ( $label ) {
412 ob_start();
413 ?>
414 <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">
415 <?php echo wp_kses_post( $label ); ?>
416 <?php if ( $required ) { ?>
417 <span class="srfm-required" aria-hidden="true"> *</span>
418 <?php } ?>
419 </label>
420 <?php
421 $markup = ob_get_clean();
422 }
423 break;
424 case 'help':
425 if ( $help ) {
426 ob_start();
427 ?>
428 <div class="srfm-description" id="srfm-description-<?php echo esc_attr( $block_id ); ?>">
429 <?php echo wp_kses_post( $help ); ?>
430 </div>
431 <?php
432 $markup = ob_get_clean();
433 }
434 break;
435 case 'error':
436 if ( $required || $override ) {
437 ob_start();
438 ?>
439 <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 ?>>
440 <?php echo esc_html( $error_msg ); ?>
441 </div>
442 <?php
443 $markup = ob_get_clean();
444 }
445 break;
446 case 'is_unique':
447 if ( $is_unique ) {
448 ob_start();
449 ?>
450 <div class="srfm-error">
451 <?php echo esc_html( $duplicate_msg ); ?>
452 </div>
453 <?php
454 $markup = ob_get_clean();
455 }
456 break;
457 case 'placeholder':
458 $markup = $label && '1' === $show_labels_as_placeholder ? wp_kses_post( $label ) . ( $required ? esc_attr( $required_sign ) : '' ) : '';
459 break;
460 case 'label_text':
461 // This has been added for generating label text for the form markup instead of adding it in the label tag.
462 if ( $label ) {
463 ob_start();
464 ?>
465 <?php echo wp_kses_post( $label ); ?>
466 <?php if ( $required ) { ?>
467 <span class="srfm-required" aria-hidden="true"> *</span>
468 <?php } ?>
469 <?php
470 $markup = ob_get_clean();
471 }
472 break;
473 default:
474 $markup = '';
475 }
476
477 return is_string( $markup ) ? $markup : '';
478 }
479
480 /**
481 * Get an SVG Icon
482 *
483 * @since 0.0.1
484 * @param string $icon the icon name.
485 * @param string $class if the baseline class should be added.
486 * @param string $html Custom attributes inside svg wrapper.
487 * @return string
488 */
489 public static function fetch_svg( $icon = '', $class = '', $html = '' ) {
490 $class = $class ? ' ' . $class : '';
491
492 if ( ! self::$srfm_svgs ) {
493 ob_start();
494
495 include_once SRFM_DIR . 'assets/svg/svgs.json';
496 self::$srfm_svgs = json_decode( self::get_string_value( ob_get_clean() ), true );
497 self::$srfm_svgs = apply_filters( 'srfm_svg_icons', self::$srfm_svgs );
498 }
499
500 ob_start();
501 ?>
502 <span class="srfm-icon<?php echo esc_attr( $class ); ?>" <?php echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
503 <?php echo self::$srfm_svgs[ $icon ] ?? ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
504 </span>
505 <?php
506 $output = ob_get_clean();
507 return is_string( $output ) ? $output : '';
508 }
509
510 /**
511 * Encrypt data using base64.
512 *
513 * @param string $input The input string which needs to be encrypted.
514 * @since 0.0.1
515 * @return string The encrypted string.
516 */
517 public static function encrypt( $input ) {
518 // If the input is empty or not a string, then abandon ship.
519 if ( empty( $input ) || ! is_string( $input ) ) {
520 return '';
521 }
522
523 // Strip HTML tags to prevent them from being included in IDs and field names.
524 $input = wp_strip_all_tags( $input );
525
526 // Encrypt the input and return it.
527 $base_64 = base64_encode( $input ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
528 return rtrim( $base_64, '=' );
529 }
530
531 /**
532 * Decrypt data using base64.
533 *
534 * @param string $input The input string which needs to be decrypted.
535 * @since 0.0.1
536 * @return string The decrypted string.
537 */
538 public static function decrypt( $input ) {
539 // If the input is empty or not a string, then abandon ship.
540 if ( empty( $input ) || ! is_string( $input ) ) {
541 return '';
542 }
543
544 // Decrypt the input and return it.
545 $base_64 = $input . str_repeat( '=', strlen( $input ) % 4 );
546 return base64_decode( $base_64 ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode
547 }
548
549 /**
550 * Update an option from the database.
551 *
552 * @param string $key The option key.
553 * @param mixed $value The value to update.
554 * @param bool $network_override Whether to allow the network_override admin setting to be overridden on subsites.
555 * @since 0.0.1
556 * @return bool True if the option was updated, false otherwise.
557 */
558 public static function update_admin_settings_option( $key, $value, $network_override = false ) {
559 // Update the site-wide option if we're in the network admin, and return the updated status.
560 return $network_override && is_multisite() ? update_site_option( $key, $value ) : update_option( $key, $value );
561 }
562
563 /**
564 * Update an option from the database.
565 *
566 * @param int|string $post_id post id / form id.
567 * @param string $key meta key name.
568 * @param bool $single single or multiple.
569 * @param mixed $default default value.
570 *
571 * @since 0.0.1
572 * @return string Meta value.
573 */
574 public static function get_meta_value( $post_id, $key, $single = true, $default = '' ) {
575 $srfm_live_mode_data = self::get_instant_form_live_data();
576
577 if ( isset( $srfm_live_mode_data[ $key ] ) ) {
578 // Give priority to live mode data if we have one set from the Instant Form.
579 return self::get_string_value( $srfm_live_mode_data[ $key ] );
580 }
581
582 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 );
583 }
584
585 /**
586 * Wrapper for the WordPress's get_post_meta function with the support for default values.
587 *
588 * @param int|string $post_id Post ID.
589 * @param string $key The meta key to retrieve.
590 * @param mixed $default Default value.
591 * @param bool $single Optional. Whether to return a single value.
592 * @since 0.0.8
593 * @return mixed Meta value.
594 */
595 public static function get_post_meta( $post_id, $key, $default = null, $single = true ) {
596 $meta_value = get_post_meta( self::get_integer_value( $post_id ), $key, $single );
597 return $meta_value ? $meta_value : $default;
598 }
599
600 /**
601 * Returns query params data for instant form live preview.
602 *
603 * @since 0.0.8
604 * @return array<mixed> Live preview data.
605 */
606 public static function get_instant_form_live_data() {
607 $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.
608
609 return $srfm_live_mode_data ? array_map(
610 // Normalize falsy values.
611 static function( $live_data ) {
612 return 'false' === $live_data ? false : $live_data;
613 },
614 $srfm_live_mode_data
615 ) : [];
616 }
617
618 /**
619 * Default dynamic block value.
620 *
621 * @since 0.0.1
622 * @return array<string> Meta value.
623 */
624 public static function default_dynamic_block_option() {
625
626 $common_err_msg = self::get_common_err_msg();
627
628 $default_values = [
629 'srfm_url_block_required_text' => $common_err_msg['required'],
630 'srfm_input_block_required_text' => $common_err_msg['required'],
631 'srfm_input_block_unique_text' => $common_err_msg['unique'],
632 'srfm_address_block_required_text' => $common_err_msg['required'],
633 'srfm_phone_block_required_text' => $common_err_msg['required'],
634 'srfm_phone_block_unique_text' => $common_err_msg['unique'],
635 'srfm_number_block_required_text' => $common_err_msg['required'],
636 'srfm_textarea_block_required_text' => $common_err_msg['required'],
637 'srfm_multi_choice_block_required_text' => $common_err_msg['required'],
638 'srfm_checkbox_block_required_text' => $common_err_msg['required'],
639 'srfm_gdpr_block_required_text' => $common_err_msg['required'],
640 'srfm_email_block_required_text' => $common_err_msg['required'],
641 'srfm_email_block_unique_text' => $common_err_msg['unique'],
642 'srfm_dropdown_block_required_text' => $common_err_msg['required'],
643 'srfm_rating_block_required_text' => $common_err_msg['required'],
644 ];
645
646 $default_values = array_merge( $default_values, Translatable::dynamic_validation_messages() );
647
648 return apply_filters( 'srfm_default_dynamic_block_option', $default_values, $common_err_msg );
649 }
650
651 /**
652 * Get default dynamic block value.
653 *
654 * @param string $key meta key name.
655 * @since 0.0.1
656 * @return string Meta value.
657 */
658 public static function get_default_dynamic_block_option( $key ) {
659 $default_dynamic_values = self::default_dynamic_block_option();
660 $option = get_option( 'srfm_default_dynamic_block_option', $default_dynamic_values );
661
662 if ( is_array( $option ) && array_key_exists( $key, $option ) ) {
663 return $option[ $key ];
664 }
665 return '';
666 }
667
668 /**
669 * Checks whether a given request has appropriate permissions.
670 *
671 * @return true|WP_Error True if the request has read access, WP_Error object otherwise.
672 * @since 0.0.1
673 */
674 public static function get_items_permissions_check() {
675 if ( self::current_user_can() ) {
676 return true;
677 }
678
679 return new WP_Error(
680 'rest_cannot_view',
681 __( 'Sorry, you are not allowed to perform this action.', 'sureforms' ),
682 [ 'status' => \rest_authorization_required_code() ]
683 );
684 }
685
686 /**
687 * Check if the current user has a given capability.
688 *
689 * @param string $capability The capability to check.
690 * @param array<mixed> $args Optional. Additional arguments to pass to the capability check.
691 *
692 * @since 0.0.3
693 * @return bool Whether the current user has the given capability or role.
694 */
695 public static function current_user_can( $capability = '', $args = [] ) {
696 if ( ! function_exists( 'current_user_can' ) ) {
697 return false;
698 }
699
700 if ( ! is_string( $capability ) || empty( $capability ) ) {
701 $capability = 'manage_options';
702 }
703
704 return ! empty( $args ) && is_array( $args ) && count( $args ) > 0
705 ? current_user_can( $capability, ...$args )
706 : current_user_can( $capability );
707 }
708
709 /**
710 * Get all the entries for the given form ids. The entries are older than the given days_old.
711 *
712 * @param int $days_old The number of days old the entries should be.
713 * @param array<int> $sf_form_ids The form ids for which the entries need to be fetched.
714 * @since 0.0.2
715 * @return array<mixed> the entries matching the criteria.
716 */
717 public static function get_entries_from_form_ids( $days_old = 0, $sf_form_ids = [] ) {
718
719 $entries = [];
720 $days_old_date = ( new \DateTime() )->modify( "-{$days_old} days" )->format( 'Y-m-d H:i:s' );
721
722 foreach ( $sf_form_ids as $form_id ) {
723 // args according to the get_all() function in the Entries class.
724 $args = [
725 'where' => [
726 [
727 [
728 'key' => 'form_id',
729 'value' => $form_id,
730 'compare' => '=',
731 ],
732 [
733 'key' => 'created_at',
734 'value' => $days_old_date,
735 'compare' => '<=',
736 ],
737 ],
738 ],
739 ];
740
741 // store all the entries in a single array.
742 $entries = array_merge( $entries, Entries::get_all( $args, false ) );
743 }
744 return $entries;
745 }
746
747 /**
748 * Decode block attributes.
749 * The function reverses the effect of serialize_block_attributes()
750 *
751 * @link https://developer.wordpress.org/reference/functions/serialize_block_attributes/
752 * @param string $encoded_data the encoded block attribute.
753 * @since 0.0.2
754 * @return string decoded block attribute
755 */
756 public static function decode_block_attribute( $encoded_data = '' ) {
757 $decoded_data = preg_replace( '/\\\\u002d\\\\u002d/', '--', self::get_string_value( $encoded_data ) );
758 $decoded_data = preg_replace( '/\\\\u003c/', '<', self::get_string_value( $decoded_data ) );
759 $decoded_data = preg_replace( '/\\\\u003e/', '>', self::get_string_value( $decoded_data ) );
760 $decoded_data = preg_replace( '/\\\\u0026/', '&', self::get_string_value( $decoded_data ) );
761 $decoded_data = preg_replace( '/\\\\\\\\"/', '"', self::get_string_value( $decoded_data ) );
762 return self::get_string_value( $decoded_data );
763 }
764
765 /**
766 * Map slugs to submission data.
767 *
768 * @param array<mixed> $submission_data submission_data.
769 * @since 0.0.3
770 * @return array<mixed>
771 */
772 public static function map_slug_to_submission_data( $submission_data = [] ) {
773 $mapped_data = [];
774 foreach ( $submission_data as $key => $value ) {
775 if ( false === strpos( $key, '-lbl-' ) ) {
776 continue;
777 }
778 $label = explode( '-lbl-', $key )[1];
779 $slug = implode( '-', array_slice( explode( '-', $label ), 1 ) );
780 $slug = str_replace( ' ', '_', $slug );
781
782 /**
783 * Filters whether a field should be skipped when mapping slugs to submission data.
784 *
785 * This filter allows plugins or custom code to determine if a field should be excluded
786 * from the mapped submission data array (such as for internal fields or extraneous meta).
787 *
788 * @since 2.0.0
789 *
790 * @param bool $skip_this_field Whether to skip this field from processing. Default false.
791 * @param array $args {
792 * Arguments used for this field.
793 *
794 * @type string $key The original key of the field in the submission data array.
795 * @type string $slug The mapped slug parsed from the field key.
796 * @type mixed $value The value assigned to this field.
797 * }
798 */
799 $skip_this_field = apply_filters(
800 'srfm_map_slug_to_submission_data_should_skip',
801 false,
802 [
803 'key' => $key,
804 'slug' => $slug,
805 'value' => $value,
806 ]
807 );
808
809 if ( $skip_this_field ) {
810 continue;
811 }
812
813 // Check if value is array to handle external package field functionality.
814 // like repeater fields that need special processing.
815 if ( is_array( $value ) && ! empty( $value ) ) {
816 // Apply filter to allow external packages to process array values.
817 // Returns processed data with 'is_processed' flag if successfully handled.
818 $filtered_submission_data = apply_filters(
819 'srfm_map_slug_to_submission_data_array',
820 [
821 'value' => $value,
822 'key' => $key,
823 'slug' => $slug,
824 ]
825 );
826 if ( isset( $filtered_submission_data['is_processed'] ) && true === $filtered_submission_data['is_processed'] ) {
827 $mapped_data[ $slug ] = $filtered_submission_data['value'];
828 continue;
829 }
830 }
831
832 // If the value is an array (e.g. multi-upload field), decode each URL value.
833 if ( is_array( $value ) ) {
834 $mapped_data[ $slug ] = array_map(
835 static function ( $val ) {
836 return is_string( $val ) ? rawurldecode( $val ) : $val;
837 },
838 $value
839 );
840 continue;
841 }
842
843 $mapped_data[ $slug ] = is_string( $value ) ? html_entity_decode( esc_attr( $value ) ) : $value;
844 }
845 return $mapped_data;
846 }
847
848 /**
849 * Get forms options. Shows all the available forms in the dropdown.
850 *
851 * @since 0.0.5
852 * @param string $key Determines the type of data to return.
853 * @return array<mixed>
854 */
855 public static function get_sureforms( $key = '' ) {
856 $forms = get_posts(
857 apply_filters(
858 'srfm_get_sureforms_query_args',
859 [
860 'post_type' => SRFM_FORMS_POST_TYPE,
861 'posts_per_page' => -1,
862 'post_status' => 'publish',
863 ]
864 )
865 );
866
867 $options = [];
868
869 foreach ( $forms as $form ) {
870 if ( $form instanceof WP_Post ) {
871 if ( 'all' === $key ) {
872 $options[ $form->ID ] = $form;
873 } elseif ( ! empty( $key ) && is_string( $key ) && isset( $form->$key ) ) {
874 $options[ $form->ID ] = $form->$key;
875 } else {
876 $options[ $form->ID ] = $form->post_title;
877 }
878 }
879 }
880
881 return $options;
882 }
883
884 /**
885 * Get all the forms.
886 *
887 * @since 0.0.5
888 * @return array<mixed>
889 */
890 public static function get_sureforms_title_with_ids() {
891 $form_options = self::get_sureforms();
892
893 foreach ( $form_options as $key => $value ) {
894 $form_options[ $key ] = $value . ' #' . $key;
895 }
896
897 return $form_options;
898 }
899
900 /**
901 * Get the CSS variables based on different field spacing sizes.
902 *
903 * @param string|null $field_spacing The field spacing size or boolean false to return complete sizes array.
904 *
905 * @since 0.0.7
906 * @return array<string|mixed>
907 */
908 public static function get_css_vars( $field_spacing = null ) {
909 /**
910 * $sizes - Field Spacing Sizes Variables.
911 * The array contains the CSS variables for different field spacing sizes.
912 * Each key corresponds to the field spacing size, and the value is an array of CSS variables.
913 *
914 * For future variables depending on the field spacing size, add the variable to the array respectively.
915 */
916 $sizes = apply_filters(
917 'srfm_css_vars_sizes',
918 [
919 'small' => [
920 '--srfm-row-gap-between-blocks' => '16px',
921 // Address block gap and spacing variables.
922 '--srfm-address-label-font-size' => '14px',
923 '--srfm-address-label-line-height' => '20px',
924 '--srfm-address-description-font-size' => '12px',
925 '--srfm-address-description-line-height' => '16px',
926 '--srfm-col-gap-between-fields' => '12px',
927 '--srfm-row-gap-between-fields' => '12px',
928 '--srfm-gap-below-address-label' => '12px',
929 // Dropdown Variables.
930 '--srfm-dropdown-font-size' => '14px',
931 '--srfm-dropdown-gap-between-input-menu' => '4px',
932 '--srfm-dropdown-badge-padding' => '2px 6px',
933 '--srfm-dropdown-multiselect-font-size' => '12px',
934 '--srfm-dropdown-multiselect-line-height' => '16px',
935 '--srfm-dropdown-padding-right' => '12px',
936 // initial padding and from 20px - 12px for dropdown arrow width and 8px for gap before dropdown arrow.
937 '--srfm-dropdown-padding-right-icon' => 'calc( var( --srfm-dropdown-padding-right ) + 20px )',
938 '--srfm-dropdown-multiselect-padding' => '8px var( --srfm-dropdown-padding-right-icon ) 8px 8px',
939 // Input Field Variables.
940 '--srfm-input-height' => '40px',
941 '--srfm-input-field-padding' => '10px 12px',
942 '--srfm-input-field-font-size' => '14px',
943 '--srfm-input-field-line-height' => '20px',
944 '--srfm-input-field-margin-top' => '4px',
945 '--srfm-input-field-margin-bottom' => '4px',
946 // Checkbox and GDPR Variables.
947 '--srfm-checkbox-label-font-size' => '14px',
948 '--srfm-checkbox-label-line-height' => '20px',
949 '--srfm-checkbox-description-font-size' => '12px',
950 '--srfm-checkbox-description-line-height' => '16px',
951 '--srfm-check-ctn-width' => '16px',
952 '--srfm-check-ctn-height' => '16px',
953 '--srfm-check-svg-size' => '10px',
954 '--srfm-checkbox-margin-top-frontend' => '2px',
955 '--srfm-checkbox-margin-top-editor' => '3px',
956 '--srfm-check-gap' => '8px',
957 '--srfm-checkbox-description-margin-left' => '24px',
958 // Phone Number field variables.
959 '--srfm-flag-section-padding' => '10px 0 10px 12px',
960 '--srfm-gap-between-icon-text' => '8px',
961 // Label Variables.
962 '--srfm-label-font-size' => '14px',
963 '--srfm-label-line-height' => '20px',
964 // Description Variables.
965 '--srfm-description-font-size' => '12px',
966 '--srfm-description-line-height' => '16px',
967 // Button Variables.
968 '--srfm-btn-padding' => '8px 14px',
969 '--srfm-btn-font-size' => '14px',
970 '--srfm-btn-line-height' => '20px',
971 // Multi Choice Variables.
972 '--srfm-multi-choice-horizontal-padding' => '16px',
973 '--srfm-multi-choice-vertical-padding' => '16px',
974 '--srfm-multi-choice-internal-option-gap' => '8px',
975 '--srfm-multi-choice-vertical-svg-size' => '32px',
976 '--srfm-multi-choice-horizontal-image-size' => '20px',
977 '--srfm-multi-choice-vertical-image-size' => '100px',
978 '--srfm-multi-choice-outer-padding' => '0',
979 ],
980 'medium' => [
981 '--srfm-row-gap-between-blocks' => '18px',
982 // Address block gap and spacing variables.
983 '--srfm-address-label-font-size' => '16px',
984 '--srfm-address-label-line-height' => '24px',
985 '--srfm-address-description-font-size' => '14px',
986 '--srfm-address-description-line-height' => '20px',
987 '--srfm-col-gap-between-fields' => '16px',
988 '--srfm-row-gap-between-fields' => '16px',
989 '--srfm-gap-below-address-label' => '14px',
990 // Input Field Variables.
991 '--srfm-input-height' => '44px',
992 '--srfm-input-field-font-size' => '16px',
993 '--srfm-input-field-line-height' => '24px',
994 '--srfm-input-field-margin-top' => '6px',
995 '--srfm-input-field-margin-bottom' => '6px',
996 // Checkbox and GDPR Variables.
997 '--srfm-checkbox-label-font-size' => '16px',
998 '--srfm-checkbox-label-line-height' => '24px',
999 '--srfm-checkbox-description-font-size' => '14px',
1000 '--srfm-checkbox-description-line-height' => '20px',
1001 '--srfm-checkbox-margin-top-frontend' => '4px',
1002 '--srfm-checkbox-margin-top-editor' => '6px',
1003 '--srfm-checkbox-description-margin-left' => '24px',
1004 // Label Variables.
1005 '--srfm-label-font-size' => '16px',
1006 '--srfm-label-line-height' => '24px',
1007 // Description Variables.
1008 '--srfm-description-font-size' => '14px',
1009 '--srfm-description-line-height' => '20px',
1010 // Button Variables.
1011 '--srfm-btn-padding' => '10px 14px',
1012 '--srfm-btn-font-size' => '16px',
1013 '--srfm-btn-line-height' => '24px',
1014 // Multi Choice Variables.
1015 '--srfm-multi-choice-horizontal-padding' => '20px',
1016 '--srfm-multi-choice-vertical-padding' => '20px',
1017 '--srfm-multi-choice-vertical-svg-size' => '40px',
1018 '--srfm-multi-choice-horizontal-image-size' => '24px',
1019 '--srfm-multi-choice-vertical-image-size' => '120px',
1020 '--srfm-multi-choice-outer-padding' => '2px',
1021 ],
1022 'large' => [
1023 '--srfm-row-gap-between-blocks' => '20px',
1024 // Address Block Gap and Spacing Variables.
1025 '--srfm-address-label-font-size' => '18px',
1026 '--srfm-address-label-line-height' => '28px',
1027 '--srfm-address-description-font-size' => '16px',
1028 '--srfm-address-description-line-height' => '24px',
1029 '--srfm-col-gap-between-fields' => '16px',
1030 '--srfm-row-gap-between-fields' => '20px',
1031 '--srfm-gap-below-address-label' => '16px',
1032 // Dropdown Variables.
1033 '--srfm-dropdown-font-size' => '16px',
1034 '--srfm-dropdown-gap-between-input-menu' => '6px',
1035 '--srfm-dropdown-badge-padding' => '6px 6px',
1036 '--srfm-dropdown-multiselect-font-size' => '14px',
1037 '--srfm-dropdown-multiselect-line-height' => '20px',
1038 '--srfm-dropdown-padding-right' => '14px',
1039 // Input Field Variables.
1040 '--srfm-input-height' => '48px',
1041 '--srfm-input-field-padding' => '10px 14px',
1042 '--srfm-input-field-font-size' => '18px',
1043 '--srfm-input-field-line-height' => '28px',
1044 '--srfm-input-field-margin-top' => '8px',
1045 '--srfm-input-field-margin-bottom' => '8px',
1046 // Checkbox and GDPR Variables.
1047 '--srfm-checkbox-label-font-size' => '18px',
1048 '--srfm-checkbox-label-line-height' => '28px',
1049 '--srfm-checkbox-description-font-size' => '16px',
1050 '--srfm-checkbox-description-line-height' => '24px',
1051 '--srfm-check-ctn-width' => '20px',
1052 '--srfm-check-ctn-height' => '20px',
1053 '--srfm-check-svg-size' => '14px',
1054 '--srfm-check-gap' => '10px',
1055 '--srfm-checkbox-margin-top-frontend' => '4px',
1056 '--srfm-checkbox-margin-top-editor' => '5px',
1057 '--srfm-checkbox-description-margin-left' => '30px',
1058 // Label Variables.
1059 '--srfm-label-font-size' => '18px',
1060 '--srfm-label-line-height' => '28px',
1061 // Description Variables.
1062 '--srfm-description-font-size' => '16px',
1063 '--srfm-description-line-height' => '24px',
1064 // Button Variables.
1065 '--srfm-btn-padding' => '10px 14px',
1066 '--srfm-btn-font-size' => '18px',
1067 '--srfm-btn-line-height' => '28px',
1068 // Multi Choice Variables.
1069 '--srfm-multi-choice-horizontal-padding' => '24px',
1070 '--srfm-multi-choice-vertical-padding' => '24px',
1071 '--srfm-multi-choice-internal-option-gap' => '12px',
1072 '--srfm-multi-choice-vertical-svg-size' => '48px',
1073 '--srfm-multi-choice-horizontal-image-size' => '28px',
1074 '--srfm-multi-choice-vertical-image-size' => '140px',
1075 '--srfm-multi-choice-outer-padding' => '4px',
1076 ],
1077 ]
1078 );
1079 // Return complete sizes array if field_spacing is false. Required in case of JS for Editor changes.
1080 if ( ! $field_spacing ) {
1081 return $sizes;
1082 }
1083
1084 $selected_size = $sizes['small'];
1085 if ( 'small' !== $field_spacing && isset( $sizes[ $field_spacing ] ) ) {
1086 $selected_size = array_merge( $selected_size, $sizes[ $field_spacing ] );
1087 }
1088
1089 return $selected_size;
1090 }
1091
1092 /**
1093 * Array of SureForms blocks which get have user input.
1094 *
1095 * @since 0.0.10
1096 * @return array<string>
1097 */
1098 public static function get_sureforms_blocks() {
1099 return apply_filters(
1100 'srfm_blocks',
1101 [
1102 'srfm/input',
1103 'srfm/email',
1104 'srfm/textarea',
1105 'srfm/number',
1106 'srfm/checkbox',
1107 'srfm/gdpr',
1108 'srfm/phone',
1109 'srfm/address',
1110 'srfm/dropdown',
1111 'srfm/multi-choice',
1112 'srfm/radio',
1113 'srfm/submit',
1114 'srfm/url',
1115 'srfm/payment',
1116 ]
1117 );
1118 }
1119
1120 /**
1121 * Render a site key missing error message.
1122 *
1123 * @param string $provider_name Name of the captcha provider (e.g., HCaptcha, Google reCAPTCHA, Turnstile).
1124 * @since 1.7.0
1125 * @since 1.7.1 moved to inc/helper.php from inc/generate-form-markup.php
1126 * @return void
1127 */
1128 public static function render_missing_sitekey_error( $provider_name ) {
1129 $icon = self::fetch_svg( 'info_circle', '', 'aria-hidden="true"' );
1130 ?>
1131 <p id="sitekey-error" class="srfm-common-error-message srfm-error-message">
1132 <?php echo wp_kses( $icon, self::$allowed_tags_svg ); ?>
1133 <span class="srfm-error-content">
1134 <?php
1135 echo esc_html(
1136 sprintf(
1137 /* translators: %s: Provider name like HCaptcha, Google reCAPTCHA, Turnstile */
1138 __( '%s sitekey is missing. Please contact your site administrator.', 'sureforms' ),
1139 $provider_name
1140 )
1141 );
1142 ?>
1143 </span>
1144 </p>
1145 <?php
1146 }
1147
1148 /**
1149 * Parse and sanitize an email list string which may contain:
1150 *
1151 * @param string $input email addresses.
1152 * @since 1.13.2
1153 * @return string Sanitized email header string.
1154 */
1155 public static function sanitize_email_header( $input ) {
1156 if ( empty( $input ) ) {
1157 return '';
1158 }
1159
1160 $parts = explode( ',', $input );
1161 $output = [];
1162
1163 foreach ( $parts as $part ) {
1164 $part = trim( $part );
1165
1166 // Match "Name <email>".
1167 if ( preg_match( '/^(.*)<(.+)>$/', $part, $matches ) ) {
1168 $name = trim( $matches[1], "\" \t\n\r\0\x0B" ); // trim quotes.
1169 $email = sanitize_email( trim( $matches[2] ) );
1170
1171 if ( is_email( $email ) ) {
1172 $safe_name = sanitize_text_field( $name );
1173 $output[] = $safe_name . ' <' . $email . '>';
1174 }
1175 } else {
1176 // Plain email case.
1177 $email = sanitize_email( $part );
1178 if ( is_email( $email ) ) {
1179 $output[] = $email;
1180 }
1181 }
1182 }
1183
1184 return ! empty( $output ) ? implode( ', ', $output ) : '';
1185 }
1186
1187 /**
1188 * Process blocks and inner blocks.
1189 *
1190 * @param array<mixed> $blocks The block data.
1191 * @param array<string> $slugs The array of existing slugs.
1192 * @param bool $updated The array of existing slugs.
1193 * @param string $prefix The array of existing slugs.
1194 * @param bool $skip_checking_existing_slug Skips the checking of existing slug if passed true. More information documented inside this function.
1195 * @since 0.0.10
1196 * @return array
1197 */
1198 public static function process_blocks( $blocks, &$slugs, &$updated, $prefix = '', $skip_checking_existing_slug = false ) {
1199
1200 if ( ! is_array( $blocks ) ) {
1201 return [ $blocks, $slugs, $updated ];
1202 }
1203
1204 foreach ( $blocks as $index => $block ) {
1205
1206 if ( ! is_array( $block ) ) {
1207 continue;
1208 }
1209 // Checking only for SureForms blocks which can have user input.
1210 if ( empty( $block['blockName'] ) || ! in_array( $block['blockName'], self::get_sureforms_blocks(), true ) ) {
1211 continue;
1212 }
1213
1214 /**
1215 * Lets continue if slug already exists.
1216 * This will ensure that we don't update already existing slugs.
1217 */
1218 if ( isset( $block['attrs'] ) && ! empty( $block['attrs']['slug'] ) && ! in_array( $block['attrs']['slug'], $slugs, true ) ) {
1219
1220 // Made it associative array, so that we can directly check it using block_id rather than mapping or using "in_array" for the checks.
1221 $slugs[ $block['attrs']['block_id'] ] = self::get_string_value( $block['attrs']['slug'] );
1222
1223 if ( is_array( $block['innerBlocks'] ) && ! empty( $block['innerBlocks'] ) ) {
1224 [ $blocks[ $index ]['innerBlocks'], $slugs, $updated ] = self::process_blocks( $block['innerBlocks'], $slugs, $updated, '' );
1225 }
1226 continue;
1227 }
1228
1229 if ( $skip_checking_existing_slug && empty( $block['innerBlocks'] ) && isset( $slugs[ $block['attrs']['block_id'] ] ) ) {
1230 /**
1231 * Skip re-processing of the already process or existing slugs if above parameter "$skip_checking_existing_slug" is passed as true.
1232 * This is helpful in the scenarios where we need to compare and verify between already saved blocks and new unsaved blocks parsed
1233 * from the contents.
1234 *
1235 * However, it is also necessary to make sure if that current block is not a parent / wrapper block
1236 * by checking "$block['innerBlocks']" empty.
1237 *
1238 * And finally, checking if the block-id "$block['attrs']['block_id']" is already set in the list of "$slugs",
1239 * making sure that we are only processing the new blocks.
1240 */
1241 continue;
1242 }
1243
1244 if ( is_array( $blocks[ $index ]['attrs'] ) ) {
1245
1246 $blocks[ $index ]['attrs']['slug'] = self::generate_unique_block_slug( $block, $slugs, $prefix );
1247 $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.
1248 $updated = true;
1249 if ( is_array( $block['innerBlocks'] ) && ! empty( $block['innerBlocks'] ) ) {
1250
1251 [ $blocks[ $index ]['innerBlocks'], $slugs, $updated ] = self::process_blocks( $block['innerBlocks'], $slugs, $updated, $blocks[ $index ]['attrs']['slug'] );
1252
1253 }
1254 }
1255 }
1256 return [ $blocks, $slugs, $updated ];
1257 }
1258
1259 /**
1260 * Generates slug based on the provided block and existing slugs.
1261 *
1262 * @param array<mixed> $block The block data.
1263 * @param array<string> $slugs The array of existing slugs.
1264 * @param string $prefix The array of existing slugs.
1265 * @since 0.0.10
1266 * @return string The generated unique block slug.
1267 */
1268 public static function generate_unique_block_slug( $block, $slugs, $prefix ) {
1269 $slug = is_string( $block['blockName'] ) ? $block['blockName'] : '';
1270
1271 if ( ! empty( $block['attrs']['label'] ) && is_string( $block['attrs']['label'] ) ) {
1272 $slug = sanitize_title( $block['attrs']['label'] );
1273
1274 // If the label contains non-Latin characters (e.g. Japanese, Chinese),
1275 // sanitize_title() produces a percent-encoded slug like "%e3%83%95%e3%83%aa".
1276 // These are unstable and break conditional logic field matching.
1277 // Fall back to the block name to ensure a stable ASCII slug.
1278 if ( false !== strpos( $slug, '%' ) ) {
1279 $block_name = is_string( $block['blockName'] ) ? $block['blockName'] : '';
1280 // Strip the 'srfm/' namespace to match JS-side cleanForSlug() output.
1281 $block_name = (string) preg_replace( '/^srfm\//', '', $block_name );
1282 $slug = sanitize_title( $block_name );
1283 }
1284 }
1285
1286 if ( ! empty( $prefix ) ) {
1287 $slug = $prefix . '-' . $slug;
1288 }
1289
1290 return self::generate_slug( $slug, $slugs );
1291 }
1292
1293 /**
1294 * This function ensures that the slug is unique.
1295 * If the slug is already taken, it appends a number to the slug to make it unique.
1296 *
1297 * @param string $slug test to be converted to slug.
1298 * @param array<string> $slugs An array of existing slugs.
1299 * @since 0.0.10
1300 * @return string The unique slug.
1301 */
1302 public static function generate_slug( $slug, $slugs ) {
1303 $slug = sanitize_title( $slug );
1304
1305 if ( ! in_array( $slug, $slugs, true ) ) {
1306 return $slug;
1307 }
1308
1309 $index = 1;
1310
1311 while ( in_array( $slug . '-' . $index, $slugs, true ) ) {
1312 $index++;
1313 }
1314
1315 return $slug . '-' . $index;
1316 }
1317
1318 /**
1319 * Encode data to JSON. This function will encode the data with JSON_UNESCAPED_SLASHES and JSON_UNESCAPED_UNICODE.
1320 *
1321 * @since 0.0.11
1322 * @param array<mixed> $data The data to encode.
1323 * @return string|false The JSON representation of the value on success or false on failure.
1324 */
1325 public static function encode_json( $data ) {
1326 return wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE );
1327 }
1328
1329 /**
1330 * Returns true if SureTriggers plugin is ready for the custom app.
1331 *
1332 * @since 1.0.3
1333 * @return bool Returns true if SureTriggers plugin is ready for the custom app.
1334 */
1335 public static function is_suretriggers_ready() {
1336 if ( ! defined( 'SURE_TRIGGERS_FILE' ) ) {
1337 // Probably plugin is de-activated or not installed at all.
1338 return false;
1339 }
1340
1341 $suretriggers_data = get_option( 'suretrigger_options', [] );
1342 if ( ! is_array( $suretriggers_data ) || empty( $suretriggers_data['secret_key'] ) || ! is_string( $suretriggers_data['secret_key'] ) ) {
1343 // SureTriggers is not authenticated yet.
1344 return false;
1345 }
1346
1347 return true;
1348 }
1349
1350 /**
1351 * Registers script translations for a specific handle.
1352 *
1353 * This function sets the script translations for a given script handle, allowing
1354 * localization of JavaScript strings using the specified text domain and path.
1355 *
1356 * @param string $handle The script handle to apply translations to.
1357 * @param string $domain Optional. The text domain for translations. Default is 'sureforms'.
1358 * @param string $path Optional. The path to the translation files. Default is the 'languages' folder in the SureForms directory.
1359 *
1360 * @since 1.0.5
1361 * @return void
1362 */
1363 public static function register_script_translations( $handle, $domain = 'sureforms', $path = SRFM_DIR . 'languages' ) {
1364 wp_set_script_translations( $handle, $domain, $path );
1365 }
1366
1367 /**
1368 * Validates whether the specified conditions or a single key-value pair exist in the request context.
1369 *
1370 * - If `$conditions` is provided as an array, it will validate all key-value pairs in `$conditions`
1371 * against the `$_REQUEST` superglobal.
1372 * - If `$conditions` is empty, it validates a single key-value pair from `$key` and `$value`.
1373 *
1374 * @param string $value The expected value to match in the request if `$conditions` is not used.
1375 * @param string $key The key to check for in the request if `$conditions` is not used.
1376 * @param array<string, string> $conditions An optional associative array of key-value pairs to validate.
1377 * @since 1.1.1
1378 * @return bool Returns true if all conditions are met or the single key-value pair is valid, otherwise false.
1379 */
1380 public static function validate_request_context( $value, $key = 'post_type', $conditions = [] ) {
1381 // If conditions are provided, validate all key-value pairs in the conditions array.
1382 if ( ! empty( $conditions ) ) {
1383 foreach ( $conditions as $condition_key => $condition_value ) {
1384 if ( ! isset( $_REQUEST[ $condition_key ] ) || $_REQUEST[ $condition_key ] !== $condition_value ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a controlled comparison of request values.
1385 // Return false if any condition is not satisfied.
1386 return false;
1387 }
1388 }
1389 // Return true if all conditions are satisfied.
1390 return true;
1391 }
1392
1393 // Validate $value and $key when no conditions are provided.
1394 if ( empty( $key ) || empty( $value ) ) {
1395 return false;
1396 }
1397
1398 // Validate a single key-value pair when no conditions are provided.
1399 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.
1400 }
1401
1402 /**
1403 * Retrieve the list of excluded fields for form data processing.
1404 *
1405 * This method returns an array of field keys that should be excluded when
1406 * processing form data.
1407 *
1408 * @since 1.1.1
1409 * @return array<string> Returns the string array of excluded fields.
1410 */
1411 public static function get_excluded_fields() {
1412 $excluded_fields = [ 'srfm-honeypot-field', 'g-recaptcha-response', 'srfm-sender-email-field', 'form-id' ];
1413
1414 return apply_filters( 'srfm_excluded_fields', $excluded_fields );
1415 }
1416
1417 /**
1418 * Check whether the current page is a SureForms admin page.
1419 *
1420 * @since 1.2.2
1421 * @return bool Returns true if the current page is a SureForms admin page, otherwise false.
1422 */
1423 public static function is_sureforms_admin_page() {
1424 $current_screen = get_current_screen();
1425 $is_screen_sureforms_menu = self::validate_request_context( 'sureforms_menu', 'page' );
1426 $is_screen_add_new_form = self::validate_request_context( 'add-new-form', 'page' );
1427 $is_screen_sureforms_form_settings = self::validate_request_context( 'sureforms_form_settings', 'page' );
1428 $is_screen_sureforms_entries = self::validate_request_context( SRFM_ENTRIES, 'page' );
1429 $is_post_type_sureforms_form = $current_screen && SRFM_FORMS_POST_TYPE === $current_screen->post_type;
1430
1431 return $is_screen_sureforms_menu || $is_screen_add_new_form || $is_screen_sureforms_form_settings || $is_screen_sureforms_entries || $is_post_type_sureforms_form;
1432 }
1433
1434 /**
1435 * Filters and concatenates valid class names from an array.
1436 *
1437 * @param array<string> $class_names The array containing potential class names.
1438 * @since 1.4.0
1439 * @return string The concatenated string of valid class names separated by spaces.
1440 */
1441 public static function join_strings( $class_names ) {
1442 // Filter the array to include only valid class names.
1443 $valid_class_names = array_filter(
1444 $class_names,
1445 static function ( $value ) {
1446 return is_string( $value ) && '' !== $value && false !== $value;
1447 }
1448 );
1449
1450 // Concatenate the valid class names with spaces and return.
1451 return implode( ' ', $valid_class_names );
1452 }
1453 /**
1454 * Get SureForms Website URL.
1455 *
1456 * @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.
1457 * @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'].
1458 * @since 0.0.7
1459 * @return string
1460 */
1461 public static function get_sureforms_website_url( $trail, $utm_args = [] ) {
1462 $url = SRFM_WEBSITE;
1463 if ( ! empty( $trail ) && is_string( $trail ) ) {
1464 $url = SRFM_WEBSITE . $trail;
1465 }
1466
1467 if ( ! is_array( $utm_args ) ) {
1468 $utm_args = [];
1469 }
1470
1471 // SRFM-2709: deterministic UTM attribution — start.
1472 // When the caller opts into UTM tracking by passing any utm_args, fill in
1473 // SureForms' deterministic source/campaign defaults. Caller-provided keys
1474 // (including the placement passed via utm_medium) always win.
1475 if ( ! empty( $utm_args ) ) {
1476 $utm_args = array_merge(
1477 [
1478 'utm_source' => 'sureforms_plugin',
1479 'utm_campaign' => 'core_plugin',
1480 ],
1481 $utm_args
1482 );
1483 }
1484 // SRFM-2709: deterministic UTM attribution — end.
1485
1486 if ( class_exists( 'BSF_UTM_Analytics' ) ) {
1487 $url = \BSF_UTM_Analytics::get_utm_ready_link( $url, 'sureforms', $utm_args );
1488 }
1489
1490 // SRFM-2709: post-BSF_UTM_Analytics fallback — start.
1491 // BSF_UTM_Analytics returns the URL unchanged when no install referer is
1492 // recorded. Merge any caller UTM keys still missing from the final URL.
1493 if ( ! empty( $utm_args ) ) {
1494 $existing = [];
1495 $query = wp_parse_url( $url, PHP_URL_QUERY );
1496 if ( is_string( $query ) && '' !== $query ) {
1497 parse_str( $query, $existing );
1498 }
1499 $missing = array_diff_key( $utm_args, $existing );
1500 if ( ! empty( $missing ) ) {
1501 $url = add_query_arg( $missing, $url );
1502 }
1503 }
1504 // SRFM-2709: post-BSF_UTM_Analytics fallback — end.
1505
1506 return esc_url( $url );
1507 }
1508
1509 /**
1510 * Validates if the given string is a valid CSS class name.
1511 *
1512 * A valid CSS class name:
1513 * - Does not start with a digit, hyphen, or underscore.
1514 * - Can contain alphanumeric characters, underscores, hyphens, and Unicode letters.
1515 *
1516 * @param string $class_name The class name to validate.
1517 *
1518 * @since 1.3.1
1519 * @return bool True if the class name is valid, otherwise false.
1520 */
1521 public static function is_valid_css_class_name( $class_name ) {
1522 // Regular expression to validate a Unicode-aware CSS class name.
1523 $class_name_regex = '/^[^\d\-_][\w\p{L}\p{N}\-_]*$/u';
1524
1525 // Check if the className matches the pattern.
1526 return preg_match( $class_name_regex, $class_name ) === 1;
1527 }
1528
1529 /**
1530 * Get the gradient css for given gradient parameters.
1531 *
1532 * @param string $type The type of gradient. Default 'linear'.
1533 * @param string $color1 The first color of the gradient. Default '#FFC9B2'.
1534 * @param string $color2 The second color of the gradient. Default '#C7CBFF'.
1535 * @param int $loc1 The location of the first color. Default 0.
1536 * @param int $loc2 The location of the second color. Default 100.
1537 * @param int $angle The angle of the gradient. Default 90.
1538 *
1539 * @since 1.4.4
1540 * @return string The gradient css.
1541 */
1542 public static function get_gradient_css( $type = 'linear', $color1 = '#FFC9B2', $color2 = '#C7CBFF', $loc1 = 0, $loc2 = 100, $angle = 90 ) {
1543 if ( 'linear' === $type ) {
1544 return "linear-gradient({$angle}deg, {$color1} {$loc1}%, {$color2} {$loc2}%)";
1545 }
1546 return "radial-gradient({$color1} {$loc1}%, {$color2} {$loc2}%)";
1547 }
1548
1549 /**
1550 * Return the classes based on background and overlay type to add to the form container.
1551 *
1552 * @param string $background_type The background type.
1553 * @param string $overlay_type The overlay type.
1554 * @param string $bg_image The background image url.
1555 *
1556 * @since 1.4.4
1557 * @return string The classes to add to the form container.
1558 */
1559 public static function get_background_classes( $background_type, $overlay_type, $bg_image = '' ) {
1560 if ( empty( $background_type ) ) {
1561 $background_type = 'color';
1562 }
1563
1564 $background_type_class = '';
1565 $overlay_class = 'image' === $background_type && ! empty( $bg_image ) && $overlay_type ? "srfm-overlay-{$overlay_type}" : '';
1566
1567 // Set the class based on the background type.
1568 switch ( $background_type ) {
1569 case 'image':
1570 $background_type_class = 'srfm-bg-image';
1571 break;
1572 case 'gradient':
1573 $background_type_class = 'srfm-bg-gradient';
1574 break;
1575 default:
1576 $background_type_class = 'srfm-bg-color';
1577 break;
1578 }
1579
1580 return self::join_strings( [ $background_type_class, $overlay_class ] );
1581 }
1582
1583 /**
1584 * Custom escape function for the textarea with rich text support.
1585 *
1586 * @param string $content The content submitted by the user in the textarea block.
1587 * @since 1.7.1
1588 *
1589 * @return string Escaped content.
1590 */
1591 public static function esc_textarea( $content ) {
1592 $content = wpautop( self::sanitize_textarea( $content ) );
1593
1594 return trim( str_replace( [ "\r\n", "\r", "\n" ], '', $content ) );
1595 }
1596
1597 /**
1598 * Custom sanitization function for the textarea with rich text support.
1599 *
1600 * @param string $content The content submitted by the user in the textarea block.
1601 * @since 1.7.1
1602 *
1603 * @return string Sanitized content.
1604 */
1605 public static function sanitize_textarea( $content ) {
1606 $count = 1;
1607 $content = convert_invalid_entities( $content );
1608
1609 // Remove the 'script' and 'style' tags recursively from the content.
1610 while ( $count ) {
1611 $content = preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', self::get_string_value( $content ), - 1, $count );
1612 }
1613
1614 // Disable the safe style attribute parsing for the textarea block.
1615 add_filter( 'safe_style_css', [ self::class, 'disable_style_attr_parsing' ], 10, 1 );
1616 $content = wp_kses_post( self::get_string_value( $content ) );
1617
1618 // Remove the filter after sanitization to avoid affecting other blocks.
1619 remove_filter( 'safe_style_css', [ self::class, 'disable_style_attr_parsing' ], 10 );
1620
1621 // Ensure all tags are balanced.
1622 return force_balance_tags( $content );
1623 }
1624
1625 /**
1626 * Disable parsing of style attributes for the textarea block.
1627 *
1628 * @param array<string> $allowed_styles The allowed styles.
1629 * @since 1.7.1
1630 *
1631 * @return array An empty array to disable style attribute parsing.
1632 */
1633 public static function disable_style_attr_parsing( $allowed_styles ) {
1634 unset( $allowed_styles );
1635 // Disable parsing of style attributes.
1636 return [];
1637 }
1638 /**
1639 * Strips JavaScript attributes from HTML content.
1640 *
1641 * @param string $html The HTML content to process.
1642 * @param bool $remove_link_target Optional. When true, removes target and strips noopener/noreferrer from rel on links. Default false.
1643 * @since 1.7.1
1644 * @since 2.5.2 Added $remove_link_target parameter.
1645 * @return string The cleaned HTML content without JavaScript attributes.
1646 */
1647 public static function strip_js_attributes( $html, $remove_link_target = false ) {
1648 $dom = new \DOMDocument();
1649
1650 // Suppress warnings due to malformed HTML.
1651 libxml_use_internal_errors( true );
1652 $loaded = $dom->loadHTML( '<?xml encoding="utf-8" ?>' . $html );
1653 libxml_clear_errors();
1654
1655 if ( ! $loaded ) {
1656 return $html; // Return original HTML if loading fails.
1657 }
1658
1659 $xpath = new \DOMXPath( $dom );
1660
1661 // 1. Remove all <script> tags.
1662 $script_nodes = $xpath->query( '//script' );
1663 if ( $script_nodes instanceof \DOMNodeList ) {
1664 foreach ( $script_nodes as $script ) {
1665 // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- This is a DOM element.
1666 $parent_node = $script->parentNode;
1667 if ( $parent_node instanceof \DOMNode ) {
1668 $parent_node->removeChild( $script );
1669 }
1670 }
1671 }
1672
1673 // 2. Remove all attributes that start with "on" (like onclick, onmouseover, etc.).
1674 $elements_with_on_attrs = $xpath->query( '//*[@*[starts-with(name(), "on")]]' );
1675 if ( $elements_with_on_attrs instanceof \DOMNodeList ) {
1676 foreach ( $elements_with_on_attrs as $element ) {
1677 if ( $element instanceof \DOMElement && $element->hasAttributes() ) {
1678 foreach ( iterator_to_array( $element->attributes ) as $attr ) {
1679 if ( $attr instanceof \DOMAttr && stripos( $attr->name, 'on' ) === 0 ) {
1680 $element->removeAttribute( $attr->name );
1681 }
1682 }
1683 }
1684 }
1685 }
1686
1687 // 3. Optionally remove target and target-related rel values (noopener, noreferrer) from links.
1688 if ( $remove_link_target ) {
1689 $links = $xpath->query( '//a[@target]' );
1690 if ( $links instanceof \DOMNodeList ) {
1691 foreach ( $links as $link ) {
1692 if ( $link instanceof \DOMElement ) {
1693 $link->removeAttribute( 'target' );
1694 $rel = $link->getAttribute( 'rel' );
1695 if ( $rel ) {
1696 $cleaned_rel = trim( (string) preg_replace( '/\s+/', ' ', (string) preg_replace( '/\b(noopener|noreferrer)\b/i', '', $rel ) ) );
1697 if ( $cleaned_rel ) {
1698 $link->setAttribute( 'rel', $cleaned_rel );
1699 } else {
1700 $link->removeAttribute( 'rel' );
1701 }
1702 }
1703 }
1704 }
1705 }
1706 }
1707
1708 // Return cleaned HTML.
1709 $body = $dom->getElementsByTagName( 'body' )->item( 0 );
1710 if ( $body instanceof \DOMNode ) {
1711 $cleaned_html = $dom->saveHTML( $body );
1712 return is_string( $cleaned_html ) ? $cleaned_html : '';
1713 }
1714 return '';
1715 }
1716
1717 /**
1718 * Encodes the given string with base64.
1719 * Moved from admin class to here.
1720 *
1721 * @param string $logo contains svg's.
1722 * @return string
1723 */
1724 public static function encode_svg( $logo ) {
1725 return 'data:image/svg+xml;base64,' . base64_encode( $logo ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
1726 }
1727
1728 /**
1729 * Get plugin status
1730 *
1731 * @since 0.0.1
1732 * @since 1.7.0 moved to inc/helper.php from inc/admin-ajax.php
1733 *
1734 * @param string $plugin_init_file Plugin init file.
1735 * @return string
1736 */
1737 public static function get_plugin_status( $plugin_init_file ) {
1738
1739 $installed_plugins = get_plugins();
1740
1741 if ( ! isset( $installed_plugins[ $plugin_init_file ] ) ) {
1742 return 'Install';
1743 }
1744 if ( is_plugin_active( $plugin_init_file ) ) {
1745 return 'Activated';
1746 }
1747 return 'Installed';
1748 }
1749
1750 /**
1751 * Return the first installed plugin from a list, or a default if none exist.
1752 *
1753 * @since 2.0.0
1754 *
1755 * @param array<string> $plugins_to_check Plugin file paths to check, in priority order.
1756 * @param string $default Optional fallback plugin file path. Default empty string.
1757 *
1758 * @return string First installed plugin file path, or the default.
1759 */
1760 public static function get_plugin_if_installed( $plugins_to_check, $default = '' ) {
1761 if ( ! function_exists( 'get_plugins' ) ) {
1762 require_once ABSPATH . 'wp-admin/includes/plugin.php';
1763 }
1764
1765 $plugins = get_plugins();
1766
1767 foreach ( self::get_array_value( $plugins_to_check ) as $plugin_file ) {
1768 if ( isset( $plugins[ $plugin_file ] ) ) {
1769 return $plugin_file;
1770 }
1771 }
1772
1773 return $default;
1774 }
1775
1776 /**
1777 * Check which Starter Templates plugin is installed and return its main plugin file path.
1778 *
1779 * @since 1.7.3
1780 *
1781 * @return string The main plugin file path of the installed Starter Templates plugin.
1782 */
1783 public static function check_starter_template_plugin() {
1784 return self::get_plugin_if_installed(
1785 [ 'astra-pro-sites/astra-pro-sites.php' ],
1786 'astra-sites/astra-sites.php'
1787 );
1788 }
1789
1790 /**
1791 * Get sureforms recommended integrations.
1792 *
1793 * @since 0.0.1
1794 * @since 1.7.0 moved to inc/helper.php from inc/admin-ajax.php
1795 *
1796 * @return array<mixed>
1797 */
1798 public static function sureforms_get_integration() {
1799 $suretrigger_connected = apply_filters( 'suretriggers_is_user_connected', '' );
1800 $logo_sure_triggers = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/suretriggers.svg' );
1801 $logo_full = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/suretriggers_full.svg' );
1802 $logo_sure_mails = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/suremails.svg' );
1803 $logo_uae = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/uae.svg' );
1804 $logo_starter_templates = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/starterTemplates.svg' );
1805 $logo_sure_rank = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/surerank.svg' );
1806 $logo_sure_contact = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/surecontact.svg' );
1807
1808 $integrations = [
1809 'sure_contact' => [
1810 'title' => __( 'SureContact', 'sureforms' ),
1811 'singleLineDescription' => __( 'Turn Emails Into Revenue with a CRM Built for Your Website!', 'sureforms' ),
1812 'subtitle' => __( 'Send newsletters, run campaigns, set up automations, manage contacts, and see exactly how much revenue your emails generate, all in one place.', 'sureforms' ),
1813 'status' => self::get_plugin_status( 'surecontact/surecontact.php' ),
1814 'slug' => 'surecontact',
1815 'path' => 'surecontact/surecontact.php',
1816 'logo' => self::encode_svg( is_string( $logo_sure_contact ) ? $logo_sure_contact : '' ),
1817 ],
1818 'sure_mails' => [
1819 'title' => __( 'SureMail', 'sureforms' ),
1820 'singleLineDescription' => __( 'Boost Your Email Deliverability Instantly!', 'sureforms' ),
1821 'subtitle' => __( 'Access a powerful, easy-to-use email delivery service that ensures your emails land in inboxes, not spam folders. Automate your WordPress email workflows confidently with SureMail.', 'sureforms' ),
1822 'status' => self::get_plugin_status( 'suremails/suremails.php' ),
1823 'slug' => 'suremails',
1824 'path' => 'suremails/suremails.php',
1825 'logo' => self::encode_svg( is_string( $logo_sure_mails ) ? $logo_sure_mails : '' ),
1826 ],
1827 'sure_triggers' => [
1828 'title' => __( 'OttoKit', 'sureforms' ),
1829 'singleLineDescription' => __( 'Automate your WordPress workflows effortlessly.', 'sureforms' ),
1830 'subtitle' => __( 'Connect your WordPress plugins and favourite apps, automate tasks, and sync data effortlessly using OttoKit’s clean, visual workflow builder — no coding or complex setup required.', 'sureforms' ),
1831 'status' => self::get_plugin_status( 'suretriggers/suretriggers.php' ),
1832 'slug' => 'suretriggers',
1833 'path' => 'suretriggers/suretriggers.php',
1834 'logo' => self::encode_svg( is_string( $logo_sure_triggers ) ? $logo_sure_triggers : '' ),
1835 'logo_full' => self::encode_svg( is_string( $logo_full ) ? $logo_full : '' ),
1836 'connected' => $suretrigger_connected,
1837 'connection_url' => admin_url( 'admin.php?page=suretriggers' ),
1838 ],
1839 'starter_templates' => [
1840 'title' => __( 'Starter Templates', 'sureforms' ),
1841 'singleLineDescription' => __( 'Launch Beautiful Websites in Minutes!', 'sureforms' ),
1842 'subtitle' => __( 'Choose from professionally designed templates, import with one click, and customize effortlessly to match your brand.', 'sureforms' ),
1843 'status' => self::get_plugin_status( self::check_starter_template_plugin() ),
1844 'slug' => 'astra-sites',
1845 'path' => self::check_starter_template_plugin(),
1846 'logo' => self::encode_svg( is_string( $logo_starter_templates ) ? $logo_starter_templates : '' ),
1847 ],
1848 ];
1849
1850 $elementor_installed = self::get_plugin_if_installed( [ 'elementor/elementor.php' ] );
1851
1852 if ( $elementor_installed ) {
1853 $integrations['uae'] = [
1854 'title' => __( 'Ultimate Addons for Elementor', 'sureforms' ),
1855 'singleLineDescription' => __( 'Power Up Elementor to Build Stunning Websites Faster!', 'sureforms' ),
1856 'subtitle' => __( 'Enhance Elementor with powerful widgets and templates. Build stunning, high-performing websites faster with creative design elements and seamless customization.', 'sureforms' ),
1857 'status' => self::get_plugin_status( 'header-footer-elementor/header-footer-elementor.php' ),
1858 'slug' => 'header-footer-elementor',
1859 'path' => 'header-footer-elementor/header-footer-elementor.php',
1860 'logo' => self::encode_svg( is_string( $logo_uae ) ? $logo_uae : '' ),
1861 ];
1862 } else {
1863 $integrations['sure_rank'] = [
1864 'title' => __( 'SureRank', 'sureforms' ),
1865 'singleLineDescription' => __( 'Elevate Your SEO and Climb Search Rankings Effortlessly!', 'sureforms' ),
1866 'subtitle' => __( 'Boost your website\'s visibility with smart SEO automation. Optimize content, track keyword performance, and get actionable insights, all inside WordPress.', 'sureforms' ),
1867 'status' => self::get_plugin_status( 'surerank/surerank.php' ),
1868 'slug' => 'surerank',
1869 'path' => 'surerank/surerank.php',
1870 'logo' => self::encode_svg( is_string( $logo_sure_rank ) ? $logo_sure_rank : '' ),
1871 ];
1872 }
1873
1874 return apply_filters( 'srfm_integrated_plugins', $integrations );
1875 }
1876
1877 /**
1878 * Get the current rotating plugin for the banner.
1879 *
1880 * Plugins rotate every 2 days. Only non-activated plugins are shown.
1881 * Returns false if all plugins are activated.
1882 *
1883 * @since 2.0.0
1884 * @return array<string, mixed>|false The current plugin data or false if all plugins are activated.
1885 */
1886 public static function get_rotating_plugin_banner() {
1887 $all_plugins = self::sureforms_get_integration();
1888
1889 if ( ! is_array( $all_plugins ) ) {
1890 return false;
1891 }
1892
1893 $available_plugins = [];
1894
1895 // Only include non-activated plugins.
1896 foreach ( $all_plugins as $plugin ) {
1897 if ( ! is_array( $plugin ) ) {
1898 continue;
1899 }
1900 if ( isset( $plugin['status'] ) && is_string( $plugin['status'] ) && 'Activated' !== $plugin['status'] ) {
1901 $available_plugins[] = $plugin;
1902 }
1903 }
1904
1905 // Re-index the array to have sequential numeric keys.
1906 $available_plugins = array_values( $available_plugins );
1907 $total_plugins = count( $available_plugins );
1908
1909 // Hide section if all plugins are active.
1910 if ( 0 === $total_plugins ) {
1911 return false;
1912 }
1913
1914 // Get stored rotation data.
1915 $rotation_data = self::get_srfm_option( 'plugin_banner_rotation', [] );
1916
1917 if ( ! is_array( $rotation_data ) ) {
1918 $rotation_data = [];
1919 }
1920
1921 // Initialize rotation data if empty.
1922 if ( empty( $rotation_data ) ) {
1923 $current_time = time();
1924 self::update_srfm_option(
1925 'plugin_banner_rotation',
1926 [
1927 'last_rotation_date' => $current_time,
1928 'plugin_index' => 0,
1929 ]
1930 );
1931 return isset( $available_plugins[0] ) && is_array( $available_plugins[0] ) ? $available_plugins[0] : false;
1932 }
1933
1934 $last_rotation_date = isset( $rotation_data['last_rotation_date'] ) && is_int( $rotation_data['last_rotation_date'] ) ? $rotation_data['last_rotation_date'] : 0;
1935 $plugin_index = isset( $rotation_data['plugin_index'] ) && is_numeric( $rotation_data['plugin_index'] ) ? intval( $rotation_data['plugin_index'] ) : 0;
1936
1937 $current_time = time();
1938 $days_since_rotation = ( $current_time - $last_rotation_date ) / DAY_IN_SECONDS;
1939
1940 // Rotate every 2 days.
1941 if ( $days_since_rotation >= 2 ) {
1942 // Rotate to next plugin.
1943 ++$plugin_index;
1944 $plugin_index %= $total_plugins;
1945
1946 // Update the rotation data.
1947 self::update_srfm_option(
1948 'plugin_banner_rotation',
1949 [
1950 'last_rotation_date' => $current_time,
1951 'plugin_index' => $plugin_index,
1952 ]
1953 );
1954 }
1955
1956 // Ensure the index is within bounds.
1957 if ( $plugin_index >= $total_plugins ) {
1958 $plugin_index = 0;
1959 }
1960
1961 return isset( $available_plugins[ $plugin_index ] ) && is_array( $available_plugins[ $plugin_index ] ) ? $available_plugins[ $plugin_index ] : false;
1962 }
1963
1964 /**
1965 * Get a value from the srfm_options array.
1966 *
1967 * @param string $key The key to retrieve.
1968 * @param mixed $default The default value to return if the key does not exist.
1969 * @since 1.8.0
1970 * @return mixed
1971 */
1972 public static function get_srfm_option( $key, $default = null ) {
1973 $options = get_option( 'srfm_options', [] );
1974 if ( ! is_array( $options ) ) {
1975 $options = [];
1976 }
1977 return array_key_exists( $key, $options ) ? $options[ $key ] : $default;
1978 }
1979
1980 /**
1981 * Update a value in the srfm_options array.
1982 *
1983 * @param string $key The key to update.
1984 * @param mixed $value The value to set.
1985 * @since 1.8.0
1986 * @return void
1987 */
1988 public static function update_srfm_option( $key, $value ) {
1989 $options = get_option( 'srfm_options', [] );
1990 if ( ! is_array( $options ) ) {
1991 $options = [];
1992 }
1993 $options[ $key ] = $value;
1994 update_option( 'srfm_options', $options );
1995 }
1996
1997 /**
1998 * Get the WordPress file types.
1999 *
2000 * @since 1.7.4
2001 * @return array<string,mixed> An associative array representing the file types.
2002 */
2003 public static function get_wp_file_types() {
2004 $formats = [];
2005 $mimes = get_allowed_mime_types();
2006 $maxsize = wp_max_upload_size() / 1048576;
2007 if ( ! empty( $mimes ) ) {
2008 foreach ( $mimes as $type => $mime ) {
2009 $multiple = explode( '|', $type );
2010 foreach ( $multiple as $single ) {
2011 $formats[] = $single;
2012 }
2013 }
2014 }
2015
2016 return [
2017 'formats' => $formats,
2018 'maxsize' => $maxsize,
2019 ];
2020 }
2021
2022 /**
2023 * Determines if the SureForms Pro plugin is installed and active.
2024 *
2025 * Checks for the presence of the SRFM_PRO_VER constant.
2026 *
2027 * @since 1.8.0
2028 *
2029 * @return bool True if the Pro plugin is active; false otherwise.
2030 */
2031 public static function has_pro() {
2032 return defined( 'SRFM_PRO_VER' );
2033 }
2034
2035 /**
2036 * Verifies the request by checking the nonce and user capabilities.
2037 *
2038 * @param string $request_type The type of request, either 'rest' or 'ajax'.
2039 * @param string $nonce_action The action name for the nonce.
2040 * @param string $nonce_name The name of the nonce field.
2041 * @param string $capability The capability required to perform the action. Default is 'manage_options'.
2042 *
2043 * @since 1.10.0
2044 * @return void
2045 */
2046 public static function verify_nonce_and_capabilities( $request_type, $nonce_action, $nonce_name, $capability = 'manage_options' ) {
2047
2048 if ( ! is_string( $nonce_action ) || ! is_string( $nonce_name ) || empty( $nonce_action ) || empty( $nonce_name ) ) {
2049 wp_send_json_error(
2050 [ 'message' => __( 'Invalid nonce action or name.', 'sureforms' ) ],
2051 400
2052 );
2053 }
2054
2055 // Verify nonce for security.
2056 if ( 'rest' === $request_type ) {
2057 // For REST API requests, use the WP_REST_Request object to verify the nonce.
2058 if ( ! wp_verify_nonce( $nonce_action, $nonce_name ) ) {
2059 wp_send_json_error(
2060 [ 'message' => __( 'Invalid security token.', 'sureforms' ) ],
2061 403
2062 );
2063 }
2064 } elseif ( 'ajax' === $request_type ) {
2065 // For non-REST requests, use the standard nonce verification.
2066 if ( ! check_ajax_referer( $nonce_action, $nonce_name, false ) ) {
2067 wp_send_json_error(
2068 [ 'message' => __( 'Invalid security token.', 'sureforms' ) ],
2069 403
2070 );
2071 }
2072 } else {
2073 // If the request type is not recognized, return an error.
2074 wp_send_json_error(
2075 [ 'message' => __( 'Invalid request type.', 'sureforms' ) ],
2076 400
2077 );
2078 }
2079
2080 // Check user capabilities.
2081 if ( ! current_user_can( $capability ) ) {
2082 wp_send_json_error(
2083 [ 'message' => esc_html__( 'You do not have permission to perform this action.', 'sureforms' ) ],
2084 403
2085 );
2086 }
2087 }
2088
2089 /**
2090 * Get the block name from a field name by extracting the first two parts.
2091 *
2092 * @param string $field_name The full field name (e.g., 'srfm-text-lbl-123').
2093 *
2094 * @since 1.11.0
2095 * @return string The block name (e.g., 'srfm-text').
2096 */
2097 public static function get_block_name_from_field( $field_name ) {
2098 return implode( '-', array_slice( explode( '-', explode( '-lbl-', $field_name )[0] ), 0, 2 ) );
2099 }
2100
2101 /**
2102 * Check if any of the top 10 popular WordPress SMTP plugins is active using array_intersect.
2103 *
2104 * @since 1.9.1
2105 * @return bool True if any SMTP plugin is active, false otherwise.
2106 */
2107 public static function is_any_smtp_plugin_active() {
2108 $smtp_plugins = [
2109 'wp-mail-smtp/wp_mail_smtp.php',
2110 'post-smtp/postman-smtp.php',
2111 'easy-wp-smtp/easy-wp-smtp.php',
2112 'wp-smtp/wp-smtp.php',
2113 'newsletter/plugin.php',
2114 'fluent-smtp/fluent-smtp.php',
2115 'pepipost-smtp/pepipost-smtp.php',
2116 'mail-bank/wp-mail-bank.php',
2117 'smtp-mailer/smtp-mailer.php',
2118 'suremails/suremails.php',
2119 'site-mailer/site-mailer.php',
2120 ];
2121
2122 $active_plugins = (array) get_option( 'active_plugins', [] );
2123 // For multisite, merge sitewide active plugins.
2124 if ( is_multisite() ) {
2125 $network_plugins = (array) get_site_option( 'active_sitewide_plugins', [] );
2126 $active_plugins = array_merge( $active_plugins, array_keys( $network_plugins ) );
2127 }
2128
2129 return (bool) array_intersect( $smtp_plugins, $active_plugins );
2130 }
2131
2132 /**
2133 * Apply a filter and return the filtered value only if it's a non-empty array.
2134 * Otherwise, return the default array.
2135 *
2136 * @param string $filter_name The name of the filter to apply.
2137 * @param mixed $default The default array to return if the filtered result is invalid.
2138 * @param mixed ...$args Additional arguments to pass to the filter.
2139 *
2140 * @return array The filtered array if valid, otherwise the default.
2141 */
2142 public static function apply_filters_as_array( $filter_name, $default, ...$args ) {
2143 // Ensure $default is an array.
2144 if ( ! is_array( $default ) ) {
2145 $default = [];
2146 }
2147
2148 // Validate the filter name.
2149 if ( ! is_string( $filter_name ) || empty( $filter_name ) ) {
2150 return $default;
2151 }
2152
2153 // Apply the filter with additional arguments.
2154 $filtered = apply_filters( $filter_name, $default, ...$args );
2155
2156 // Return filtered result if it's a non-empty array.
2157 return is_array( $filtered ) && ! empty( $filtered ) ? $filtered : $default;
2158 }
2159
2160 /**
2161 * Get forms with entry counts for a specific time period.
2162 *
2163 * @param int $timestamp The timestamp to get entries after.
2164 * @param int $limit Maximum number of forms to return (0 for all).
2165 * @param bool $sort Whether to sort by entry count descending.
2166 * @return array Array of form data with entry counts.
2167 * @since 1.9.1
2168 */
2169 public static function get_forms_with_entry_counts( $timestamp, $limit = 0, $sort = true ) {
2170 // Get all published forms with post objects for bulk title access.
2171 $args = [
2172 'post_type' => SRFM_FORMS_POST_TYPE,
2173 'posts_per_page' => -1,
2174 'post_status' => 'publish',
2175 'orderby' => 'ID',
2176 'order' => 'DESC',
2177 'no_found_rows' => true,
2178 'update_post_term_cache' => false,
2179 'update_post_meta_cache' => false,
2180 ];
2181
2182 $query = new \WP_Query( $args );
2183
2184 if ( ! $query->have_posts() ) {
2185 return [];
2186 }
2187
2188 $all_forms = [];
2189
2190 // Process posts directly from the query results without touching global $post.
2191 foreach ( $query->posts as $form ) {
2192 // Ensure we have a valid post object.
2193 if ( ! $form instanceof \WP_Post ) {
2194 continue;
2195 }
2196
2197 $form_id = (int) $form->ID;
2198 if ( $form_id <= 0 ) {
2199 continue;
2200 }
2201
2202 // Get entries count after the timestamp for this specific form.
2203 $entry_count = Entries::get_entries_count_after( $timestamp, $form_id );
2204
2205 // Get form title directly from post object, use "Blank Form" if empty.
2206 $form_title = $form->post_title;
2207 if ( empty( trim( self::get_string_value( $form_title ) ) ) ) {
2208 $form_title = __( 'Blank Form', 'sureforms' );
2209 }
2210
2211 $all_forms[] = [
2212 'form_id' => $form_id,
2213 'title' => $form_title,
2214 'count' => $entry_count,
2215 ];
2216 }
2217
2218 // Sort by count descending, then by form_id descending for consistency.
2219 if ( $sort ) {
2220 usort(
2221 $all_forms,
2222 static function( $a, $b ) {
2223 if ( $a['count'] === $b['count'] ) {
2224 return $b['form_id'] - $a['form_id'];
2225 }
2226 return $b['count'] - $a['count'];
2227 }
2228 );
2229 }
2230
2231 // Return limited results if specified.
2232 if ( $limit > 0 ) {
2233 return array_slice( $all_forms, 0, $limit );
2234 }
2235
2236 return $all_forms;
2237 }
2238
2239 /**
2240 * Check if the given form ID is valid SureForms form ID.
2241 * A valid form ID is a numeric value that corresponds to an existing SureForms form in the database.
2242 *
2243 * @since 1.9.1
2244 *
2245 * @param int|string|mixed $form_id The form ID to validate.
2246 * @return bool True if the form ID is valid, false otherwise.
2247 */
2248 public static function is_valid_form( $form_id ) {
2249
2250 // Check for a valid form ID.
2251 if ( empty( $form_id ) || ! is_numeric( $form_id ) ) {
2252 return false;
2253 }
2254
2255 // Check if the form ID exists in the database.
2256 $form = get_post( self::get_integer_value( $form_id ) );
2257
2258 // If the form does not exist or is not of the correct post type, return false.
2259 if ( ! $form || ! is_a( $form, 'WP_Post' ) || SRFM_FORMS_POST_TYPE !== $form->post_type ) {
2260 return false;
2261 }
2262
2263 return true;
2264 }
2265
2266 /**
2267 * Get the timestamp from a string.
2268 *
2269 * This function uses WordPress's configured timezone (from Settings → General → Timezone)
2270 * to ensure consistent behavior regardless of the server's timezone settings.
2271 *
2272 * @param string $date The date in YYYY-MM-DD format (e.g., '2026-01-10').
2273 * @param string $hours The hours in 12-hour format (e.g., '12', '01'-'12').
2274 * @param string $minutes The minutes (e.g., '00', '00'-'59').
2275 * @param string $meridiem The meridiem (e.g., 'AM' or 'PM').
2276 *
2277 * @since 1.10.1
2278 * @return int|false The timestamp if successful, false otherwise.
2279 */
2280 public static function get_timestamp_from_string( $date, $hours = '12', $minutes = '00', $meridiem = 'AM' ) {
2281
2282 if ( empty( $date ) || ! is_string( $date ) ) {
2283 return false; // Invalid input.
2284 }
2285
2286 // Ensure the date is in a valid format of YYYY-MM-DD.
2287 if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) {
2288 return false; // Invalid date format.
2289 }
2290
2291 $time_string = $date . ' ' . $hours . ':' . $minutes . ' ' . $meridiem;
2292
2293 // Convert to timestamp using WordPress timezone.
2294 // This ensures the date/time is interpreted in the site's configured timezone,
2295 // not the server's timezone or PHP's default timezone.
2296 try {
2297 $datetime = date_create( $time_string, wp_timezone() );
2298
2299 if ( false === $datetime ) {
2300 return false;
2301 }
2302
2303 $timestamp = $datetime->getTimestamp();
2304
2305 if ( is_int( $timestamp ) && $timestamp > 0 ) {
2306 return $timestamp;
2307 }
2308 } catch ( \Exception $e ) {
2309 // If timezone conversion fails, return false.
2310 return false;
2311 }
2312
2313 // If conversion fails, return false.
2314 return false;
2315 }
2316
2317 /**
2318 * Generate a unique ID for the saved form.
2319 * Also ensures that the generated ID does not already exist in the database table.
2320 *
2321 * @param class-string $class The class name where the get method is defined to check for existing IDs.
2322 * @param int<1, max> $length The length of the random bytes to generate. Default is 8.
2323 * @return string
2324 * @since 2.2.0
2325 */
2326 public static function generate_unique_id( $class, $length = 8 ) {
2327 // Ensure length is at least 1.
2328 $length = max( 1, $length );
2329
2330 do {
2331 $id = bin2hex( random_bytes( $length ) );
2332 } while ( is_callable( [ $class, 'get' ] ) && call_user_func( [ $class, 'get' ], $id ) );
2333 return $id;
2334 }
2335
2336 /**
2337 * Log error messages to the error log.
2338 *
2339 * This function checks if error_log function exists, validates the message,
2340 * and logs it with the print_r second argument set to true.
2341 *
2342 * Logging is disabled by default. To enable logging, add this to wp-config.php:
2343 * define( 'SRFM_LOG', true );
2344 *
2345 * @param mixed $message The error message to log. Can be string or any type.
2346 * @param string $prefix Optional prefix to add before the message. Default: 'Log :'.
2347 *
2348 * @since 2.0.0
2349 * @return void
2350 */
2351 public static function srfm_log( $message, $prefix = 'Log :' ) {
2352 // Check if logging is enabled via SRFM_LOG constant.
2353 if ( ! defined( 'SRFM_LOG' ) ) {
2354 return;
2355 }
2356 unset( $message, $prefix );
2357 }
2358
2359 /**
2360 * Encodes data to base64 after JSON encoding with validation.
2361 *
2362 * This function checks if the data is non-empty and valid for JSON encoding.
2363 * If data is not valid, returns an empty string.
2364 * Otherwise, it attempts to JSON encode and then base64 encode the result.
2365 *
2366 * @param mixed $data The data to JSON encode and then base64 encode.
2367 * @return string The base64-encoded JSON string, or empty string on failure.
2368 */
2369 public static function srfm_base64_json_encode( $data ) {
2370 if ( empty( $data ) || ! is_array( $data ) ) {
2371 return '';
2372 }
2373
2374 $json = wp_json_encode( $data );
2375 if ( false === $json || '' === $json ) {
2376 return '';
2377 }
2378
2379 // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode
2380 return base64_encode( $json );
2381 }
2382
2383 /**
2384 * Get the visitor's IP address.
2385 *
2386 * Centralised IP detection that checks common proxy headers before
2387 * falling back to REMOTE_ADDR. Handles comma-separated IPs that
2388 * load-balancers / CDNs may append (takes the first, i.e. client IP).
2389 *
2390 * NOTE: Existing callers (Smart_Tags::get_the_user_ip, Front_End::get_user_ip,
2391 * inline reads in Form_Submit) can be migrated to this method in the future
2392 * to avoid duplicating the same header-chain logic.
2393 *
2394 * @since 2.8.0
2395 * @return string Validated IP address, or empty string if unavailable.
2396 */
2397 public static function get_visitor_ip() {
2398 $headers = [
2399 'HTTP_CLIENT_IP',
2400 'HTTP_X_FORWARDED_FOR',
2401 'HTTP_X_REAL_IP',
2402 'HTTP_X_FORWARDED',
2403 'HTTP_FORWARDED_FOR',
2404 'HTTP_FORWARDED',
2405 'REMOTE_ADDR',
2406 ];
2407
2408 foreach ( $headers as $header ) {
2409 if ( empty( $_SERVER[ $header ] ) ) {
2410 continue;
2411 }
2412
2413 // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Validated by FILTER_VALIDATE_IP below.
2414 $raw = wp_unslash( $_SERVER[ $header ] );
2415
2416 // Proxies may send comma-separated IPs; the first is the original client.
2417 if ( false !== strpos( $raw, ',' ) ) {
2418 $raw = trim( explode( ',', $raw )[0] );
2419 }
2420
2421 $ip = filter_var( $raw, FILTER_VALIDATE_IP );
2422 if ( false !== $ip ) {
2423 /**
2424 * Filters the detected visitor IP address.
2425 *
2426 * @since 2.8.0
2427 *
2428 * @param string $ip Validated IP address.
2429 */
2430 return apply_filters( 'srfm_visitor_ip', $ip );
2431 }
2432 }
2433
2434 return '';
2435 }
2436
2437 }
2438