Skip to content
Merged
7 changes: 6 additions & 1 deletion assets/js/src/site/plugins/pum-url-tracking.js
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,13 @@
href = $link.attr( 'href' );

if ( self.isInternalUrl( href ) ) {
// Get the filterable param name (defaults to 'pid').
var pidParam =
window.pum_vars?.paramNames?.popup_id || 'pid';

// Internal URLs: Append PID parameter (tracked via server redirect).
var urlParams = { pid: pid };
var urlParams = {};
urlParams[ pidParam ] = pid;

// Allow extensions to add additional parameters.
if ( window.PUM && window.PUM.hooks ) {
Expand Down
2 changes: 1 addition & 1 deletion classes/Base/CallToAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -183,7 +183,7 @@ public function safe_redirect( string $redirect_url = '', string $fallback_url =
} else {
// Default fallback.
/** @var string[] $cta_args */
$cta_args = apply_filters( 'popup_maker/cta_valid_url_args', [ 'cta', 'pid' ] );
$cta_args = apply_filters( 'popup_maker/cta_valid_url_args', [ \PopupMaker\get_param_name( 'cta' ), \PopupMaker\get_param_name( 'popup_id' ) ] );
$url = remove_query_arg( $cta_args );
\PopupMaker\safe_redirect( $url );
}
Expand Down
2 changes: 1 addition & 1 deletion classes/CallToAction/Link.php
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public function action_handler( \PopupMaker\Models\CallToAction $call_to_action,
$url = $call_to_action->get_setting( 'url' );

if ( ! $url ) {
$cta_args = apply_filters( 'popup_maker/cta_valid_url_args', [ 'cta', 'pid' ] );
$cta_args = apply_filters( 'popup_maker/cta_valid_url_args', [ \PopupMaker\get_param_name( 'cta' ), \PopupMaker\get_param_name( 'popup_id' ) ] );
// Strip query args and use the current page.
$url = remove_query_arg( $cta_args );
}
Expand Down
10 changes: 4 additions & 6 deletions classes/Controllers/CallToActions.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,13 +35,11 @@ public function init() {
* Redirects when needed.
*/
public function template_redirect() {
$cta_args = apply_filters( 'popup_maker/cta_valid_url_args', [ 'cta', 'pid' ] );
$cta_args = apply_filters( 'popup_maker/cta_valid_url_args', [ \PopupMaker\get_param_name( 'cta' ), \PopupMaker\get_param_name( 'popup_id' ) ] );

/* phpcs:disable WordPress.Security.NonceVerification.Recommended */
$cta_uuid = ! empty( $_GET['cta'] ) ? sanitize_text_field( wp_unslash( $_GET['cta'] ) ) : '';
$popup_id = ! empty( $_GET['pid'] ) ? absint( $_GET['pid'] ) : null;
$notrack = (bool) ( ! empty( $_GET['notrack'] ) ? sanitize_text_field( wp_unslash( $_GET['notrack'] ) ) : false );
/* phpcs:enable WordPress.Security.NonceVerification.Recommended */
$cta_uuid = \PopupMaker\get_param_value( 'cta', '', 'string' );
$popup_id = \PopupMaker\get_param_value( 'popup_id', null, 'int' );
$notrack = \PopupMaker\get_param_value( 'notrack', false, 'bool' );

/**
* Filter the CTA identifier before lookup.
Expand Down
6 changes: 3 additions & 3 deletions classes/Shortcode/CallToAction.php
Original file line number Diff line number Diff line change
Expand Up @@ -195,9 +195,9 @@ public function handler( $atts, $content = null ) {
// Get the current popup id.
$popup_id = pum_get_popup_id();

$url = $cta->generate_url('', [
'pid' => $popup_id ? $popup_id : null,
]);
$url = $cta->generate_url( '', [
\PopupMaker\get_param_name( 'popup_id' ) => $popup_id ?: false,
] );
Comment thread
coderabbitai[bot] marked this conversation as resolved.

$wrapper_classes = [
'pum-cta-wrapper',
Expand Down
1 change: 1 addition & 0 deletions classes/Site/Assets.php
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,7 @@ public static function localize_scripts() {
'core_sub_forms_enabled' => ! PUM_Newsletters::$disabled,
'popups' => [],
'cookie_domain' => apply_filters( 'pum_cookie_domain', '' ),
'paramNames' => \PopupMaker\get_param_names(),
]
)
);
Expand Down
162 changes: 156 additions & 6 deletions includes/namespaced/utils.php
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,15 @@ function ( $hosts ) use ( $parsed_url ) {
*/
function progress_bar( $percentage, $args = [] ) {

$args = wp_parse_args( $args, [
'size' => null,
'title' => '',
'class' => '',
'show_percentage' => true,
] );
$args = wp_parse_args(
$args,
[
'size' => null,
'title' => '',
'class' => '',
'show_percentage' => true,
]
);

$classes = [
'pum-progress-bar',
Expand All @@ -187,3 +190,150 @@ function progress_bar( $percentage, $args = [] ) {

echo '</div>';
}

/**
* 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 ];
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* Get all filterable query parameter names.
*
* @since 1.22.0
*
* @return array<string,string> 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 );
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* 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 );
}
Comment thread
coderabbitai[bot] marked this conversation as resolved.

/**
* 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 ) {
Comment thread
danieliser marked this conversation as resolved.
// 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':
return is_array( $value ) ? array_map( 'sanitize_text_field', $value ) : [];

case 'string':
default:
return sanitize_text_field( $value );
}
}
Loading