PluginProbe
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz / 1.12.0
SureForms – Contact Form Builder, AI Forms, Payment Form, Survey & Quiz v1.12.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 1.12.0, at inc/helper.php

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