$data Array to fetch from.
* @param string|null $key_case Case to use for key (snake_case|camelCase).
*
* @return mixed|null
*
* @since 1.21.0
*/
function fetch_key_from_array( $key, $data, $key_case = null ) {
// If key is .notation, then we need to traverse the array.
$dotted_keys = explode( '.', $key );
foreach ( $dotted_keys as $key ) {
if ( $key_case ) {
switch ( $key_case ) {
case 'snake_case':
// Check if key is camelCase & convert to snake_case.
$key = camel_case_to_snake_case( $key );
break;
case 'camelCase':
// Check if key is snake_case & convert to camelCase.
$key = snake_case_to_camel_case( $key );
break;
}
}
if ( ! isset( $data[ $key ] ) ) {
return null;
}
$data = $data[ $key ];
}
return $data ? $data : null;
}
/**
* Generate a short unique ID.
* This generates a unique ID that is URL-safe by combining timestamp and random elements.
*
* @param string $prefix Optional prefix for the UUID.
* @param int $random_length Length of random suffix (default 4).
* @return string
*/
function generate_uuid( $prefix = '', $random_length = 4 ) {
// Get microtime as base36 - this gives us a 6-7 character time component
$time = base_convert( str_replace( '.', '', microtime( true ) ), 10, 36 );
// Add random suffix
$chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
$random = '';
for ( $i = 0; $i < $random_length; $i++ ) {
$random .= $chars[ random_int( 0, strlen( $chars ) - 1 ) ];
}
return $prefix . $time . $random;
}
/**
* Safely redirect to URL, allowing external domains when appropriate.
*
* This function provides a wrapper around wp_safe_redirect() that allows
* external redirects with proper security controls via filters.
*
* @param string $url URL to redirect to.
* @param int $status HTTP status code (default 302).
*
* @return void
*/
function safe_redirect( $url, $status = 302 ) {
/**
* Filter to determine if external redirects should be allowed.
*
* @param bool $allow_external Whether to allow external redirects.
* @param string $url The URL being redirected to.
*/
$allow_external = apply_filters( 'popup_maker/allow_external_redirect', true, $url );
if ( $allow_external ) {
// Parse the URL to check if it's external
$parsed_url = wp_parse_url( $url );
$site_url = wp_parse_url( home_url() );
// If it's an external URL, temporarily add the host to allowed hosts
if ( isset( $parsed_url['host'] ) &&
isset( $site_url['host'] ) &&
$parsed_url['host'] !== $site_url['host']
) {
add_filter(
'allowed_redirect_hosts',
function ( $hosts ) use ( $parsed_url ) {
if ( ! in_array( $parsed_url['host'], $hosts, true ) ) {
$hosts[] = $parsed_url['host'];
}
return $hosts;
},
20
);
}
}
wp_safe_redirect( sanitize_url( $url ), $status );
exit;
}
/**
* Render a progress bar.
*
* @param float|int $percentage The percentage to display.
* @param array{size:string,title:string,class:string} $args The arguments for the progress bar.
* @return void
*/
function progress_bar( $percentage, $args = [] ) {
$args = wp_parse_args(
$args,
[
'size' => null,
'title' => '',
'class' => '',
'show_percentage' => true,
]
);
$classes = [
'pum-progress-bar',
];
if ( $args['size'] ) {
$classes[] = 'pum-progress-bar--' . esc_attr( $args['size'] );
}
if ( $args['class'] ) {
$classes[] = esc_attr( $args['class'] );
}
echo '
';
echo '
';
if ( $args['show_percentage'] ) {
echo '
' . esc_html( round( $percentage, 1 ) ) . '%';
}
echo '
';
}
/**
* Get a filterable query parameter name.
*
* Used for URL tracking parameters like 'pid' which can conflict
* with other plugins. Site admins can filter to change the param name.
*
* @since 1.22.0
*
* @param string $key Parameter key (e.g., 'popup_id').
*
* @return string The parameter name.
*/
function get_param_name( $key ) {
static $cache = [];
if ( ! isset( $cache[ $key ] ) ) {
$defaults = [ 'popup_id' => 'pid' ];
$cache[ $key ] = sanitize_key(
apply_filters(
"popup_maker/param_name/{$key}",
$defaults[ $key ] ?? $key
)
);
}
return $cache[ $key ];
}
/**
* Get all filterable query parameter names.
*
* @since 1.22.0
*
* @return array Parameter names keyed by their identifier.
*/
function get_param_names() {
return [
'popup_id' => get_param_name( 'popup_id' ),
'cta' => get_param_name( 'cta' ),
'notrack' => get_param_name( 'notrack' ),
];
}
/**
* Get a query parameter value with type safety and filtering support.
*
* Uses the filterable parameter name system via get_param_name().
* Returns fallback if parameter is not set OR is an empty string.
* Note: Allows "0" as a valid value (unlike empty() check).
*
* @since 1.22.0
*
* @param string $key Parameter key (e.g., 'popup_id', 'cta').
* @param mixed $fallback Fallback value if parameter not set or empty.
* @param string $type Type to cast value to: 'string', 'int', 'bool', 'key', 'email', 'url', 'array'.
* Defaults to 'string'.
*
* @return mixed The sanitized parameter value, or fallback if not set/empty.
*/
function get_param_value( $key, $fallback = null, $type = 'string' ) {
$param_name = get_param_name( $key );
// phpcs:ignore WordPress.Security.NonceVerification.Recommended -- Parameter reading, not state-changing operation.
if ( ! isset( $_GET[ $param_name ] ) || '' === $_GET[ $param_name ] ) {
return $fallback;
}
// phpcs:ignore WordPress.Security.NonceVerification.Recommended, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitization handled by sanitize_param_by_type().
$value = wp_unslash( $_GET[ $param_name ] );
return sanitize_param_by_type( $value, $type );
}
/**
* Get a POST parameter value with type safety.
*
* Separate function for POST to enforce deliberate intent.
* Note: POST parameters do not use the filterable name system.
*
* @since 1.22.0
*
* @param string $key Parameter key (e.g., 'action', 'nonce').
* @param mixed $fallback Fallback value if parameter not set or empty.
* @param string $type Type to cast value to: 'string', 'int', 'bool', 'key', 'email', 'url', 'array'.
* Defaults to 'string'.
*
* @return mixed The sanitized parameter value, or fallback if not set/empty.
*/
function get_post_param_value( $key, $fallback = null, $type = 'string' ) {
// phpcs:ignore WordPress.Security.NonceVerification.Missing -- Parameter reading, not state-changing operation.
if ( ! isset( $_POST[ $key ] ) || '' === $_POST[ $key ] ) {
return $fallback;
}
// phpcs:ignore WordPress.Security.NonceVerification.Missing, WordPress.Security.ValidatedSanitizedInput.InputNotSanitized -- Sanitization handled by sanitize_param_by_type().
$value = wp_unslash( $_POST[ $key ] );
return sanitize_param_by_type( $value, $type );
}
/**
* Sanitize a parameter value based on type specification.
*
* Reusable helper for type-safe sanitization of request data.
*
* @since 1.22.0
*
* @param mixed $value Raw parameter value.
* @param string $type Type to sanitize for: 'string', 'int', 'bool', 'key', 'email', 'url', 'array'.
*
* @return mixed Sanitized value.
*/
function sanitize_param_by_type( $value, $type ) {
// Handle array values passed when expecting scalar types.
if ( is_array( $value ) && 'array' !== $type ) {
if ( ! empty( $value ) ) {
$value = reset( $value );
} else {
return 'bool' === $type ? false : ( 'int' === $type ? 0 : '' );
}
}
switch ( $type ) {
case 'int':
return absint( $value );
case 'bool':
return filter_var( $value, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE ) ?? false;
case 'key':
return sanitize_key( $value );
case 'email':
return sanitize_email( $value );
case 'url':
return esc_url_raw( $value );
case 'array':
if ( ! is_array( $value ) ) {
return [];
}
// Safely handle nested arrays by only sanitizing scalar values.
return array_map(
function ( $v ) {
return is_scalar( $v ) ? sanitize_text_field( (string) $v ) : '';
},
$value
);
case 'string':
default:
return sanitize_text_field( $value );
}
}