FileMaster
Search
Toggle Dark Mode
Home
/
.
/
wp-content
/
plugins
/
sureforms
/
inc
Edit File: helper.php
<?php /** * Sureforms Submit Class file. * * @package sureforms. * @since 0.0.1 */ namespace SRFM\Inc; use SRFM\Inc\Database\Tables\Entries; use SRFM\Inc\Traits\Get_Instance; use WP_Error; use WP_Post; if ( ! defined( 'ABSPATH' ) ) { exit; // Exit if accessed directly. } /** * Sureforms Helper Class. * * @since 0.0.1 */ class Helper { use Get_Instance; /** * Allowed HTML tags for SVG. * * @var array<string, array<string, bool>> */ public static $allowed_tags_svg = [ 'span' => [ 'class' => true, 'aria-hidden' => true, ], 'svg' => [ 'xmlns' => true, 'width' => true, 'height' => true, 'viewBox' => true, 'fill' => true, ], 'path' => [ 'd' => true, 'stroke' => true, 'stroke-opacity' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, ], ]; /** * Sureforms SVGs. * * @var mixed srfm_svgs */ private static $srfm_svgs = null; /** * Get common error message. * * @since 0.0.2 * @return array<string> */ public static function get_common_err_msg() { return [ 'required' => __( 'This field is required.', 'sureforms' ), 'unique' => __( 'Value needs to be unique.', 'sureforms' ), ]; } /** * Convert a file URL to a file path. * * @param string $file_url The URL of the file. * * @since 1.3.0 * @return string The file path. */ public static function convert_fileurl_to_filepath( $file_url ) { static $upload_dir = null; if ( ! $upload_dir ) { // Internally cache the upload directory. $upload_dir = wp_get_upload_dir(); } return wp_normalize_path( str_replace( $upload_dir['baseurl'], $upload_dir['basedir'], $file_url ) ); } /** * Checks if current value is string or else returns default value * * @param mixed $data data which need to be checked if is string. * * @since 0.0.1 * @return string */ public static function get_string_value( $data ) { if ( is_scalar( $data ) ) { return (string) $data; } if ( is_object( $data ) && method_exists( $data, '__toString' ) ) { return $data->__toString(); } if ( is_null( $data ) ) { return ''; } return ''; } /** * Checks if current value is number or else returns default value * * @param mixed $value data which need to be checked if is string. * @param int $base value can be set is $data is not a string, defaults to empty string. * * @since 0.0.1 * @return int */ public static function get_integer_value( $value, $base = 10 ) { if ( is_numeric( $value ) ) { return (int) $value; } if ( is_string( $value ) ) { $trimmed_value = trim( $value ); return intval( $trimmed_value, $base ); } return 0; } /** * Validate a date string in Y-m-d format. * * @param string $date The date string to validate. * @since 2.6.0 * @return bool */ public static function validate_date( string $date ): bool { $d = \DateTime::createFromFormat( 'Y-m-d', $date ); return $d && $d->format( 'Y-m-d' ) === $date; } /** * Returns a boolean representation of the given value. * * @param mixed $data Data which needs to be converted to boolean. * * @since 2.5.2 * @return bool */ public static function get_boolean_value( $data ) { return (bool) $data; } /** * Checks if current value is an array or else returns default value * * @param mixed $data Data which needs to be checked if it is an array. * * @since 0.0.3 * @return array */ public static function get_array_value( $data ) { if ( is_array( $data ) ) { return $data; } if ( is_null( $data ) ) { return []; } return (array) $data; } /** * Extracts the field type from the dynamic field key ( or field slug ). * * @param string $field_key Dynamic field key. * @since 0.0.6 * @return string Extracted field type. */ public static function get_field_type_from_key( $field_key ) { if ( false === strpos( $field_key, '-lbl-' ) ) { return ''; } return trim( explode( '-', $field_key )[1] ); } /** * Extracts the field label from the dynamic field key ( or field slug ). * * @param string $field_key Dynamic field key. * @since 1.1.1 * @return string Extracted field label. */ public static function get_field_label_from_key( $field_key ) { if ( false === strpos( $field_key, '-lbl-' ) ) { return ''; } $label = explode( '-lbl-', $field_key )[1]; // Getting the encrypted label. we are removing the block slug here. $label = explode( '-', $label )[0]; return $label ? html_entity_decode( self::decrypt( $label ) ) : ''; } /** * Extracts the block ID from the dynamic field key ( or field slug ). * * @param string $field_key Dynamic field key. * @since 1.6.1 * @return string Extracted block ID. */ public static function get_block_id_from_key( $field_key ) { // Check if the key contains the block ID identifier. if ( strpos( $field_key, 'srfm-' ) === 0 && strpos( $field_key, '-lbl-' ) === false ) { return ''; // Return empty if the key format is invalid. } $parts = explode( '-lbl-', $field_key ); if ( isset( $parts[0] ) ) { $block_id = explode( '-', $parts[0] ); if ( is_array( $block_id ) && ! empty( $block_id ) ) { return end( $block_id ); } } return ''; } /** * Returns the proper sanitize callback functions according to the field type. * * @param string $field_type HTML field type. * @since 0.0.6 * @return callable Returns sanitize callbacks according to the provided field type. */ public static function get_field_type_sanitize_function( $field_type ) { $callbacks = apply_filters( 'srfm_field_type_sanitize_functions', [ 'url' => 'esc_url_raw', 'input' => 'sanitize_text_field', 'number' => [ self::class, 'sanitize_number' ], 'email' => 'sanitize_email', 'textarea' => [ self::class, 'sanitize_textarea' ], ] ); return $callbacks[ $field_type ] ?? 'sanitize_text_field'; } /** * Sanitizes a numeric value. * * This function checks if the input value is numeric. If it is numeric, it sanitizes * the value to ensure it's a float or integer, allowing for fractions and thousand separators. * If the value is not numeric, it sanitizes it as a text field. * * @param mixed $value The value to be sanitized. * @since 0.0.6 * @return int|float|string The sanitized value. */ public static function sanitize_number( $value ) { if ( ! is_numeric( $value ) ) { // phpcs:ignore /** @phpstan-ignore-next-line */ return sanitize_text_field( $value ); // If it is not numeric, then let user get some sanitized data to view. } // phpcs:ignore /** @phpstan-ignore-next-line */ return sanitize_text_field( filter_var( $value, FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION | FILTER_FLAG_ALLOW_THOUSAND ) ); } /** * Sanitize a CSS value to prevent injection. * * Strips characters that can break out of a CSS property value context * and removes dangerous CSS functions while preserving safe ones * (rgb, hsl, linear-gradient, etc.). * * @param mixed $value Raw CSS value. * @return string Sanitized CSS value. * @since 2.7.0 */ public static function sanitize_css_value( $value ) { $value = self::get_string_value( $value ); // Strip characters that can break out of a CSS property value context. $value = preg_replace( '/[{}<>;\\\\"\'`]/', '', $value ) ?? ''; // Remove dangerous CSS functions (url, expression, import, etc.) while preserving safe ones (rgb, hsl, linear-gradient, etc.). return preg_replace( '/\b(url|expression|import|javascript)\s*\(/i', '(', $value ) ?? ''; } /** * This function sanitizes the submitted form data according to the field type. * * @param array<mixed> $form_data $form_data User submitted form data. * @since 0.0.6 * @return array<mixed> $result Sanitized form data. */ public static function sanitize_by_field_type( $form_data ) { $result = []; if ( empty( $form_data ) || ! is_array( $form_data ) ) { return $result; } foreach ( $form_data as $field_key => &$value ) { $field_type = self::get_field_type_from_key( $field_key ); $sanitize_function = self::get_field_type_sanitize_function( $field_type ); $sanitized_data = is_array( $value ) ? self::sanitize_by_field_type( $value ) : call_user_func( $sanitize_function, $value ); $result[ $field_key ] = $sanitized_data; } return $result; } /** * This function performs array_map for multi dimensional array * * @param string $function function name to be applied on each element on array. * @param array<mixed> $data_array array on which function needs to be performed. * @return array<mixed> * @since 0.0.1 */ public static function sanitize_recursively( $function, $data_array ) { $response = []; if ( is_array( $data_array ) ) { if ( ! is_callable( $function ) ) { return $data_array; } foreach ( $data_array as $key => $data ) { $val = is_array( $data ) ? self::sanitize_recursively( $function, $data ) : $function( $data ); $response[ $key ] = $val; } } return $response; } /** * Generates common markup liked label, etc * * @param int|string $form_id form id. * @param string $type Type of form markup. * @param string $label Label for the form markup. * @param string $slug Slug for the form markup. * @param string $block_id Block id for the form markup. * @param bool $required If field is required or not. * @param string $help Help for the form markup. * @param string $error_msg Error message for the form markup. * @param bool $is_unique Check if the field is unique. * @param string $duplicate_msg Duplicate message for field. * @param bool $override Override for error markup. * @return string * @since 0.0.1 */ 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 ) { $duplicate_msg = $duplicate_msg ? ' data-unique-msg="' . esc_attr( $duplicate_msg ) . '"' : ''; $markup = ''; $show_labels_as_placeholder = get_post_meta( self::get_integer_value( $form_id ), '_srfm_use_label_as_placeholder', true ); $show_labels_as_placeholder = $show_labels_as_placeholder ? self::get_string_value( $show_labels_as_placeholder ) : false; $required_sign = apply_filters( 'srfm_value_after_label_placeholder', ' *' ); if ( ! is_string( $required_sign ) ) { $required_sign = ' *'; } switch ( $type ) { case 'label': if ( $label ) { ob_start(); ?> <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"> <?php echo wp_kses_post( $label ); ?> <?php if ( $required ) { ?> <span class="srfm-required" aria-hidden="true"> *</span> <?php } ?> </label> <?php $markup = ob_get_clean(); } break; case 'help': if ( $help ) { ob_start(); ?> <div class="srfm-description" id="srfm-description-<?php echo esc_attr( $block_id ); ?>"> <?php echo wp_kses_post( $help ); ?> </div> <?php $markup = ob_get_clean(); } break; case 'error': if ( $required || $override ) { ob_start(); ?> <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 ?>> <?php echo esc_html( $error_msg ); ?> </div> <?php $markup = ob_get_clean(); } break; case 'is_unique': if ( $is_unique ) { ob_start(); ?> <div class="srfm-error"> <?php echo esc_html( $duplicate_msg ); ?> </div> <?php $markup = ob_get_clean(); } break; case 'placeholder': $markup = $label && '1' === $show_labels_as_placeholder ? wp_kses_post( $label ) . ( $required ? esc_attr( $required_sign ) : '' ) : ''; break; case 'label_text': // This has been added for generating label text for the form markup instead of adding it in the label tag. if ( $label ) { ob_start(); ?> <?php echo wp_kses_post( $label ); ?> <?php if ( $required ) { ?> <span class="srfm-required" aria-hidden="true"> *</span> <?php } ?> <?php $markup = ob_get_clean(); } break; default: $markup = ''; } return is_string( $markup ) ? $markup : ''; } /** * Get an SVG Icon * * @since 0.0.1 * @param string $icon the icon name. * @param string $class if the baseline class should be added. * @param string $html Custom attributes inside svg wrapper. * @return string */ public static function fetch_svg( $icon = '', $class = '', $html = '' ) { $class = $class ? ' ' . $class : ''; if ( ! self::$srfm_svgs ) { ob_start(); include_once SRFM_DIR . 'assets/svg/svgs.json'; self::$srfm_svgs = json_decode( self::get_string_value( ob_get_clean() ), true ); self::$srfm_svgs = apply_filters( 'srfm_svg_icons', self::$srfm_svgs ); } ob_start(); ?> <span class="srfm-icon<?php echo esc_attr( $class ); ?>" <?php echo $html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>> <?php echo self::$srfm_svgs[ $icon ] ?? ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?> </span> <?php $output = ob_get_clean(); return is_string( $output ) ? $output : ''; } /** * Encrypt data using base64. * * @param string $input The input string which needs to be encrypted. * @since 0.0.1 * @return string The encrypted string. */ public static function encrypt( $input ) { // If the input is empty or not a string, then abandon ship. if ( empty( $input ) || ! is_string( $input ) ) { return ''; } // Strip HTML tags to prevent them from being included in IDs and field names. $input = wp_strip_all_tags( $input ); // Encrypt the input and return it. $base_64 = base64_encode( $input ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode return rtrim( $base_64, '=' ); } /** * Decrypt data using base64. * * @param string $input The input string which needs to be decrypted. * @since 0.0.1 * @return string The decrypted string. */ public static function decrypt( $input ) { // If the input is empty or not a string, then abandon ship. if ( empty( $input ) || ! is_string( $input ) ) { return ''; } // Decrypt the input and return it. $base_64 = $input . str_repeat( '=', strlen( $input ) % 4 ); return base64_decode( $base_64 ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_decode } /** * Update an option from the database. * * @param string $key The option key. * @param mixed $value The value to update. * @param bool $network_override Whether to allow the network_override admin setting to be overridden on subsites. * @since 0.0.1 * @return bool True if the option was updated, false otherwise. */ public static function update_admin_settings_option( $key, $value, $network_override = false ) { // Update the site-wide option if we're in the network admin, and return the updated status. return $network_override && is_multisite() ? update_site_option( $key, $value ) : update_option( $key, $value ); } /** * Update an option from the database. * * @param int|string $post_id post id / form id. * @param string $key meta key name. * @param bool $single single or multiple. * @param mixed $default default value. * * @since 0.0.1 * @return string Meta value. */ public static function get_meta_value( $post_id, $key, $single = true, $default = '' ) { $srfm_live_mode_data = self::get_instant_form_live_data(); if ( isset( $srfm_live_mode_data[ $key ] ) ) { // Give priority to live mode data if we have one set from the Instant Form. return self::get_string_value( $srfm_live_mode_data[ $key ] ); } 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 ); } /** * Wrapper for the WordPress's get_post_meta function with the support for default values. * * @param int|string $post_id Post ID. * @param string $key The meta key to retrieve. * @param mixed $default Default value. * @param bool $single Optional. Whether to return a single value. * @since 0.0.8 * @return mixed Meta value. */ public static function get_post_meta( $post_id, $key, $default = null, $single = true ) { $meta_value = get_post_meta( self::get_integer_value( $post_id ), $key, $single ); return $meta_value ? $meta_value : $default; } /** * Returns query params data for instant form live preview. * * @since 0.0.8 * @return array<mixed> Live preview data. */ public static function get_instant_form_live_data() { $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. return $srfm_live_mode_data ? array_map( // Normalize falsy values. static function( $live_data ) { return 'false' === $live_data ? false : $live_data; }, $srfm_live_mode_data ) : []; } /** * Default dynamic block value. * * @since 0.0.1 * @return array<string> Meta value. */ public static function default_dynamic_block_option() { $common_err_msg = self::get_common_err_msg(); $default_values = [ 'srfm_url_block_required_text' => $common_err_msg['required'], 'srfm_input_block_required_text' => $common_err_msg['required'], 'srfm_input_block_unique_text' => $common_err_msg['unique'], 'srfm_address_block_required_text' => $common_err_msg['required'], 'srfm_phone_block_required_text' => $common_err_msg['required'], 'srfm_phone_block_unique_text' => $common_err_msg['unique'], 'srfm_number_block_required_text' => $common_err_msg['required'], 'srfm_textarea_block_required_text' => $common_err_msg['required'], 'srfm_multi_choice_block_required_text' => $common_err_msg['required'], 'srfm_checkbox_block_required_text' => $common_err_msg['required'], 'srfm_gdpr_block_required_text' => $common_err_msg['required'], 'srfm_email_block_required_text' => $common_err_msg['required'], 'srfm_email_block_unique_text' => $common_err_msg['unique'], 'srfm_dropdown_block_required_text' => $common_err_msg['required'], 'srfm_rating_block_required_text' => $common_err_msg['required'], ]; $default_values = array_merge( $default_values, Translatable::dynamic_validation_messages() ); return apply_filters( 'srfm_default_dynamic_block_option', $default_values, $common_err_msg ); } /** * Get default dynamic block value. * * @param string $key meta key name. * @since 0.0.1 * @return string Meta value. */ public static function get_default_dynamic_block_option( $key ) { $default_dynamic_values = self::default_dynamic_block_option(); $option = get_option( 'srfm_default_dynamic_block_option', $default_dynamic_values ); if ( is_array( $option ) && array_key_exists( $key, $option ) ) { return $option[ $key ]; } return ''; } /** * Checks whether a given request has appropriate permissions. * * @return true|WP_Error True if the request has read access, WP_Error object otherwise. * @since 0.0.1 */ public static function get_items_permissions_check() { if ( self::current_user_can() ) { return true; } return new WP_Error( 'rest_cannot_view', __( 'Sorry, you are not allowed to perform this action.', 'sureforms' ), [ 'status' => \rest_authorization_required_code() ] ); } /** * Check if the current user has a given capability. * * @param string $capability The capability to check. * @param array<mixed> $args Optional. Additional arguments to pass to the capability check. * * @since 0.0.3 * @return bool Whether the current user has the given capability or role. */ public static function current_user_can( $capability = '', $args = [] ) { if ( ! function_exists( 'current_user_can' ) ) { return false; } if ( ! is_string( $capability ) || empty( $capability ) ) { $capability = 'manage_options'; } return ! empty( $args ) && is_array( $args ) && count( $args ) > 0 ? current_user_can( $capability, ...$args ) : current_user_can( $capability ); } /** * Get all the entries for the given form ids. The entries are older than the given days_old. * * @param int $days_old The number of days old the entries should be. * @param array<int> $sf_form_ids The form ids for which the entries need to be fetched. * @since 0.0.2 * @return array<mixed> the entries matching the criteria. */ public static function get_entries_from_form_ids( $days_old = 0, $sf_form_ids = [] ) { $entries = []; $days_old_date = ( new \DateTime() )->modify( "-{$days_old} days" )->format( 'Y-m-d H:i:s' ); foreach ( $sf_form_ids as $form_id ) { // args according to the get_all() function in the Entries class. $args = [ 'where' => [ [ [ 'key' => 'form_id', 'value' => $form_id, 'compare' => '=', ], [ 'key' => 'created_at', 'value' => $days_old_date, 'compare' => '<=', ], ], ], ]; // store all the entries in a single array. $entries = array_merge( $entries, Entries::get_all( $args, false ) ); } return $entries; } /** * Decode block attributes. * The function reverses the effect of serialize_block_attributes() * * @link https://developer.wordpress.org/reference/functions/serialize_block_attributes/ * @param string $encoded_data the encoded block attribute. * @since 0.0.2 * @return string decoded block attribute */ public static function decode_block_attribute( $encoded_data = '' ) { $decoded_data = preg_replace( '/\\\\u002d\\\\u002d/', '--', self::get_string_value( $encoded_data ) ); $decoded_data = preg_replace( '/\\\\u003c/', '<', self::get_string_value( $decoded_data ) ); $decoded_data = preg_replace( '/\\\\u003e/', '>', self::get_string_value( $decoded_data ) ); $decoded_data = preg_replace( '/\\\\u0026/', '&', self::get_string_value( $decoded_data ) ); $decoded_data = preg_replace( '/\\\\\\\\"/', '"', self::get_string_value( $decoded_data ) ); return self::get_string_value( $decoded_data ); } /** * Map slugs to submission data. * * @param array<mixed> $submission_data submission_data. * @since 0.0.3 * @return array<mixed> */ public static function map_slug_to_submission_data( $submission_data = [] ) { $mapped_data = []; foreach ( $submission_data as $key => $value ) { if ( false === strpos( $key, '-lbl-' ) ) { continue; } $label = explode( '-lbl-', $key )[1]; $slug = implode( '-', array_slice( explode( '-', $label ), 1 ) ); /** * Filters whether a field should be skipped when mapping slugs to submission data. * * This filter allows plugins or custom code to determine if a field should be excluded * from the mapped submission data array (such as for internal fields or extraneous meta). * * @since 2.0.0 * * @param bool $skip_this_field Whether to skip this field from processing. Default false. * @param array $args { * Arguments used for this field. * * @type string $key The original key of the field in the submission data array. * @type string $slug The mapped slug parsed from the field key. * @type mixed $value The value assigned to this field. * } */ $skip_this_field = apply_filters( 'srfm_map_slug_to_submission_data_should_skip', false, [ 'key' => $key, 'slug' => $slug, 'value' => $value, ] ); if ( $skip_this_field ) { continue; } // Check if value is array to handle external package field functionality. // like repeater fields that need special processing. if ( is_array( $value ) && ! empty( $value ) ) { // Apply filter to allow external packages to process array values. // Returns processed data with 'is_processed' flag if successfully handled. $filtered_submission_data = apply_filters( 'srfm_map_slug_to_submission_data_array', [ 'value' => $value, 'key' => $key, 'slug' => $slug, ] ); if ( isset( $filtered_submission_data['is_processed'] ) && true === $filtered_submission_data['is_processed'] ) { $mapped_data[ $slug ] = $filtered_submission_data['value']; continue; } } // If the value is an array (e.g. multi-upload field), decode each URL value. if ( is_array( $value ) ) { $mapped_data[ $slug ] = array_map( static function ( $val ) { return is_string( $val ) ? rawurldecode( $val ) : $val; }, $value ); continue; } $mapped_data[ $slug ] = is_string( $value ) ? html_entity_decode( esc_attr( $value ) ) : $value; } return $mapped_data; } /** * Get forms options. Shows all the available forms in the dropdown. * * @since 0.0.5 * @param string $key Determines the type of data to return. * @return array<mixed> */ public static function get_sureforms( $key = '' ) { $forms = get_posts( apply_filters( 'srfm_get_sureforms_query_args', [ 'post_type' => SRFM_FORMS_POST_TYPE, 'posts_per_page' => -1, 'post_status' => 'publish', ] ) ); $options = []; foreach ( $forms as $form ) { if ( $form instanceof WP_Post ) { if ( 'all' === $key ) { $options[ $form->ID ] = $form; } elseif ( ! empty( $key ) && is_string( $key ) && isset( $form->$key ) ) { $options[ $form->ID ] = $form->$key; } else { $options[ $form->ID ] = $form->post_title; } } } return $options; } /** * Get all the forms. * * @since 0.0.5 * @return array<mixed> */ public static function get_sureforms_title_with_ids() { $form_options = self::get_sureforms(); foreach ( $form_options as $key => $value ) { $form_options[ $key ] = $value . ' #' . $key; } return $form_options; } /** * Get the CSS variables based on different field spacing sizes. * * @param string|null $field_spacing The field spacing size or boolean false to return complete sizes array. * * @since 0.0.7 * @return array<string|mixed> */ public static function get_css_vars( $field_spacing = null ) { /** * $sizes - Field Spacing Sizes Variables. * The array contains the CSS variables for different field spacing sizes. * Each key corresponds to the field spacing size, and the value is an array of CSS variables. * * For future variables depending on the field spacing size, add the variable to the array respectively. */ $sizes = apply_filters( 'srfm_css_vars_sizes', [ 'small' => [ '--srfm-row-gap-between-blocks' => '16px', // Address block gap and spacing variables. '--srfm-address-label-font-size' => '14px', '--srfm-address-label-line-height' => '20px', '--srfm-address-description-font-size' => '12px', '--srfm-address-description-line-height' => '16px', '--srfm-col-gap-between-fields' => '12px', '--srfm-row-gap-between-fields' => '12px', '--srfm-gap-below-address-label' => '12px', // Dropdown Variables. '--srfm-dropdown-font-size' => '14px', '--srfm-dropdown-gap-between-input-menu' => '4px', '--srfm-dropdown-badge-padding' => '2px 6px', '--srfm-dropdown-multiselect-font-size' => '12px', '--srfm-dropdown-multiselect-line-height' => '16px', '--srfm-dropdown-padding-right' => '12px', // initial padding and from 20px - 12px for dropdown arrow width and 8px for gap before dropdown arrow. '--srfm-dropdown-padding-right-icon' => 'calc( var( --srfm-dropdown-padding-right ) + 20px )', '--srfm-dropdown-multiselect-padding' => '8px var( --srfm-dropdown-padding-right-icon ) 8px 8px', // Input Field Variables. '--srfm-input-height' => '40px', '--srfm-input-field-padding' => '10px 12px', '--srfm-input-field-font-size' => '14px', '--srfm-input-field-line-height' => '20px', '--srfm-input-field-margin-top' => '4px', '--srfm-input-field-margin-bottom' => '4px', // Checkbox and GDPR Variables. '--srfm-checkbox-label-font-size' => '14px', '--srfm-checkbox-label-line-height' => '20px', '--srfm-checkbox-description-font-size' => '12px', '--srfm-checkbox-description-line-height' => '16px', '--srfm-check-ctn-width' => '16px', '--srfm-check-ctn-height' => '16px', '--srfm-check-svg-size' => '10px', '--srfm-checkbox-margin-top-frontend' => '2px', '--srfm-checkbox-margin-top-editor' => '3px', '--srfm-check-gap' => '8px', '--srfm-checkbox-description-margin-left' => '24px', // Phone Number field variables. '--srfm-flag-section-padding' => '10px 0 10px 12px', '--srfm-gap-between-icon-text' => '8px', // Label Variables. '--srfm-label-font-size' => '14px', '--srfm-label-line-height' => '20px', // Description Variables. '--srfm-description-font-size' => '12px', '--srfm-description-line-height' => '16px', // Button Variables. '--srfm-btn-padding' => '8px 14px', '--srfm-btn-font-size' => '14px', '--srfm-btn-line-height' => '20px', // Multi Choice Variables. '--srfm-multi-choice-horizontal-padding' => '16px', '--srfm-multi-choice-vertical-padding' => '16px', '--srfm-multi-choice-internal-option-gap' => '8px', '--srfm-multi-choice-vertical-svg-size' => '32px', '--srfm-multi-choice-horizontal-image-size' => '20px', '--srfm-multi-choice-vertical-image-size' => '100px', '--srfm-multi-choice-outer-padding' => '0', ], 'medium' => [ '--srfm-row-gap-between-blocks' => '18px', // Address block gap and spacing variables. '--srfm-address-label-font-size' => '16px', '--srfm-address-label-line-height' => '24px', '--srfm-address-description-font-size' => '14px', '--srfm-address-description-line-height' => '20px', '--srfm-col-gap-between-fields' => '16px', '--srfm-row-gap-between-fields' => '16px', '--srfm-gap-below-address-label' => '14px', // Input Field Variables. '--srfm-input-height' => '44px', '--srfm-input-field-font-size' => '16px', '--srfm-input-field-line-height' => '24px', '--srfm-input-field-margin-top' => '6px', '--srfm-input-field-margin-bottom' => '6px', // Checkbox and GDPR Variables. '--srfm-checkbox-label-font-size' => '16px', '--srfm-checkbox-label-line-height' => '24px', '--srfm-checkbox-description-font-size' => '14px', '--srfm-checkbox-description-line-height' => '20px', '--srfm-checkbox-margin-top-frontend' => '4px', '--srfm-checkbox-margin-top-editor' => '6px', '--srfm-checkbox-description-margin-left' => '24px', // Label Variables. '--srfm-label-font-size' => '16px', '--srfm-label-line-height' => '24px', // Description Variables. '--srfm-description-font-size' => '14px', '--srfm-description-line-height' => '20px', // Button Variables. '--srfm-btn-padding' => '10px 14px', '--srfm-btn-font-size' => '16px', '--srfm-btn-line-height' => '24px', // Multi Choice Variables. '--srfm-multi-choice-horizontal-padding' => '20px', '--srfm-multi-choice-vertical-padding' => '20px', '--srfm-multi-choice-vertical-svg-size' => '40px', '--srfm-multi-choice-horizontal-image-size' => '24px', '--srfm-multi-choice-vertical-image-size' => '120px', '--srfm-multi-choice-outer-padding' => '2px', ], 'large' => [ '--srfm-row-gap-between-blocks' => '20px', // Address Block Gap and Spacing Variables. '--srfm-address-label-font-size' => '18px', '--srfm-address-label-line-height' => '28px', '--srfm-address-description-font-size' => '16px', '--srfm-address-description-line-height' => '24px', '--srfm-col-gap-between-fields' => '16px', '--srfm-row-gap-between-fields' => '20px', '--srfm-gap-below-address-label' => '16px', // Dropdown Variables. '--srfm-dropdown-font-size' => '16px', '--srfm-dropdown-gap-between-input-menu' => '6px', '--srfm-dropdown-badge-padding' => '6px 6px', '--srfm-dropdown-multiselect-font-size' => '14px', '--srfm-dropdown-multiselect-line-height' => '20px', '--srfm-dropdown-padding-right' => '14px', // Input Field Variables. '--srfm-input-height' => '48px', '--srfm-input-field-padding' => '10px 14px', '--srfm-input-field-font-size' => '18px', '--srfm-input-field-line-height' => '28px', '--srfm-input-field-margin-top' => '8px', '--srfm-input-field-margin-bottom' => '8px', // Checkbox and GDPR Variables. '--srfm-checkbox-label-font-size' => '18px', '--srfm-checkbox-label-line-height' => '28px', '--srfm-checkbox-description-font-size' => '16px', '--srfm-checkbox-description-line-height' => '24px', '--srfm-check-ctn-width' => '20px', '--srfm-check-ctn-height' => '20px', '--srfm-check-svg-size' => '14px', '--srfm-check-gap' => '10px', '--srfm-checkbox-margin-top-frontend' => '4px', '--srfm-checkbox-margin-top-editor' => '5px', '--srfm-checkbox-description-margin-left' => '30px', // Label Variables. '--srfm-label-font-size' => '18px', '--srfm-label-line-height' => '28px', // Description Variables. '--srfm-description-font-size' => '16px', '--srfm-description-line-height' => '24px', // Button Variables. '--srfm-btn-padding' => '10px 14px', '--srfm-btn-font-size' => '18px', '--srfm-btn-line-height' => '28px', // Multi Choice Variables. '--srfm-multi-choice-horizontal-padding' => '24px', '--srfm-multi-choice-vertical-padding' => '24px', '--srfm-multi-choice-internal-option-gap' => '12px', '--srfm-multi-choice-vertical-svg-size' => '48px', '--srfm-multi-choice-horizontal-image-size' => '28px', '--srfm-multi-choice-vertical-image-size' => '140px', '--srfm-multi-choice-outer-padding' => '4px', ], ] ); // Return complete sizes array if field_spacing is false. Required in case of JS for Editor changes. if ( ! $field_spacing ) { return $sizes; } $selected_size = $sizes['small']; if ( 'small' !== $field_spacing && isset( $sizes[ $field_spacing ] ) ) { $selected_size = array_merge( $selected_size, $sizes[ $field_spacing ] ); } return $selected_size; } /** * Array of SureForms blocks which get have user input. * * @since 0.0.10 * @return array<string> */ public static function get_sureforms_blocks() { return apply_filters( 'srfm_blocks', [ 'srfm/input', 'srfm/email', 'srfm/textarea', 'srfm/number', 'srfm/checkbox', 'srfm/gdpr', 'srfm/phone', 'srfm/address', 'srfm/dropdown', 'srfm/multi-choice', 'srfm/radio', 'srfm/submit', 'srfm/url', 'srfm/payment', ] ); } /** * Render a site key missing error message. * * @param string $provider_name Name of the captcha provider (e.g., HCaptcha, Google reCAPTCHA, Turnstile). * @since 1.7.0 * @since 1.7.1 moved to inc/helper.php from inc/generate-form-markup.php * @return void */ public static function render_missing_sitekey_error( $provider_name ) { $icon = self::fetch_svg( 'info_circle', '', 'aria-hidden="true"' ); ?> <p id="sitekey-error" class="srfm-common-error-message srfm-error-message"> <?php echo wp_kses( $icon, self::$allowed_tags_svg ); ?> <span class="srfm-error-content"> <?php echo esc_html( sprintf( /* translators: %s: Provider name like HCaptcha, Google reCAPTCHA, Turnstile */ __( '%s sitekey is missing. Please contact your site administrator.', 'sureforms' ), $provider_name ) ); ?> </span> </p> <?php } /** * Parse and sanitize an email list string which may contain: * * @param string $input email addresses. * @since 1.13.2 * @return string Sanitized email header string. */ public static function sanitize_email_header( $input ) { if ( empty( $input ) ) { return ''; } $parts = explode( ',', $input ); $output = []; foreach ( $parts as $part ) { $part = trim( $part ); // Match "Name <email>". if ( preg_match( '/^(.*)<(.+)>$/', $part, $matches ) ) { $name = trim( $matches[1], "\" \t\n\r\0\x0B" ); // trim quotes. $email = sanitize_email( trim( $matches[2] ) ); if ( is_email( $email ) ) { $safe_name = sanitize_text_field( $name ); $output[] = $safe_name . ' <' . $email . '>'; } } else { // Plain email case. $email = sanitize_email( $part ); if ( is_email( $email ) ) { $output[] = $email; } } } return ! empty( $output ) ? implode( ', ', $output ) : ''; } /** * Process blocks and inner blocks. * * @param array<mixed> $blocks The block data. * @param array<string> $slugs The array of existing slugs. * @param bool $updated The array of existing slugs. * @param string $prefix The array of existing slugs. * @param bool $skip_checking_existing_slug Skips the checking of existing slug if passed true. More information documented inside this function. * @since 0.0.10 * @return array */ public static function process_blocks( $blocks, &$slugs, &$updated, $prefix = '', $skip_checking_existing_slug = false ) { if ( ! is_array( $blocks ) ) { return [ $blocks, $slugs, $updated ]; } foreach ( $blocks as $index => $block ) { if ( ! is_array( $block ) ) { continue; } // Checking only for SureForms blocks which can have user input. if ( empty( $block['blockName'] ) || ! in_array( $block['blockName'], self::get_sureforms_blocks(), true ) ) { continue; } /** * Lets continue if slug already exists. * This will ensure that we don't update already existing slugs. */ if ( isset( $block['attrs'] ) && ! empty( $block['attrs']['slug'] ) && ! in_array( $block['attrs']['slug'], $slugs, true ) ) { // Made it associative array, so that we can directly check it using block_id rather than mapping or using "in_array" for the checks. $slugs[ $block['attrs']['block_id'] ] = self::get_string_value( $block['attrs']['slug'] ); if ( is_array( $block['innerBlocks'] ) && ! empty( $block['innerBlocks'] ) ) { [ $blocks[ $index ]['innerBlocks'], $slugs, $updated ] = self::process_blocks( $block['innerBlocks'], $slugs, $updated, '' ); } continue; } if ( $skip_checking_existing_slug && empty( $block['innerBlocks'] ) && isset( $slugs[ $block['attrs']['block_id'] ] ) ) { /** * Skip re-processing of the already process or existing slugs if above parameter "$skip_checking_existing_slug" is passed as true. * This is helpful in the scenarios where we need to compare and verify between already saved blocks and new unsaved blocks parsed * from the contents. * * However, it is also necessary to make sure if that current block is not a parent / wrapper block * by checking "$block['innerBlocks']" empty. * * And finally, checking if the block-id "$block['attrs']['block_id']" is already set in the list of "$slugs", * making sure that we are only processing the new blocks. */ continue; } if ( is_array( $blocks[ $index ]['attrs'] ) ) { $blocks[ $index ]['attrs']['slug'] = self::generate_unique_block_slug( $block, $slugs, $prefix ); $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. $updated = true; if ( is_array( $block['innerBlocks'] ) && ! empty( $block['innerBlocks'] ) ) { [ $blocks[ $index ]['innerBlocks'], $slugs, $updated ] = self::process_blocks( $block['innerBlocks'], $slugs, $updated, $blocks[ $index ]['attrs']['slug'] ); } } } return [ $blocks, $slugs, $updated ]; } /** * Generates slug based on the provided block and existing slugs. * * @param array<mixed> $block The block data. * @param array<string> $slugs The array of existing slugs. * @param string $prefix The array of existing slugs. * @since 0.0.10 * @return string The generated unique block slug. */ public static function generate_unique_block_slug( $block, $slugs, $prefix ) { $slug = is_string( $block['blockName'] ) ? $block['blockName'] : ''; if ( ! empty( $block['attrs']['label'] ) && is_string( $block['attrs']['label'] ) ) { $slug = sanitize_title( $block['attrs']['label'] ); } if ( ! empty( $prefix ) ) { $slug = $prefix . '-' . $slug; } return self::generate_slug( $slug, $slugs ); } /** * This function ensures that the slug is unique. * If the slug is already taken, it appends a number to the slug to make it unique. * * @param string $slug test to be converted to slug. * @param array<string> $slugs An array of existing slugs. * @since 0.0.10 * @return string The unique slug. */ public static function generate_slug( $slug, $slugs ) { $slug = sanitize_title( $slug ); if ( ! in_array( $slug, $slugs, true ) ) { return $slug; } $index = 1; while ( in_array( $slug . '-' . $index, $slugs, true ) ) { $index++; } return $slug . '-' . $index; } /** * Encode data to JSON. This function will encode the data with JSON_UNESCAPED_SLASHES and JSON_UNESCAPED_UNICODE. * * @since 0.0.11 * @param array<mixed> $data The data to encode. * @return string|false The JSON representation of the value on success or false on failure. */ public static function encode_json( $data ) { return wp_json_encode( $data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE ); } /** * Returns true if SureTriggers plugin is ready for the custom app. * * @since 1.0.3 * @return bool Returns true if SureTriggers plugin is ready for the custom app. */ public static function is_suretriggers_ready() { if ( ! defined( 'SURE_TRIGGERS_FILE' ) ) { // Probably plugin is de-activated or not installed at all. return false; } $suretriggers_data = get_option( 'suretrigger_options', [] ); if ( ! is_array( $suretriggers_data ) || empty( $suretriggers_data['secret_key'] ) || ! is_string( $suretriggers_data['secret_key'] ) ) { // SureTriggers is not authenticated yet. return false; } return true; } /** * Registers script translations for a specific handle. * * This function sets the script translations for a given script handle, allowing * localization of JavaScript strings using the specified text domain and path. * * @param string $handle The script handle to apply translations to. * @param string $domain Optional. The text domain for translations. Default is 'sureforms'. * @param string $path Optional. The path to the translation files. Default is the 'languages' folder in the SureForms directory. * * @since 1.0.5 * @return void */ public static function register_script_translations( $handle, $domain = 'sureforms', $path = SRFM_DIR . 'languages' ) { wp_set_script_translations( $handle, $domain, $path ); } /** * Validates whether the specified conditions or a single key-value pair exist in the request context. * * - If `$conditions` is provided as an array, it will validate all key-value pairs in `$conditions` * against the `$_REQUEST` superglobal. * - If `$conditions` is empty, it validates a single key-value pair from `$key` and `$value`. * * @param string $value The expected value to match in the request if `$conditions` is not used. * @param string $key The key to check for in the request if `$conditions` is not used. * @param array<string, string> $conditions An optional associative array of key-value pairs to validate. * @since 1.1.1 * @return bool Returns true if all conditions are met or the single key-value pair is valid, otherwise false. */ public static function validate_request_context( $value, $key = 'post_type', $conditions = [] ) { // If conditions are provided, validate all key-value pairs in the conditions array. if ( ! empty( $conditions ) ) { foreach ( $conditions as $condition_key => $condition_value ) { if ( ! isset( $_REQUEST[ $condition_key ] ) || $_REQUEST[ $condition_key ] !== $condition_value ) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended -- This is a controlled comparison of request values. // Return false if any condition is not satisfied. return false; } } // Return true if all conditions are satisfied. return true; } // Validate $value and $key when no conditions are provided. if ( empty( $key ) || empty( $value ) ) { return false; } // Validate a single key-value pair when no conditions are provided. 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. } /** * Retrieve the list of excluded fields for form data processing. * * This method returns an array of field keys that should be excluded when * processing form data. * * @since 1.1.1 * @return array<string> Returns the string array of excluded fields. */ public static function get_excluded_fields() { $excluded_fields = [ 'srfm-honeypot-field', 'g-recaptcha-response', 'srfm-sender-email-field', 'form-id' ]; return apply_filters( 'srfm_excluded_fields', $excluded_fields ); } /** * Check whether the current page is a SureForms admin page. * * @since 1.2.2 * @return bool Returns true if the current page is a SureForms admin page, otherwise false. */ public static function is_sureforms_admin_page() { $current_screen = get_current_screen(); $is_screen_sureforms_menu = self::validate_request_context( 'sureforms_menu', 'page' ); $is_screen_add_new_form = self::validate_request_context( 'add-new-form', 'page' ); $is_screen_sureforms_form_settings = self::validate_request_context( 'sureforms_form_settings', 'page' ); $is_screen_sureforms_entries = self::validate_request_context( SRFM_ENTRIES, 'page' ); $is_post_type_sureforms_form = $current_screen && SRFM_FORMS_POST_TYPE === $current_screen->post_type; return $is_screen_sureforms_menu || $is_screen_add_new_form || $is_screen_sureforms_form_settings || $is_screen_sureforms_entries || $is_post_type_sureforms_form; } /** * Filters and concatenates valid class names from an array. * * @param array<string> $class_names The array containing potential class names. * @since 1.4.0 * @return string The concatenated string of valid class names separated by spaces. */ public static function join_strings( $class_names ) { // Filter the array to include only valid class names. $valid_class_names = array_filter( $class_names, static function ( $value ) { return is_string( $value ) && '' !== $value && false !== $value; } ); // Concatenate the valid class names with spaces and return. return implode( ' ', $valid_class_names ); } /** * Get SureForms Website URL. * * @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. * @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']. * @since 0.0.7 * @return string */ public static function get_sureforms_website_url( $trail, $utm_args = [] ) { $url = SRFM_WEBSITE; if ( ! empty( $trail ) && is_string( $trail ) ) { $url = SRFM_WEBSITE . $trail; } if ( ! is_array( $utm_args ) ) { $utm_args = []; } if ( class_exists( 'BSF_UTM_Analytics' ) ) { $url = \BSF_UTM_Analytics::get_utm_ready_link( $url, 'sureforms', $utm_args ); } return esc_url( $url ); } /** * Validates if the given string is a valid CSS class name. * * A valid CSS class name: * - Does not start with a digit, hyphen, or underscore. * - Can contain alphanumeric characters, underscores, hyphens, and Unicode letters. * * @param string $class_name The class name to validate. * * @since 1.3.1 * @return bool True if the class name is valid, otherwise false. */ public static function is_valid_css_class_name( $class_name ) { // Regular expression to validate a Unicode-aware CSS class name. $class_name_regex = '/^[^\d\-_][\w\p{L}\p{N}\-_]*$/u'; // Check if the className matches the pattern. return preg_match( $class_name_regex, $class_name ) === 1; } /** * Get the gradient css for given gradient parameters. * * @param string $type The type of gradient. Default 'linear'. * @param string $color1 The first color of the gradient. Default '#FFC9B2'. * @param string $color2 The second color of the gradient. Default '#C7CBFF'. * @param int $loc1 The location of the first color. Default 0. * @param int $loc2 The location of the second color. Default 100. * @param int $angle The angle of the gradient. Default 90. * * @since 1.4.4 * @return string The gradient css. */ public static function get_gradient_css( $type = 'linear', $color1 = '#FFC9B2', $color2 = '#C7CBFF', $loc1 = 0, $loc2 = 100, $angle = 90 ) { if ( 'linear' === $type ) { return "linear-gradient({$angle}deg, {$color1} {$loc1}%, {$color2} {$loc2}%)"; } return "radial-gradient({$color1} {$loc1}%, {$color2} {$loc2}%)"; } /** * Return the classes based on background and overlay type to add to the form container. * * @param string $background_type The background type. * @param string $overlay_type The overlay type. * @param string $bg_image The background image url. * * @since 1.4.4 * @return string The classes to add to the form container. */ public static function get_background_classes( $background_type, $overlay_type, $bg_image = '' ) { if ( empty( $background_type ) ) { $background_type = 'color'; } $background_type_class = ''; $overlay_class = 'image' === $background_type && ! empty( $bg_image ) && $overlay_type ? "srfm-overlay-{$overlay_type}" : ''; // Set the class based on the background type. switch ( $background_type ) { case 'image': $background_type_class = 'srfm-bg-image'; break; case 'gradient': $background_type_class = 'srfm-bg-gradient'; break; default: $background_type_class = 'srfm-bg-color'; break; } return self::join_strings( [ $background_type_class, $overlay_class ] ); } /** * Custom escape function for the textarea with rich text support. * * @param string $content The content submitted by the user in the textarea block. * @since 1.7.1 * * @return string Escaped content. */ public static function esc_textarea( $content ) { $content = wpautop( self::sanitize_textarea( $content ) ); return trim( str_replace( [ "\r\n", "\r", "\n" ], '', $content ) ); } /** * Custom sanitization function for the textarea with rich text support. * * @param string $content The content submitted by the user in the textarea block. * @since 1.7.1 * * @return string Sanitized content. */ public static function sanitize_textarea( $content ) { $count = 1; $content = convert_invalid_entities( $content ); // Remove the 'script' and 'style' tags recursively from the content. while ( $count ) { $content = preg_replace( '@<(script|style)[^>]*?>.*?</\\1>@si', '', self::get_string_value( $content ), - 1, $count ); } // Disable the safe style attribute parsing for the textarea block. add_filter( 'safe_style_css', [ self::class, 'disable_style_attr_parsing' ], 10, 1 ); $content = wp_kses_post( self::get_string_value( $content ) ); // Remove the filter after sanitization to avoid affecting other blocks. remove_filter( 'safe_style_css', [ self::class, 'disable_style_attr_parsing' ], 10 ); // Ensure all tags are balanced. return force_balance_tags( $content ); } /** * Disable parsing of style attributes for the textarea block. * * @param array<string> $allowed_styles The allowed styles. * @since 1.7.1 * * @return array An empty array to disable style attribute parsing. */ public static function disable_style_attr_parsing( $allowed_styles ) { unset( $allowed_styles ); // Disable parsing of style attributes. return []; } /** * Strips JavaScript attributes from HTML content. * * @param string $html The HTML content to process. * @param bool $remove_link_target Optional. When true, removes target and strips noopener/noreferrer from rel on links. Default false. * @since 1.7.1 * @since 2.5.2 Added $remove_link_target parameter. * @return string The cleaned HTML content without JavaScript attributes. */ public static function strip_js_attributes( $html, $remove_link_target = false ) { $dom = new \DOMDocument(); // Suppress warnings due to malformed HTML. libxml_use_internal_errors( true ); $loaded = $dom->loadHTML( '<?xml encoding="utf-8" ?>' . $html ); libxml_clear_errors(); if ( ! $loaded ) { return $html; // Return original HTML if loading fails. } $xpath = new \DOMXPath( $dom ); // 1. Remove all <script> tags. $script_nodes = $xpath->query( '//script' ); if ( $script_nodes instanceof \DOMNodeList ) { foreach ( $script_nodes as $script ) { // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- This is a DOM element. $parent_node = $script->parentNode; if ( $parent_node instanceof \DOMNode ) { $parent_node->removeChild( $script ); } } } // 2. Remove all attributes that start with "on" (like onclick, onmouseover, etc.). $elements_with_on_attrs = $xpath->query( '//*[@*[starts-with(name(), "on")]]' ); if ( $elements_with_on_attrs instanceof \DOMNodeList ) { foreach ( $elements_with_on_attrs as $element ) { if ( $element instanceof \DOMElement && $element->hasAttributes() ) { foreach ( iterator_to_array( $element->attributes ) as $attr ) { if ( $attr instanceof \DOMAttr && stripos( $attr->name, 'on' ) === 0 ) { $element->removeAttribute( $attr->name ); } } } } } // 3. Optionally remove target and target-related rel values (noopener, noreferrer) from links. if ( $remove_link_target ) { $links = $xpath->query( '//a[@target]' ); if ( $links instanceof \DOMNodeList ) { foreach ( $links as $link ) { if ( $link instanceof \DOMElement ) { $link->removeAttribute( 'target' ); $rel = $link->getAttribute( 'rel' ); if ( $rel ) { $cleaned_rel = trim( (string) preg_replace( '/\s+/', ' ', (string) preg_replace( '/\b(noopener|noreferrer)\b/i', '', $rel ) ) ); if ( $cleaned_rel ) { $link->setAttribute( 'rel', $cleaned_rel ); } else { $link->removeAttribute( 'rel' ); } } } } } } // Return cleaned HTML. $body = $dom->getElementsByTagName( 'body' )->item( 0 ); if ( $body instanceof \DOMNode ) { $cleaned_html = $dom->saveHTML( $body ); return is_string( $cleaned_html ) ? $cleaned_html : ''; } return ''; } /** * Encodes the given string with base64. * Moved from admin class to here. * * @param string $logo contains svg's. * @return string */ public static function encode_svg( $logo ) { return 'data:image/svg+xml;base64,' . base64_encode( $logo ); // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode } /** * Get plugin status * * @since 0.0.1 * @since 1.7.0 moved to inc/helper.php from inc/admin-ajax.php * * @param string $plugin_init_file Plugin init file. * @return string */ public static function get_plugin_status( $plugin_init_file ) { $installed_plugins = get_plugins(); if ( ! isset( $installed_plugins[ $plugin_init_file ] ) ) { return 'Install'; } if ( is_plugin_active( $plugin_init_file ) ) { return 'Activated'; } return 'Installed'; } /** * Return the first installed plugin from a list, or a default if none exist. * * @since 2.0.0 * * @param array<string> $plugins_to_check Plugin file paths to check, in priority order. * @param string $default Optional fallback plugin file path. Default empty string. * * @return string First installed plugin file path, or the default. */ public static function get_plugin_if_installed( $plugins_to_check, $default = '' ) { if ( ! function_exists( 'get_plugins' ) ) { require_once ABSPATH . 'wp-admin/includes/plugin.php'; } $plugins = get_plugins(); foreach ( self::get_array_value( $plugins_to_check ) as $plugin_file ) { if ( isset( $plugins[ $plugin_file ] ) ) { return $plugin_file; } } return $default; } /** * Check which Starter Templates plugin is installed and return its main plugin file path. * * @since 1.7.3 * * @return string The main plugin file path of the installed Starter Templates plugin. */ public static function check_starter_template_plugin() { return self::get_plugin_if_installed( [ 'astra-pro-sites/astra-pro-sites.php' ], 'astra-sites/astra-sites.php' ); } /** * Get sureforms recommended integrations. * * @since 0.0.1 * @since 1.7.0 moved to inc/helper.php from inc/admin-ajax.php * * @return array<mixed> */ public static function sureforms_get_integration() { $suretrigger_connected = apply_filters( 'suretriggers_is_user_connected', '' ); $logo_sure_triggers = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/suretriggers.svg' ); $logo_full = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/suretriggers_full.svg' ); $logo_sure_mails = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/suremails.svg' ); $logo_uae = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/uae.svg' ); $logo_starter_templates = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/starterTemplates.svg' ); $logo_sure_rank = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/surerank.svg' ); $logo_sure_contact = file_get_contents( plugin_dir_path( SRFM_FILE ) . 'images/surecontact.svg' ); $integrations = [ 'sure_contact' => [ 'title' => __( 'SureContact', 'sureforms' ), 'singleLineDescription' => __( 'Turn Emails Into Revenue with a CRM Built for Your Website!', 'sureforms' ), 'subtitle' => __( 'Send newsletters, run campaigns, set up automations, manage contacts, and see exactly how much revenue your emails generate, all in one place.', 'sureforms' ), 'status' => self::get_plugin_status( 'surecontact/surecontact.php' ), 'slug' => 'surecontact', 'path' => 'surecontact/surecontact.php', 'logo' => self::encode_svg( is_string( $logo_sure_contact ) ? $logo_sure_contact : '' ), ], 'sure_mails' => [ 'title' => __( 'SureMail', 'sureforms' ), 'singleLineDescription' => __( 'Boost Your Email Deliverability Instantly!', 'sureforms' ), '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' ), 'status' => self::get_plugin_status( 'suremails/suremails.php' ), 'slug' => 'suremails', 'path' => 'suremails/suremails.php', 'logo' => self::encode_svg( is_string( $logo_sure_mails ) ? $logo_sure_mails : '' ), ], 'sure_triggers' => [ 'title' => __( 'OttoKit', 'sureforms' ), 'singleLineDescription' => __( 'Automate your WordPress workflows effortlessly.', 'sureforms' ), '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' ), 'status' => self::get_plugin_status( 'suretriggers/suretriggers.php' ), 'slug' => 'suretriggers', 'path' => 'suretriggers/suretriggers.php', 'logo' => self::encode_svg( is_string( $logo_sure_triggers ) ? $logo_sure_triggers : '' ), 'logo_full' => self::encode_svg( is_string( $logo_full ) ? $logo_full : '' ), 'connected' => $suretrigger_connected, 'connection_url' => admin_url( 'admin.php?page=suretriggers' ), ], 'starter_templates' => [ 'title' => __( 'Starter Templates', 'sureforms' ), 'singleLineDescription' => __( 'Launch Beautiful Websites in Minutes!', 'sureforms' ), 'subtitle' => __( 'Choose from professionally designed templates, import with one click, and customize effortlessly to match your brand.', 'sureforms' ), 'status' => self::get_plugin_status( self::check_starter_template_plugin() ), 'slug' => 'astra-sites', 'path' => self::check_starter_template_plugin(), 'logo' => self::encode_svg( is_string( $logo_starter_templates ) ? $logo_starter_templates : '' ), ], ]; $elementor_installed = self::get_plugin_if_installed( [ 'elementor/elementor.php' ] ); if ( $elementor_installed ) { $integrations['uae'] = [ 'title' => __( 'Ultimate Addons for Elementor', 'sureforms' ), 'singleLineDescription' => __( 'Power Up Elementor to Build Stunning Websites Faster!', 'sureforms' ), 'subtitle' => __( 'Enhance Elementor with powerful widgets and templates. Build stunning, high-performing websites faster with creative design elements and seamless customization.', 'sureforms' ), 'status' => self::get_plugin_status( 'header-footer-elementor/header-footer-elementor.php' ), 'slug' => 'header-footer-elementor', 'path' => 'header-footer-elementor/header-footer-elementor.php', 'logo' => self::encode_svg( is_string( $logo_uae ) ? $logo_uae : '' ), ]; } else { $integrations['sure_rank'] = [ 'title' => __( 'SureRank', 'sureforms' ), 'singleLineDescription' => __( 'Elevate Your SEO and Climb Search Rankings Effortlessly!', 'sureforms' ), 'subtitle' => __( 'Boost your website\'s visibility with smart SEO automation. Optimize content, track keyword performance, and get actionable insights, all inside WordPress.', 'sureforms' ), 'status' => self::get_plugin_status( 'surerank/surerank.php' ), 'slug' => 'surerank', 'path' => 'surerank/surerank.php', 'logo' => self::encode_svg( is_string( $logo_sure_rank ) ? $logo_sure_rank : '' ), ]; } return apply_filters( 'srfm_integrated_plugins', $integrations ); } /** * Get the current rotating plugin for the banner. * * Plugins rotate every 2 days. Only non-activated plugins are shown. * Returns false if all plugins are activated. * * @since 2.0.0 * @return array<string, mixed>|false The current plugin data or false if all plugins are activated. */ public static function get_rotating_plugin_banner() { $all_plugins = self::sureforms_get_integration(); if ( ! is_array( $all_plugins ) ) { return false; } $available_plugins = []; // Only include non-activated plugins. foreach ( $all_plugins as $plugin ) { if ( ! is_array( $plugin ) ) { continue; } if ( isset( $plugin['status'] ) && is_string( $plugin['status'] ) && 'Activated' !== $plugin['status'] ) { $available_plugins[] = $plugin; } } // Re-index the array to have sequential numeric keys. $available_plugins = array_values( $available_plugins ); $total_plugins = count( $available_plugins ); // Hide section if all plugins are active. if ( 0 === $total_plugins ) { return false; } // Get stored rotation data. $rotation_data = self::get_srfm_option( 'plugin_banner_rotation', [] ); if ( ! is_array( $rotation_data ) ) { $rotation_data = []; } // Initialize rotation data if empty. if ( empty( $rotation_data ) ) { $current_time = time(); self::update_srfm_option( 'plugin_banner_rotation', [ 'last_rotation_date' => $current_time, 'plugin_index' => 0, ] ); return isset( $available_plugins[0] ) && is_array( $available_plugins[0] ) ? $available_plugins[0] : false; } $last_rotation_date = isset( $rotation_data['last_rotation_date'] ) && is_int( $rotation_data['last_rotation_date'] ) ? $rotation_data['last_rotation_date'] : 0; $plugin_index = isset( $rotation_data['plugin_index'] ) && is_numeric( $rotation_data['plugin_index'] ) ? intval( $rotation_data['plugin_index'] ) : 0; $current_time = time(); $days_since_rotation = ( $current_time - $last_rotation_date ) / DAY_IN_SECONDS; // Rotate every 2 days. if ( $days_since_rotation >= 2 ) { // Rotate to next plugin. ++$plugin_index; $plugin_index %= $total_plugins; // Update the rotation data. self::update_srfm_option( 'plugin_banner_rotation', [ 'last_rotation_date' => $current_time, 'plugin_index' => $plugin_index, ] ); } // Ensure the index is within bounds. if ( $plugin_index >= $total_plugins ) { $plugin_index = 0; } return isset( $available_plugins[ $plugin_index ] ) && is_array( $available_plugins[ $plugin_index ] ) ? $available_plugins[ $plugin_index ] : false; } /** * Get a value from the srfm_options array. * * @param string $key The key to retrieve. * @param mixed $default The default value to return if the key does not exist. * @since 1.8.0 * @return mixed */ public static function get_srfm_option( $key, $default = null ) { $options = get_option( 'srfm_options', [] ); if ( ! is_array( $options ) ) { $options = []; } return array_key_exists( $key, $options ) ? $options[ $key ] : $default; } /** * Update a value in the srfm_options array. * * @param string $key The key to update. * @param mixed $value The value to set. * @since 1.8.0 * @return void */ public static function update_srfm_option( $key, $value ) { $options = get_option( 'srfm_options', [] ); if ( ! is_array( $options ) ) { $options = []; } $options[ $key ] = $value; update_option( 'srfm_options', $options ); } /** * Get the WordPress file types. * * @since 1.7.4 * @return array<string,mixed> An associative array representing the file types. */ public static function get_wp_file_types() { $formats = []; $mimes = get_allowed_mime_types(); $maxsize = wp_max_upload_size() / 1048576; if ( ! empty( $mimes ) ) { foreach ( $mimes as $type => $mime ) { $multiple = explode( '|', $type ); foreach ( $multiple as $single ) { $formats[] = $single; } } } return [ 'formats' => $formats, 'maxsize' => $maxsize, ]; } /** * Determines if the SureForms Pro plugin is installed and active. * * Checks for the presence of the SRFM_PRO_VER constant. * * @since 1.8.0 * * @return bool True if the Pro plugin is active; false otherwise. */ public static function has_pro() { return defined( 'SRFM_PRO_VER' ); } /** * Verifies the request by checking the nonce and user capabilities. * * @param string $request_type The type of request, either 'rest' or 'ajax'. * @param string $nonce_action The action name for the nonce. * @param string $nonce_name The name of the nonce field. * @param string $capability The capability required to perform the action. Default is 'manage_options'. * * @since 1.10.0 * @return void */ public static function verify_nonce_and_capabilities( $request_type, $nonce_action, $nonce_name, $capability = 'manage_options' ) { if ( ! is_string( $nonce_action ) || ! is_string( $nonce_name ) || empty( $nonce_action ) || empty( $nonce_name ) ) { wp_send_json_error( [ 'message' => __( 'Invalid nonce action or name.', 'sureforms' ) ], 400 ); } // Verify nonce for security. if ( 'rest' === $request_type ) { // For REST API requests, use the WP_REST_Request object to verify the nonce. if ( ! wp_verify_nonce( $nonce_action, $nonce_name ) ) { wp_send_json_error( [ 'message' => __( 'Invalid security token.', 'sureforms' ) ], 403 ); } } elseif ( 'ajax' === $request_type ) { // For non-REST requests, use the standard nonce verification. if ( ! check_ajax_referer( $nonce_action, $nonce_name, false ) ) { wp_send_json_error( [ 'message' => __( 'Invalid security token.', 'sureforms' ) ], 403 ); } } else { // If the request type is not recognized, return an error. wp_send_json_error( [ 'message' => __( 'Invalid request type.', 'sureforms' ) ], 400 ); } // Check user capabilities. if ( ! current_user_can( $capability ) ) { wp_send_json_error( [ 'message' => esc_html__( 'You do not have permission to perform this action.', 'sureforms' ) ], 403 ); } } /** * Get the block name from a field name by extracting the first two parts. * * @param string $field_name The full field name (e.g., 'srfm-text-lbl-123'). * * @since 1.11.0 * @return string The block name (e.g., 'srfm-text'). */ public static function get_block_name_from_field( $field_name ) { return implode( '-', array_slice( explode( '-', explode( '-lbl-', $field_name )[0] ), 0, 2 ) ); } /** * Check if any of the top 10 popular WordPress SMTP plugins is active using array_intersect. * * @since 1.9.1 * @return bool True if any SMTP plugin is active, false otherwise. */ public static function is_any_smtp_plugin_active() { $smtp_plugins = [ 'wp-mail-smtp/wp_mail_smtp.php', 'post-smtp/postman-smtp.php', 'easy-wp-smtp/easy-wp-smtp.php', 'wp-smtp/wp-smtp.php', 'newsletter/plugin.php', 'fluent-smtp/fluent-smtp.php', 'pepipost-smtp/pepipost-smtp.php', 'mail-bank/wp-mail-bank.php', 'smtp-mailer/smtp-mailer.php', 'suremails/suremails.php', 'site-mailer/site-mailer.php', ]; $active_plugins = (array) get_option( 'active_plugins', [] ); // For multisite, merge sitewide active plugins. if ( is_multisite() ) { $network_plugins = (array) get_site_option( 'active_sitewide_plugins', [] ); $active_plugins = array_merge( $active_plugins, array_keys( $network_plugins ) ); } return (bool) array_intersect( $smtp_plugins, $active_plugins ); } /** * Apply a filter and return the filtered value only if it's a non-empty array. * Otherwise, return the default array. * * @param string $filter_name The name of the filter to apply. * @param mixed $default The default array to return if the filtered result is invalid. * @param mixed ...$args Additional arguments to pass to the filter. * * @return array The filtered array if valid, otherwise the default. */ public static function apply_filters_as_array( $filter_name, $default, ...$args ) { // Ensure $default is an array. if ( ! is_array( $default ) ) { $default = []; } // Validate the filter name. if ( ! is_string( $filter_name ) || empty( $filter_name ) ) { return $default; } // Apply the filter with additional arguments. $filtered = apply_filters( $filter_name, $default, ...$args ); // Return filtered result if it's a non-empty array. return is_array( $filtered ) && ! empty( $filtered ) ? $filtered : $default; } /** * Get forms with entry counts for a specific time period. * * @param int $timestamp The timestamp to get entries after. * @param int $limit Maximum number of forms to return (0 for all). * @param bool $sort Whether to sort by entry count descending. * @return array Array of form data with entry counts. * @since 1.9.1 */ public static function get_forms_with_entry_counts( $timestamp, $limit = 0, $sort = true ) { // Get all published forms with post objects for bulk title access. $args = [ 'post_type' => SRFM_FORMS_POST_TYPE, 'posts_per_page' => -1, 'post_status' => 'publish', 'orderby' => 'ID', 'order' => 'DESC', 'no_found_rows' => true, 'update_post_term_cache' => false, 'update_post_meta_cache' => false, ]; $query = new \WP_Query( $args ); if ( ! $query->have_posts() ) { return []; } $all_forms = []; // Process posts directly from the query results without touching global $post. foreach ( $query->posts as $form ) { // Ensure we have a valid post object. if ( ! $form instanceof \WP_Post ) { continue; } $form_id = (int) $form->ID; if ( $form_id <= 0 ) { continue; } // Get entries count after the timestamp for this specific form. $entry_count = Entries::get_entries_count_after( $timestamp, $form_id ); // Get form title directly from post object, use "Blank Form" if empty. $form_title = $form->post_title; if ( empty( trim( self::get_string_value( $form_title ) ) ) ) { $form_title = __( 'Blank Form', 'sureforms' ); } $all_forms[] = [ 'form_id' => $form_id, 'title' => $form_title, 'count' => $entry_count, ]; } // Sort by count descending, then by form_id descending for consistency. if ( $sort ) { usort( $all_forms, static function( $a, $b ) { if ( $a['count'] === $b['count'] ) { return $b['form_id'] - $a['form_id']; } return $b['count'] - $a['count']; } ); } // Return limited results if specified. if ( $limit > 0 ) { return array_slice( $all_forms, 0, $limit ); } return $all_forms; } /** * Check if the given form ID is valid SureForms form ID. * A valid form ID is a numeric value that corresponds to an existing SureForms form in the database. * * @since 1.9.1 * * @param int|string|mixed $form_id The form ID to validate. * @return bool True if the form ID is valid, false otherwise. */ public static function is_valid_form( $form_id ) { // Check for a valid form ID. if ( empty( $form_id ) || ! is_numeric( $form_id ) ) { return false; } // Check if the form ID exists in the database. $form = get_post( self::get_integer_value( $form_id ) ); // If the form does not exist or is not of the correct post type, return false. if ( ! $form || ! is_a( $form, 'WP_Post' ) || SRFM_FORMS_POST_TYPE !== $form->post_type ) { return false; } return true; } /** * Get the timestamp from a string. * * This function uses WordPress's configured timezone (from Settings → General → Timezone) * to ensure consistent behavior regardless of the server's timezone settings. * * @param string $date The date in YYYY-MM-DD format (e.g., '2026-01-10'). * @param string $hours The hours in 12-hour format (e.g., '12', '01'-'12'). * @param string $minutes The minutes (e.g., '00', '00'-'59'). * @param string $meridiem The meridiem (e.g., 'AM' or 'PM'). * * @since 1.10.1 * @return int|false The timestamp if successful, false otherwise. */ public static function get_timestamp_from_string( $date, $hours = '12', $minutes = '00', $meridiem = 'AM' ) { if ( empty( $date ) || ! is_string( $date ) ) { return false; // Invalid input. } // Ensure the date is in a valid format of YYYY-MM-DD. if ( ! preg_match( '/^\d{4}-\d{2}-\d{2}$/', $date ) ) { return false; // Invalid date format. } $time_string = $date . ' ' . $hours . ':' . $minutes . ' ' . $meridiem; // Convert to timestamp using WordPress timezone. // This ensures the date/time is interpreted in the site's configured timezone, // not the server's timezone or PHP's default timezone. try { $datetime = date_create( $time_string, wp_timezone() ); if ( false === $datetime ) { return false; } $timestamp = $datetime->getTimestamp(); if ( is_int( $timestamp ) && $timestamp > 0 ) { return $timestamp; } } catch ( \Exception $e ) { // If timezone conversion fails, return false. return false; } // If conversion fails, return false. return false; } /** * Generate a unique ID for the saved form. * Also ensures that the generated ID does not already exist in the database table. * * @param class-string $class The class name where the get method is defined to check for existing IDs. * @param int<1, max> $length The length of the random bytes to generate. Default is 8. * @return string * @since 2.2.0 */ public static function generate_unique_id( $class, $length = 8 ) { // Ensure length is at least 1. $length = max( 1, $length ); do { $id = bin2hex( random_bytes( $length ) ); } while ( is_callable( [ $class, 'get' ] ) && call_user_func( [ $class, 'get' ], $id ) ); return $id; } /** * Log error messages to the error log. * * This function checks if error_log function exists, validates the message, * and logs it with the print_r second argument set to true. * * Logging is disabled by default. To enable logging, add this to wp-config.php: * define( 'SRFM_LOG', true ); * * @param mixed $message The error message to log. Can be string or any type. * @param string $prefix Optional prefix to add before the message. Default: 'Log :'. * * @since 2.0.0 * @return void */ public static function srfm_log( $message, $prefix = 'Log :' ) { // Check if logging is enabled via SRFM_LOG constant. if ( ! defined( 'SRFM_LOG' ) ) { return; } unset( $message, $prefix ); } /** * Encodes data to base64 after JSON encoding with validation. * * This function checks if the data is non-empty and valid for JSON encoding. * If data is not valid, returns an empty string. * Otherwise, it attempts to JSON encode and then base64 encode the result. * * @param mixed $data The data to JSON encode and then base64 encode. * @return string The base64-encoded JSON string, or empty string on failure. */ public static function srfm_base64_json_encode( $data ) { if ( empty( $data ) || ! is_array( $data ) ) { return ''; } $json = wp_json_encode( $data ); if ( false === $json || '' === $json ) { return ''; } // phpcs:ignore WordPress.PHP.DiscouragedPHPFunctions.obfuscation_base64_encode return base64_encode( $json ); } }
Save
Back