Skip to content
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions includes/admin.php
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,165 @@ function pmproga4_show_setup_notice() {
}
add_action( 'admin_notices', 'pmproga4_show_setup_notice' );

/**
* Load the Google Analytics script on the admin pages.
*
* @since TBD
*/
function pmproga4_load_admin_script() {

// Only run this if PMPro is installed.
if ( ! defined( 'PMPRO_VERSION' ) ) {
return;
}

// Only show on PMPro admin pages.
if ( empty( $_REQUEST['page'] ) || strpos( $_REQUEST['page'], 'pmpro' ) === false ) {
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Direct access to $_REQUEST['page'] without sanitization poses a security risk. The value should be sanitized before use in strpos(). Consider using sanitize_text_field($_REQUEST['page']) to prevent potential XSS vulnerabilities.

Suggested change
if ( empty( $_REQUEST['page'] ) || strpos( $_REQUEST['page'], 'pmpro' ) === false ) {
$page = isset( $_REQUEST['page'] ) ? sanitize_text_field( wp_unslash( $_REQUEST['page'] ) ) : '';
if ( empty( $page ) || strpos( $page, 'pmpro' ) === false ) {

Copilot uses AI. Check for mistakes.
return;
}
Comment on lines +129 to +131
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent indentation: these lines use tabs while the surrounding code uses spaces. For consistency with WordPress coding standards and the rest of the file, use tabs for indentation.

Copilot uses AI. Check for mistakes.

extract( $pmproga4_settings = get_option( 'pmproga4_settings',
array(
'measurement_id' => '',
'track_levels' => array()
)
) );

Comment on lines +133 to +139
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The use of extract() is a discouraged practice in WordPress as it can lead to variable collision and make code harder to maintain. Consider accessing array values directly using $pmproga4_settings['measurement_id'] and $pmproga4_settings['track_levels'] instead.

Suggested change
extract( $pmproga4_settings = get_option( 'pmproga4_settings',
array(
'measurement_id' => '',
'track_levels' => array()
)
) );
$pmproga4_settings = get_option(
'pmproga4_settings',
array(
'measurement_id' => '',
'track_levels' => array(),
)
);
$measurement_id = isset( $pmproga4_settings['measurement_id'] ) ? $pmproga4_settings['measurement_id'] : '';
$track_levels = isset( $pmproga4_settings['track_levels'] ) ? $pmproga4_settings['track_levels'] : array();

Copilot uses AI. Check for mistakes.
/**
* Determines whether to halt tracking based on specific conditions.
*
* This filter provides an opportunity for developers to stop tracking for specific
* scenarios, such as based on user ID, post, custom roles, etc. If the filter returns
* `true`, tracking will be halted.
*
* @since 1.0
*
* @param bool $stop_tracking Default value is `false`. If set to `true` by any filter, tracking will be halted.
*
* @return void
*/
if ( apply_filters( 'pmproga4_dont_track', false ) ) {
return;
}

// No measurement ID found, let's bail.
if ( empty( $measurement_id ) ) {
return;
}

/**
* Filters the attributes applied to the Google Analytics script tag loaded in wp_admin.
*
* Allows developers to customize or add specific attributes to the Google Analytics script tag
* for enhanced control or additional features.
*
* @since 1.0
*
* @param string $script_atts Default value is an empty string. Contains attributes for the GA script tag.
*
* @return string Modified attributes for the GA admin script tag.
*/
$script_atts = apply_filters( 'pmproga4_admin_script_atts', '' );

?>
<!-- Paid Memberships Pro - Google Analytics -->
<script async src="https://www.googletagmanager.com/gtag/js?id=<?php echo esc_attr( $measurement_id ); ?>"></script>
<script <?php echo esc_attr($script_atts); ?>>
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing space before the opening brace on the script tag attribute. For consistency with WordPress coding standards, there should be a space before the PHP echo statement: <script >

Suggested change
<script <?php echo esc_attr($script_atts); ?>>
<script <?php echo esc_attr( $script_atts ); ?>>

Copilot uses AI. Check for mistakes.
window.dataLayer = window.dataLayer || [];
function gtag(){dataLayer.push(arguments);}
gtag('js', new Date());
gtag('config',
'<?php echo esc_attr( $measurement_id ); ?>',
{
'currency': '<?php echo get_option( "pmpro_currency" ); ?>',
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The currency value from get_option() is output directly into JavaScript without proper escaping. Use esc_js() to prevent potential XSS vulnerabilities: 'currency': ''

Suggested change
'currency': '<?php echo get_option( "pmpro_currency" ); ?>',
'currency': '<?php echo esc_js( get_option( "pmpro_currency" ) ); ?>',

Copilot uses AI. Check for mistakes.
'send_page_view': false,
}
);
</script>
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Inconsistent indentation: this line uses a tab while the surrounding code uses spaces. For consistency with WordPress coding standards and the rest of the file, use a tab for indentation.

Suggested change
</script>
</script>

Copilot uses AI. Check for mistakes.
<?php

}
add_action( 'admin_enqueue_scripts', 'pmproga4_load_admin_script' );

/**
* Load the pmproga4_refund_event function on the pmpro_updated_order hook if status is refunded.
*
* @since TBD
*/
function pmproga4_refund_event_on_order_status_refunded( $pmpro_invoice, $original_status ) {
// Prevent unnecessary data sent to GA4 by only running this if the order wasn't already in refund status.
if ( 'refunded' != $original_status ) {
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use Yoda condition style for consistency with WordPress coding standards. The comparison should be: if ( 'refunded' !== $original_status ). Also use strict comparison (!==) instead of loose comparison (!=) to avoid type coercion issues.

Suggested change
if ( 'refunded' != $original_status ) {
if ( 'refunded' !== $original_status ) {

Copilot uses AI. Check for mistakes.
pmproga4_refund_event( $pmpro_invoice );
}
}
add_action( 'pmpro_order_status_refunded', 'pmproga4_refund_event_on_order_status_refunded', 10, 2 );

/**
* Enqueue the Google Analytics script for a refunded order.
*
* @since TBD
*/
function pmproga4_load_admin_order_refunded_script( $pmpro_invoice ) {
pmproga4_refund_event( $pmpro_invoice );
}

/**
Comment on lines +210 to +218
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function pmproga4_load_admin_order_refunded_script is defined but never used or hooked into WordPress. This appears to be dead code that should either be removed or properly integrated with an action hook.

Suggested change
* Enqueue the Google Analytics script for a refunded order.
*
* @since TBD
*/
function pmproga4_load_admin_order_refunded_script( $pmpro_invoice ) {
pmproga4_refund_event( $pmpro_invoice );
}
/**

Copilot uses AI. Check for mistakes.
* Function for refund event.
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The function documentation is incomplete. It should include a proper description of what the function does, document the @param tag for $pmpro_invoice parameter, and specify the @return type (which appears to be void).

Suggested change
* Function for refund event.
* Build and enqueue the Google Analytics 4 refund event for a given order.
*
* @param object $pmpro_invoice Paid Memberships Pro invoice/order object used to populate refund data.
*
* @return void

Copilot uses AI. Check for mistakes.
*/
function pmproga4_refund_event( $pmpro_invoice ) {

// Set the ecommerce dataLayer script if the order ID matches the session variable.
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment mentions checking if the order ID matches a session variable, but no session variable is being checked in this code. The comment should be updated to accurately reflect what the code does: checking if the invoice object and its ID are not empty.

Suggested change
// Set the ecommerce dataLayer script if the order ID matches the session variable.
// Set the ecommerce dataLayer script if the invoice and its ID are not empty.

Copilot uses AI. Check for mistakes.
if ( ! empty( $pmpro_invoice ) && ! empty( $pmpro_invoice->id ) ) {
$pmpro_invoice->getMembershipLevel();

// Set the ecommerce dataLayer script.
$gtag_config_ecommerce_data = array();
$gtag_config_ecommerce_data['transaction_id'] = $pmpro_invoice->code;
$gtag_config_ecommerce_data['value'] = $pmpro_invoice->membership_level->initial_payment;

if ( ! empty( $pmpro_invoice->tax ) ) {
$gtag_config_ecommerce_data['tax'] = $pmpro_invoice->tax;
} else {
$gtag_config_ecommerce_data['tax'] = 0;
}

if ( $pmpro_invoice->getDiscountCode() ) {
$gtag_config_ecommerce_data['coupon'] = $pmpro_invoice->discount_code->code;
} else {
$gtag_config_ecommerce_data['coupon'] = '';
}

// Build an array of product data.
$gtag_config_ecommerce_products = array();
$gtag_config_ecommerce_products['item_id'] = $pmpro_invoice->membership_level->id;
$gtag_config_ecommerce_products['item_name'] = $pmpro_invoice->membership_level->name;
$gtag_config_ecommerce_products['affiliation'] = get_bloginfo( 'name' );
if ( $pmpro_invoice->getDiscountCode() ) {
$gtag_config_ecommerce_products['coupon'] = $pmpro_invoice->discount_code->code;
}
$gtag_config_ecommerce_products['index'] = 0;
$gtag_config_ecommerce_products['price'] = $pmpro_invoice->membership_level->initial_payment;
$gtag_config_ecommerce_products['quantity'] = 1;
?>
<script>
jQuery(document).ready(function() {
gtag( 'event', 'refund', {
transaction_id: '<?php echo $gtag_config_ecommerce_data['transaction_id']; ?>',
value: <?php echo $gtag_config_ecommerce_data['value']; ?>,
<?php if ( ! empty( $gtag_config_ecommerce_data['tax'] ) ) { ?>
tax: <?php echo $gtag_config_ecommerce_data['tax']; ?>,
Comment on lines +260 to +262
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Numeric values should be explicitly cast to ensure they are numeric types before output into JavaScript. Use floatval() for value and tax: value: and tax: . This prevents potential issues if the database values are unexpectedly non-numeric.

Suggested change
value: <?php echo $gtag_config_ecommerce_data['value']; ?>,
<?php if ( ! empty( $gtag_config_ecommerce_data['tax'] ) ) { ?>
tax: <?php echo $gtag_config_ecommerce_data['tax']; ?>,
value: <?php echo floatval( $gtag_config_ecommerce_data['value'] ); ?>,
<?php if ( ! empty( $gtag_config_ecommerce_data['tax'] ) ) { ?>
tax: <?php echo floatval( $gtag_config_ecommerce_data['tax'] ); ?>,

Copilot uses AI. Check for mistakes.
<?php } ?>
<?php if( ! empty( $gtag_config_ecommerce_data['coupon'] ) ) { ?>
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing space after 'if' keyword. WordPress coding standards require a space after control structure keywords: if ( ! empty( ... ) )

Suggested change
<?php if( ! empty( $gtag_config_ecommerce_data['coupon'] ) ) { ?>
<?php if ( ! empty( $gtag_config_ecommerce_data['coupon'] ) ) { ?>

Copilot uses AI. Check for mistakes.
coupon: '<?php echo $gtag_config_ecommerce_data['coupon']; ?>',
Comment on lines +259 to +265
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The transaction_id should be escaped with esc_js() for proper JavaScript string escaping, and the coupon value on line 265 should also use esc_js() instead of being output directly. While these values come from the database, they should still be properly escaped for JavaScript context.

Suggested change
transaction_id: '<?php echo $gtag_config_ecommerce_data['transaction_id']; ?>',
value: <?php echo $gtag_config_ecommerce_data['value']; ?>,
<?php if ( ! empty( $gtag_config_ecommerce_data['tax'] ) ) { ?>
tax: <?php echo $gtag_config_ecommerce_data['tax']; ?>,
<?php } ?>
<?php if( ! empty( $gtag_config_ecommerce_data['coupon'] ) ) { ?>
coupon: '<?php echo $gtag_config_ecommerce_data['coupon']; ?>',
transaction_id: '<?php echo esc_js( $gtag_config_ecommerce_data['transaction_id'] ); ?>',
value: <?php echo $gtag_config_ecommerce_data['value']; ?>,
<?php if ( ! empty( $gtag_config_ecommerce_data['tax'] ) ) { ?>
tax: <?php echo $gtag_config_ecommerce_data['tax']; ?>,
<?php } ?>
<?php if( ! empty( $gtag_config_ecommerce_data['coupon'] ) ) { ?>
coupon: '<?php echo esc_js( $gtag_config_ecommerce_data['coupon'] ); ?>',

Copilot uses AI. Check for mistakes.
<?php } ?>
items: [ <?php echo json_encode( $gtag_config_ecommerce_products ); ?> ]
});
});
</script>
<?php
Comment on lines +227 to +271
Copy link

Copilot AI Jan 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

After calling getMembershipLevel(), there's no verification that the membership_level property was successfully populated. Add a check to ensure $pmpro_invoice->membership_level is not empty before accessing its properties like initial_payment, id, and name to prevent potential PHP warnings or errors.

Suggested change
// Set the ecommerce dataLayer script.
$gtag_config_ecommerce_data = array();
$gtag_config_ecommerce_data['transaction_id'] = $pmpro_invoice->code;
$gtag_config_ecommerce_data['value'] = $pmpro_invoice->membership_level->initial_payment;
if ( ! empty( $pmpro_invoice->tax ) ) {
$gtag_config_ecommerce_data['tax'] = $pmpro_invoice->tax;
} else {
$gtag_config_ecommerce_data['tax'] = 0;
}
if ( $pmpro_invoice->getDiscountCode() ) {
$gtag_config_ecommerce_data['coupon'] = $pmpro_invoice->discount_code->code;
} else {
$gtag_config_ecommerce_data['coupon'] = '';
}
// Build an array of product data.
$gtag_config_ecommerce_products = array();
$gtag_config_ecommerce_products['item_id'] = $pmpro_invoice->membership_level->id;
$gtag_config_ecommerce_products['item_name'] = $pmpro_invoice->membership_level->name;
$gtag_config_ecommerce_products['affiliation'] = get_bloginfo( 'name' );
if ( $pmpro_invoice->getDiscountCode() ) {
$gtag_config_ecommerce_products['coupon'] = $pmpro_invoice->discount_code->code;
}
$gtag_config_ecommerce_products['index'] = 0;
$gtag_config_ecommerce_products['price'] = $pmpro_invoice->membership_level->initial_payment;
$gtag_config_ecommerce_products['quantity'] = 1;
?>
<script>
jQuery(document).ready(function() {
gtag( 'event', 'refund', {
transaction_id: '<?php echo $gtag_config_ecommerce_data['transaction_id']; ?>',
value: <?php echo $gtag_config_ecommerce_data['value']; ?>,
<?php if ( ! empty( $gtag_config_ecommerce_data['tax'] ) ) { ?>
tax: <?php echo $gtag_config_ecommerce_data['tax']; ?>,
<?php } ?>
<?php if( ! empty( $gtag_config_ecommerce_data['coupon'] ) ) { ?>
coupon: '<?php echo $gtag_config_ecommerce_data['coupon']; ?>',
<?php } ?>
items: [ <?php echo json_encode( $gtag_config_ecommerce_products ); ?> ]
});
});
</script>
<?php
if ( ! empty( $pmpro_invoice->membership_level ) ) {
// Set the ecommerce dataLayer script.
$gtag_config_ecommerce_data = array();
$gtag_config_ecommerce_data['transaction_id'] = $pmpro_invoice->code;
$gtag_config_ecommerce_data['value'] = $pmpro_invoice->membership_level->initial_payment;
if ( ! empty( $pmpro_invoice->tax ) ) {
$gtag_config_ecommerce_data['tax'] = $pmpro_invoice->tax;
} else {
$gtag_config_ecommerce_data['tax'] = 0;
}
if ( $pmpro_invoice->getDiscountCode() ) {
$gtag_config_ecommerce_data['coupon'] = $pmpro_invoice->discount_code->code;
} else {
$gtag_config_ecommerce_data['coupon'] = '';
}
// Build an array of product data.
$gtag_config_ecommerce_products = array();
$gtag_config_ecommerce_products['item_id'] = $pmpro_invoice->membership_level->id;
$gtag_config_ecommerce_products['item_name'] = $pmpro_invoice->membership_level->name;
$gtag_config_ecommerce_products['affiliation'] = get_bloginfo( 'name' );
if ( $pmpro_invoice->getDiscountCode() ) {
$gtag_config_ecommerce_products['coupon'] = $pmpro_invoice->discount_code->code;
}
$gtag_config_ecommerce_products['index'] = 0;
$gtag_config_ecommerce_products['price'] = $pmpro_invoice->membership_level->initial_payment;
$gtag_config_ecommerce_products['quantity'] = 1;
?>
<script>
jQuery(document).ready(function() {
gtag( 'event', 'refund', {
transaction_id: '<?php echo $gtag_config_ecommerce_data['transaction_id']; ?>',
value: <?php echo $gtag_config_ecommerce_data['value']; ?>,
<?php if ( ! empty( $gtag_config_ecommerce_data['tax'] ) ) { ?>
tax: <?php echo $gtag_config_ecommerce_data['tax']; ?>,
<?php } ?>
<?php if( ! empty( $gtag_config_ecommerce_data['coupon'] ) ) { ?>
coupon: '<?php echo $gtag_config_ecommerce_data['coupon']; ?>',
<?php } ?>
items: [ <?php echo json_encode( $gtag_config_ecommerce_products ); ?> ]
});
});
</script>
<?php
}

Copilot uses AI. Check for mistakes.
}
}

/**
* Add a link to the settings page to the plugin action links.
*/
Expand Down