PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 2.3.0
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v2.3.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 in SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz 2.3.0, at inc/helper.php

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