*/ private static $campaign_meta_defaults = [ 'goal_type' => 'raised_amount', 'goal_amount' => 0, 'campaign_status' => 'active', 'email_settings' => [], 'allow_fees_coverage' => false, 'require_terms' => false, 'terms_text' => '', 'thank_you_message' => '', ]; /** * Get a value from the suredonation_options array. * * @param string $key The key to retrieve. * @param mixed $default_value Default value if key doesn't exist. * @return mixed * @since 0.0.1 */ public static function get_suredonation_option( $key, $default_value = null ) { $options = get_option( self::OPTION_NAME, [] ); if ( ! is_array( $options ) ) { $options = []; } return array_key_exists( $key, $options ) ? $options[ $key ] : $default_value; } /** * Update a value in the suredonation_options array. * * @param string $key The key to update. * @param mixed $value The value to set. * @return bool True on success, false on failure. * @since 0.0.1 */ public static function update_suredonation_option( $key, $value ) { $options = get_option( self::OPTION_NAME, [] ); if ( ! is_array( $options ) ) { $options = []; } $options[ $key ] = $value; return update_option( self::OPTION_NAME, $options ); } /** * Get all campaign meta as an array. * * @param int $campaign_id Campaign post ID. * @return array Campaign meta values. * @since 0.0.1 */ public static function get_campaign_meta( $campaign_id ) { $raw = get_post_meta( $campaign_id, self::SUREDONATION_CAMPAIGN_META_KEY, true ); $meta = ! empty( $raw ) && is_string( $raw ) ? json_decode( $raw, true ) : []; if ( ! is_array( $meta ) ) { $meta = []; } return array_merge( self::$campaign_meta_defaults, $meta ); } /** * Get a single campaign meta value. * * @param int $campaign_id Campaign post ID. * @param string $key Meta key within the campaign meta array. * @param mixed $default_value Default value if not set. * @return mixed * @since 0.0.1 */ public static function get_campaign_meta_value( $campaign_id, $key, $default_value = null ) { $meta = self::get_campaign_meta( $campaign_id ); return $meta[ $key ] ?? $default_value; } /** * Update campaign meta. Merges provided values with existing meta. * * @param int $campaign_id Campaign post ID. * @param array $values Key-value pairs to update. * @return bool|int Meta ID on success, false on failure. * @since 0.0.1 */ public static function update_campaign_meta( $campaign_id, $values ) { $meta = self::get_campaign_meta( $campaign_id ); $meta = array_merge( $meta, $values ); return update_post_meta( $campaign_id, self::SUREDONATION_CAMPAIGN_META_KEY, wp_json_encode( $meta ) ); } /** * Checks if current value is string or else returns default value * * @param mixed $data data which need to be checked if is string. * @return string * @since 0.0.1 */ 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. * @return int * @since 0.0.1 */ 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; } /** * Safely converts a mixed value to float * * @param mixed $value The value to convert. * @param float $default_value Default value if conversion fails. * @return float * @since 0.0.1 */ public static function get_float_value( $value, $default_value = 0.0 ) { if ( is_numeric( $value ) ) { return (float) $value; } return $default_value; } /** * Safely get array value with type checking * * @param mixed $value The value to check. * @param array $default_value Default value if not an array. * @return array * @since 0.0.1 */ public static function get_array_value( $value, $default_value = [] ) { return is_array( $value ) ? $value : $default_value; } /** * Check if current user has required capability. * * @param string $capability Capability to check (default: 'manage_options'). * @param array $args Additional arguments for capability check. * @return bool True if user has capability. * @since 0.0.1 */ 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 ) ? current_user_can( $capability, ...$args ) : current_user_can( $capability ); } /** * Join an array of strings into a single string, filtering out empty values. * * @param array $strings Array of strings to join. * @param string $glue Separator to use (default: ' '). * @return string Joined string. * @since 0.0.1 */ public static function join_strings( $strings, $glue = ' ' ) { if ( ! is_array( $strings ) ) { return ''; } $filtered = array_filter( $strings, static function ( $item ) { return is_string( $item ) && '' !== trim( $item ); } ); return implode( $glue, array_map( 'trim', $filtered ) ); } /** * Process blocks to generate unique slugs for SureDonation blocks. * * Recursively processes all blocks and generates slugs for those that * don't have one set. Ensures all slugs are unique within the form. * * @param array $blocks The blocks to process. * @param array $slugs Array of existing slugs (keyed by block_id). * @param bool $updated Whether any blocks were updated. * @param string $prefix Optional prefix for nested blocks. * @return array{0: array, 1: array, 2: bool} Processed blocks, slugs, and updated flag. * @since 0.0.1 */ public static function process_blocks( $blocks, $slugs = [], $updated = false, $prefix = '' ) { if ( ! is_array( $blocks ) ) { return [ [], $slugs, $updated ]; } foreach ( $blocks as $index => $block ) { if ( ! is_array( $block ) ) { continue; } // Skip non-SureDonation blocks. if ( ! isset( $block['blockName'] ) || ! is_string( $block['blockName'] ) || strpos( $block['blockName'], 'sd/' ) !== 0 ) { // Process inner blocks if any. if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) { [ $blocks[ $index ]['innerBlocks'], $slugs, $updated ] = self::process_blocks( $block['innerBlocks'], $slugs, $updated, $prefix ); } continue; } // Skip if no attrs or slug is already set and block_id is in slugs array. if ( ! isset( $block['attrs'] ) || ! is_array( $block['attrs'] ) || ( ! empty( $block['attrs']['slug'] ) && isset( $block['attrs']['block_id'] ) && isset( $slugs[ $block['attrs']['block_id'] ] ) ) ) { // Process inner blocks if any. if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) { [ $blocks[ $index ]['innerBlocks'], $slugs, $updated ] = self::process_blocks( $block['innerBlocks'], $slugs, $updated, $prefix ); } continue; } // Generate slug if empty. if ( empty( $block['attrs']['slug'] ) ) { $blocks[ $index ]['attrs']['slug'] = self::generate_unique_block_slug( $block, $slugs, $prefix ); $updated = true; } // Track the slug if block_id is set. if ( isset( $block['attrs']['block_id'] ) ) { $slugs[ $block['attrs']['block_id'] ] = $blocks[ $index ]['attrs']['slug']; } // Process inner blocks recursively. if ( ! empty( $block['innerBlocks'] ) && is_array( $block['innerBlocks'] ) ) { [ $blocks[ $index ]['innerBlocks'], $slugs, $updated ] = self::process_blocks( $block['innerBlocks'], $slugs, $updated, $blocks[ $index ]['attrs']['slug'] ); } } return [ $blocks, $slugs, $updated ]; } /** * Generates a unique slug based on the provided block and existing slugs. * * @param array $block The block data. * @param array $slugs The array of existing slugs. * @param string $prefix Optional prefix for nested blocks. * @return string The generated unique block slug. * @since 0.0.1 */ public static function generate_unique_block_slug( $block, $slugs, $prefix = '' ) { $slug = is_string( $block['blockName'] ?? '' ) ? str_replace( 'sd/', '', $block['blockName'] ) : ''; // Use label if available. if ( ! empty( $block['attrs']['label'] ) && is_string( $block['attrs']['label'] ) ) { $slug = sanitize_title( $block['attrs']['label'] ); } // Add prefix for nested blocks. if ( ! empty( $prefix ) ) { $slug = $prefix . '-' . $slug; } return self::generate_unique_slug( $slug, $slugs ); } /** * Ensures that the slug is unique. * * If the slug is already taken, it appends a number to make it unique. * * @param string $slug The slug to make unique. * @param array $slugs Array of existing slugs. * @return string The unique slug. * @since 0.0.1 */ public static function generate_unique_slug( $slug, $slugs ) { $slug = sanitize_title( $slug ); // Check if slug exists in the array values. if ( ! in_array( $slug, $slugs, true ) ) { return $slug; } // Append a number to make it unique. $index = 1; while ( in_array( $slug . '-' . $index, $slugs, true ) ) { ++$index; } return $slug . '-' . $index; } /** * Get client IP address for logging purposes. * * Checks forwarded headers first (for proxied/load-balanced environments) * then falls back to REMOTE_ADDR. This is suitable for informational * logging only — do NOT use for security-critical IP validation. * * @return string Client IP address. * @since 0.0.1 */ public static function get_client_ip() { $ip_headers = [ 'HTTP_CLIENT_IP', 'HTTP_X_FORWARDED_FOR', 'REMOTE_ADDR', ]; foreach ( $ip_headers as $header ) { if ( ! empty( $_SERVER[ $header ] ) ) { $ips = explode( ',', sanitize_text_field( wp_unslash( $_SERVER[ $header ] ) ) ); $ip = trim( $ips[0] ); if ( filter_var( $ip, FILTER_VALIDATE_IP ) ) { return $ip; } } } return ''; } /** * Get allowed HTML tags for form markup. * * The wp_kses_post() doesn't allow form elements, so we need a custom allowed tags array. * This is safe because the markup is generated internally by trusted code that already * escapes user input with esc_attr(), esc_html(), etc. * * @return array> Allowed HTML tags and attributes. * @since 0.0.1 */ public static function get_allowed_form_html() { // Note: data-* wildcard doesn't work in wp_kses, so we list each data attribute explicitly. $common_data_attrs = [ 'data-block-id' => true, 'data-form-id' => true, 'data-gateway' => true, 'data-stripe-key' => true, 'data-currency' => true, 'data-payment-mode' => true, 'data-amount-type' => true, 'data-fixed-amount' => true, 'data-payment-type' => true, 'data-customer-name-field' => true, 'data-customer-email-field' => true, 'data-nonce' => true, 'data-variable-amount-field' => true, 'data-minimum-amount' => true, 'data-subscription-plan-name' => true, 'data-subscription-interval' => true, 'data-subscription-billing-cycles' => true, 'data-currency-symbol' => true, 'data-message-format' => true, 'data-slug' => true, 'data-required' => true, 'data-fee-percentage' => true, 'data-fee-fixed' => true, 'data-invalid-email-msg' => true, ]; return [ 'div' => array_merge( [ 'id' => true, 'class' => true, 'style' => true, 'role' => true, 'aria-live' => true, 'aria-atomic' => true, 'aria-labelledby' => true, ], $common_data_attrs ), 'form' => array_merge( [ 'id' => true, 'class' => true, 'method' => true, 'action' => true, ], $common_data_attrs ), 'fieldset' => [ 'id' => true, 'class' => true, ], 'legend' => [ 'id' => true, 'class' => true, ], 'label' => [ 'id' => true, 'class' => true, 'for' => true, ], 'input' => array_merge( [ 'id' => true, 'class' => true, 'type' => true, 'name' => true, 'value' => true, 'placeholder' => true, 'min' => true, 'max' => true, 'step' => true, 'maxlength' => true, 'checked' => true, 'disabled' => true, 'readonly' => true, 'required' => true, 'aria-describedby' => true, 'aria-required' => true, 'aria-hidden' => true, ], $common_data_attrs ), 'button' => array_merge( [ 'id' => true, 'class' => true, 'type' => true, 'disabled' => true, ], $common_data_attrs ), 'select' => array_merge( [ 'id' => true, 'class' => true, 'name' => true, 'disabled' => true, 'required' => true, 'aria-describedby' => true, 'aria-required' => true, ], $common_data_attrs ), 'option' => [ 'value' => true, 'selected' => true, 'disabled' => true, ], 'textarea' => array_merge( [ 'id' => true, 'class' => true, 'name' => true, 'rows' => true, 'cols' => true, 'placeholder' => true, 'maxlength' => true, 'disabled' => true, 'readonly' => true, 'required' => true, 'aria-describedby' => true, 'aria-required' => true, ], $common_data_attrs ), 'span' => [ 'id' => true, 'class' => true, 'style' => true, 'aria-hidden' => true, ], 'p' => [ 'id' => true, 'class' => true, 'style' => true, 'role' => true, ], 'a' => [ 'id' => true, 'class' => true, 'href' => true, 'target' => true, 'rel' => true, 'style' => true, ], 'strong' => [ 'class' => true, ], 'em' => [ 'class' => true, ], 'br' => [], 'svg' => [ 'class' => true, 'width' => true, 'height' => true, 'viewbox' => true, 'fill' => true, 'xmlns' => true, 'aria-hidden' => true, ], 'circle' => [ 'cx' => true, 'cy' => true, 'r' => true, 'stroke' => true, 'stroke-width' => true, 'fill' => true, ], 'rect' => [ 'x' => true, 'y' => true, 'width' => true, 'height' => true, 'rx' => true, 'stroke' => true, 'stroke-width' => true, ], 'path' => [ 'class' => true, 'd' => true, 'stroke' => true, 'stroke-width' => true, 'stroke-linecap' => true, 'stroke-linejoin' => true, 'fill' => true, ], ]; } }