diff --git a/CHANGELOG.md b/CHANGELOG.md index bd4fbb430..a874d0f59 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ **Improvements** - Popup title field is now editable in the Block Editor sidebar, matching the classic editor experience. +- URL tracking parameter names (`pid`, `cta`, `notrack`) are now filterable via `popup_maker/param_name/{key}`, allowing site admins to resolve conflicts with other plugins. Example: `add_filter( 'popup_maker/param_name/popup_id', function() { return 'pum_id'; } );` - Improved PID tracking reliability by firing template_redirect at priority 0, ensuring tracking occurs before other plugins that might redirect. - Enhanced all Popup list views with sortable Enabled column and bulk enable/disable actions for easier management of multiple popups. - Block library assets (CSS) loading unnecessarily on all front-end pages. WordPress now automatically loads these styles only when Popup Maker blocks are actually rendered. diff --git a/assets/js/src/site/plugins/pum-url-tracking.js b/assets/js/src/site/plugins/pum-url-tracking.js index f840630cb..037258b8f 100644 --- a/assets/js/src/site/plugins/pum-url-tracking.js +++ b/assets/js/src/site/plugins/pum-url-tracking.js @@ -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 ) { diff --git a/classes/Base/CallToAction.php b/classes/Base/CallToAction.php index f9cad3cf6..ad90e03d7 100644 --- a/classes/Base/CallToAction.php +++ b/classes/Base/CallToAction.php @@ -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 ); } diff --git a/classes/CallToAction/Link.php b/classes/CallToAction/Link.php index 8e2026df5..9b853efe7 100644 --- a/classes/CallToAction/Link.php +++ b/classes/CallToAction/Link.php @@ -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 ); } diff --git a/classes/Controllers/CallToActions.php b/classes/Controllers/CallToActions.php index 01e5a6fe4..ca580e67d 100644 --- a/classes/Controllers/CallToActions.php +++ b/classes/Controllers/CallToActions.php @@ -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. diff --git a/classes/Shortcode/CallToAction.php b/classes/Shortcode/CallToAction.php index 11d627406..64c28ec92 100644 --- a/classes/Shortcode/CallToAction.php +++ b/classes/Shortcode/CallToAction.php @@ -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, + ] ); $wrapper_classes = [ 'pum-cta-wrapper', diff --git a/classes/Site/Assets.php b/classes/Site/Assets.php index fa689744c..e0e55337f 100644 --- a/classes/Site/Assets.php +++ b/classes/Site/Assets.php @@ -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(), ] ) ); diff --git a/includes/namespaced/utils.php b/includes/namespaced/utils.php index f0fa0961e..02fe42ff7 100644 --- a/includes/namespaced/utils.php +++ b/includes/namespaced/utils.php @@ -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', @@ -187,3 +190,159 @@ function progress_bar( $percentage, $args = [] ) { 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 ); + } +}