Last active
August 8, 2025 15:56
-
-
Save stuartduff/c6a4abc3dd52402e86d5efdb122707f0 to your computer and use it in GitHub Desktop.
WooCommerce Box Office - Link Guest Tickets to Registered Users
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
/** | |
* Plugin Name: WooCommerce Box Office - Link Guest Tickets to Registered Users | |
* Plugin URI: https://gist.github.com/stuartduff/c6a4abc3dd52402e86d5efdb122707f0/edit | |
* Description: Links tickets purchased as a guest to the customer's account when they register using the same email address. | |
* Version: 1.0.0 | |
* Author: Stuart Duff | |
* Author URI: https:/stuartduff.com | |
* License: GPL v2 or later | |
* License URI: https://www.gnu.org/licenses/gpl-2.0.html | |
* Text Domain: wcbo-link-tickets | |
* Domain Path: /languages | |
* Requires at least: 5.0 | |
* Tested up to: 6.4 | |
* Requires PHP: 7.4 | |
* WC requires at least: 5.0 | |
* WC tested up to: 8.0 | |
* | |
* @package WCBO_Link_Tickets | |
* @version 1.0.0 | |
* @since 1.0.0 | |
*/ | |
// Prevent direct access | |
if ( ! defined( 'ABSPATH' ) ) { | |
exit; // Exit if accessed directly | |
} | |
// Define plugin constants | |
if ( ! defined( 'WCBO_LINK_TICKETS_VERSION' ) ) { | |
define( 'WCBO_LINK_TICKETS_VERSION', '1.0.0' ); | |
} | |
if ( ! defined( 'WCBO_LINK_TICKETS_PLUGIN_FILE' ) ) { | |
define( 'WCBO_LINK_TICKETS_PLUGIN_FILE', __FILE__ ); | |
} | |
if ( ! defined( 'WCBO_LINK_TICKETS_PLUGIN_DIR' ) ) { | |
define( 'WCBO_LINK_TICKETS_PLUGIN_DIR', plugin_dir_path( __FILE__ ) ); | |
} | |
if ( ! defined( 'WCBO_LINK_TICKETS_PLUGIN_URL' ) ) { | |
define( 'WCBO_LINK_TICKETS_PLUGIN_URL', plugin_dir_url( __FILE__ ) ); | |
} | |
// Security: Check if WooCommerce is active | |
if ( ! function_exists( 'WC' ) ) { | |
add_action( 'admin_notices', 'wcbo_link_tickets_woocommerce_missing_notice' ); | |
return; | |
} | |
/** | |
* Display admin notice if WooCommerce is not active | |
*/ | |
function wcbo_link_tickets_woocommerce_missing_notice() { | |
?> | |
<div class="notice notice-error"> | |
<p><?php esc_html_e( 'WooCommerce Box Office - Link Guest Tickets requires WooCommerce to be installed and active.', 'wcbo-link-tickets' ); ?></p> | |
</div> | |
<?php | |
} | |
// Hook into user registration to link guest tickets | |
add_action( 'user_register', 'wcbo_link_guest_tickets_to_user', 10, 1 ); | |
/** | |
* Link guest tickets to newly registered user if email matches | |
* | |
* @param int $user_id The newly registered user ID | |
* @return void | |
*/ | |
function wcbo_link_guest_tickets_to_user( $user_id ) { | |
// Validate user ID | |
if ( ! is_numeric( $user_id ) || $user_id <= 0 ) { | |
return; | |
} | |
// Security: Prevent processing if user doesn't exist | |
$user = get_user_by( 'id', $user_id ); | |
if ( ! $user || ! $user->user_email ) { | |
return; | |
} | |
// Validate and sanitize email | |
$user_email = sanitize_email( $user->user_email ); | |
if ( ! is_email( $user_email ) ) { | |
return; | |
} | |
// Rate limiting: Check if we've already processed this user recently | |
$last_processed = get_user_meta( $user_id, '_wcbo_last_ticket_link_attempt', true ); | |
if ( $last_processed && ( time() - absint( $last_processed ) ) < 300 ) { // 5 minutes | |
return; | |
} | |
update_user_meta( $user_id, '_wcbo_last_ticket_link_attempt', time() ); | |
// Security: Limit the number of tickets that can be processed per user | |
$max_tickets_per_user = apply_filters( 'wcbo_max_tickets_per_user', 50 ); | |
// Find all tickets with customer_id = 0 (guest tickets) | |
$guest_tickets = get_posts( array( | |
'post_type' => 'event_ticket', | |
'post_status' => 'publish', | |
'posts_per_page' => $max_tickets_per_user, | |
'meta_query' => array( | |
array( | |
'key' => '_user', | |
'value' => '0', | |
'compare' => '=', | |
), | |
), | |
) ); | |
if ( empty( $guest_tickets ) ) { | |
return; | |
} | |
$linked_tickets = 0; | |
$processed_tickets = 0; | |
// Use database transaction for data integrity | |
global $wpdb; | |
$wpdb->query( 'START TRANSACTION' ); | |
try { | |
foreach ( $guest_tickets as $ticket ) { | |
$ticket_id = absint( $ticket->ID ); | |
// Validate ticket exists and is accessible | |
if ( ! get_post( $ticket_id ) || get_post_type( $ticket_id ) !== 'event_ticket' ) { | |
continue; | |
} | |
// Get the order associated with this ticket | |
$order_id = get_post_meta( $ticket_id, '_order', true ); | |
if ( ! $order_id || ! is_numeric( $order_id ) ) { | |
continue; | |
} | |
$order = wc_get_order( $order_id ); | |
if ( ! $order ) { | |
continue; | |
} | |
// Check if the order's billing email matches the user's email | |
$order_email = $order->get_billing_email(); | |
if ( $order_email && strtolower( sanitize_email( $order_email ) ) === strtolower( $user_email ) ) { | |
// Verify ticket is still unassigned before linking | |
$current_user = get_post_meta( $ticket_id, '_user', true ); | |
if ( $current_user && $current_user !== '0' ) { | |
continue; // Ticket already assigned | |
} | |
// Link the ticket to the user | |
$update_user = update_post_meta( $ticket_id, '_user', $user_id ); | |
$update_customer = update_post_meta( $ticket_id, '_customer_id', $user_id ); | |
if ( $update_user && $update_customer ) { | |
$linked_tickets++; | |
} | |
} | |
$processed_tickets++; | |
// Safety check to prevent infinite loops | |
if ( $processed_tickets >= $max_tickets_per_user ) { | |
break; | |
} | |
} | |
// Commit transaction | |
$wpdb->query( 'COMMIT' ); | |
// Log the linking process (with reduced sensitive info) | |
if ( $linked_tickets > 0 ) { | |
wcbo_log_ticket_linking( $linked_tickets, $user_id, 'registration' ); | |
// Fire action for other plugins to hook into | |
do_action( 'wcbo_tickets_linked', $linked_tickets, $user_id, 'registration' ); | |
} | |
} catch ( Exception $e ) { | |
// Rollback on error | |
$wpdb->query( 'ROLLBACK' ); | |
wcbo_log_error( 'Ticket linking failed for user ' . $user_id . ': ' . $e->getMessage() ); | |
} | |
} | |
/** | |
* Alternative approach: Link tickets when user logs in for the first time | |
* This can be used in addition to or instead of the user registration hook | |
*/ | |
add_action( 'wp_login', 'wcbo_link_guest_tickets_on_login', 10, 2 ); | |
/** | |
* Link guest tickets on user login | |
* | |
* @param string $user_login The user login name | |
* @param WP_User $user The user object | |
* @return void | |
*/ | |
function wcbo_link_guest_tickets_on_login( $user_login, $user ) { | |
// Validate user object | |
if ( ! is_object( $user ) || ! isset( $user->ID ) ) { | |
return; | |
} | |
// Only run this once per user (you might want to add a flag) | |
$has_linked_tickets = get_user_meta( $user->ID, '_wcbo_tickets_linked', true ); | |
if ( $has_linked_tickets ) { | |
return; | |
} | |
// Get the user's email | |
$user_email = $user->user_email; | |
if ( ! $user_email ) { | |
return; | |
} | |
// Validate and sanitize email | |
$user_email = sanitize_email( $user_email ); | |
if ( ! is_email( $user_email ) ) { | |
return; | |
} | |
// Security: Limit the number of tickets that can be processed per user | |
$max_tickets_per_user = apply_filters( 'wcbo_max_tickets_per_user', 50 ); | |
// Find all tickets with customer_id = 0 (guest tickets) | |
$guest_tickets = get_posts( array( | |
'post_type' => 'event_ticket', | |
'post_status' => 'publish', | |
'posts_per_page' => $max_tickets_per_user, | |
'meta_query' => array( | |
array( | |
'key' => '_user', | |
'value' => '0', | |
'compare' => '=', | |
), | |
), | |
) ); | |
if ( empty( $guest_tickets ) ) { | |
return; | |
} | |
$linked_tickets = 0; | |
$processed_tickets = 0; | |
// Use database transaction for data integrity | |
global $wpdb; | |
$wpdb->query( 'START TRANSACTION' ); | |
try { | |
foreach ( $guest_tickets as $ticket ) { | |
$ticket_id = absint( $ticket->ID ); | |
// Validate ticket exists and is accessible | |
if ( ! get_post( $ticket_id ) || get_post_type( $ticket_id ) !== 'event_ticket' ) { | |
continue; | |
} | |
// Get the order associated with this ticket | |
$order_id = get_post_meta( $ticket_id, '_order', true ); | |
if ( ! $order_id || ! is_numeric( $order_id ) ) { | |
continue; | |
} | |
$order = wc_get_order( $order_id ); | |
if ( ! $order ) { | |
continue; | |
} | |
// Check if the order's billing email matches the user's email | |
$order_email = $order->get_billing_email(); | |
if ( $order_email && strtolower( sanitize_email( $order_email ) ) === strtolower( $user_email ) ) { | |
// Verify ticket is still unassigned before linking | |
$current_user = get_post_meta( $ticket_id, '_user', true ); | |
if ( $current_user && $current_user !== '0' ) { | |
continue; // Ticket already assigned | |
} | |
// Link the ticket to the user | |
$update_user = update_post_meta( $ticket_id, '_user', $user->ID ); | |
$update_customer = update_post_meta( $ticket_id, '_customer_id', $user->ID ); | |
if ( $update_user && $update_customer ) { | |
$linked_tickets++; | |
} | |
} | |
$processed_tickets++; | |
// Safety check to prevent infinite loops | |
if ( $processed_tickets >= $max_tickets_per_user ) { | |
break; | |
} | |
} | |
// Commit transaction | |
$wpdb->query( 'COMMIT' ); | |
// Mark that we've linked tickets for this user | |
if ( $linked_tickets > 0 ) { | |
update_user_meta( $user->ID, '_wcbo_tickets_linked', 'yes' ); | |
wcbo_log_ticket_linking( $linked_tickets, $user->ID, 'login' ); | |
// Fire action for other plugins to hook into | |
do_action( 'wcbo_tickets_linked', $linked_tickets, $user->ID, 'login' ); | |
} | |
} catch ( Exception $e ) { | |
// Rollback on error | |
$wpdb->query( 'ROLLBACK' ); | |
wcbo_log_error( 'Ticket linking failed for user ' . $user->ID . ': ' . $e->getMessage() ); | |
} | |
} | |
/** | |
* Admin function to manually link tickets for existing users | |
* This can be run from the WordPress admin or via WP-CLI | |
* | |
* @return int|false Number of linked tickets or false on failure | |
*/ | |
function wcbo_link_all_guest_tickets() { | |
// Security check: Ensure user has proper capabilities | |
if ( ! current_user_can( 'manage_woocommerce' ) ) { | |
return false; | |
} | |
// Rate limiting for admin function | |
$last_admin_run = get_option( '_wcbo_last_admin_link_run', 0 ); | |
if ( ( time() - absint( $last_admin_run ) ) < 60 ) { // 1 minute | |
return false; | |
} | |
update_option( '_wcbo_last_admin_link_run', time() ); | |
// Security: Limit the number of users to process | |
$max_users_to_process = apply_filters( 'wcbo_max_users_to_process', 1000 ); | |
// Get all users | |
$users = get_users( array( | |
'fields' => array( 'ID', 'user_email' ), | |
'number' => $max_users_to_process, | |
) ); | |
$total_linked = 0; | |
$max_tickets_per_user = apply_filters( 'wcbo_max_tickets_per_user', 50 ); | |
// Use database transaction for data integrity | |
global $wpdb; | |
$wpdb->query( 'START TRANSACTION' ); | |
try { | |
foreach ( $users as $user ) { | |
if ( ! $user->user_email ) { | |
continue; | |
} | |
// Validate and sanitize email | |
$user_email = sanitize_email( $user->user_email ); | |
if ( ! is_email( $user_email ) ) { | |
continue; | |
} | |
// Find all tickets with customer_id = 0 (guest tickets) | |
$guest_tickets = get_posts( array( | |
'post_type' => 'event_ticket', | |
'post_status' => 'publish', | |
'posts_per_page' => $max_tickets_per_user, | |
'meta_query' => array( | |
array( | |
'key' => '_user', | |
'value' => '0', | |
'compare' => '=', | |
), | |
), | |
) ); | |
if ( empty( $guest_tickets ) ) { | |
continue; | |
} | |
$user_linked_tickets = 0; | |
foreach ( $guest_tickets as $ticket ) { | |
$ticket_id = absint( $ticket->ID ); | |
// Validate ticket exists and is accessible | |
if ( ! get_post( $ticket_id ) || get_post_type( $ticket_id ) !== 'event_ticket' ) { | |
continue; | |
} | |
// Get the order associated with this ticket | |
$order_id = get_post_meta( $ticket_id, '_order', true ); | |
if ( ! $order_id || ! is_numeric( $order_id ) ) { | |
continue; | |
} | |
$order = wc_get_order( $order_id ); | |
if ( ! $order ) { | |
continue; | |
} | |
// Check if the order's billing email matches the user's email | |
$order_email = $order->get_billing_email(); | |
if ( $order_email && strtolower( sanitize_email( $order_email ) ) === strtolower( $user_email ) ) { | |
// Verify ticket is still unassigned before linking | |
$current_user = get_post_meta( $ticket_id, '_user', true ); | |
if ( $current_user && $current_user !== '0' ) { | |
continue; // Ticket already assigned | |
} | |
// Link the ticket to the user | |
$update_user = update_post_meta( $ticket_id, '_user', $user->ID ); | |
$update_customer = update_post_meta( $ticket_id, '_customer_id', $user->ID ); | |
if ( $update_user && $update_customer ) { | |
$total_linked++; | |
$user_linked_tickets++; | |
} | |
} | |
// Safety check to prevent infinite loops | |
if ( $user_linked_tickets >= $max_tickets_per_user ) { | |
break; | |
} | |
} | |
} | |
// Commit transaction | |
$wpdb->query( 'COMMIT' ); | |
// Log the bulk linking process | |
if ( $total_linked > 0 ) { | |
wcbo_log_ticket_linking( $total_linked, 0, 'admin_bulk' ); | |
// Fire action for other plugins to hook into | |
do_action( 'wcbo_tickets_linked', $total_linked, 0, 'admin_bulk' ); | |
} | |
} catch ( Exception $e ) { | |
// Rollback on error | |
$wpdb->query( 'ROLLBACK' ); | |
wcbo_log_error( 'Bulk ticket linking failed: ' . $e->getMessage() ); | |
return false; | |
} | |
return $total_linked; | |
} | |
/** | |
* Secure logging function | |
* | |
* @param int $ticket_count Number of tickets linked | |
* @param int $user_id User ID | |
* @param string $context Context of the linking operation | |
* @return void | |
*/ | |
function wcbo_log_ticket_linking( $ticket_count, $user_id, $context ) { | |
$log_message = sprintf( | |
'WooCommerce Box Office: Linked %d guest tickets to user ID %d (%s)', | |
intval( $ticket_count ), | |
intval( $user_id ), | |
sanitize_text_field( $context ) | |
); | |
// Use WordPress logging if available, otherwise fall back to error_log | |
if ( function_exists( 'wc_get_logger' ) ) { | |
$logger = wc_get_logger(); | |
$logger->info( $log_message, array( 'source' => 'wcbo-ticket-linking' ) ); | |
} else { | |
error_log( $log_message ); | |
} | |
} | |
/** | |
* Error logging function | |
* | |
* @param string $message Error message | |
* @return void | |
*/ | |
function wcbo_log_error( $message ) { | |
$log_message = 'WooCommerce Box Office Error: ' . sanitize_text_field( $message ); | |
// Use WordPress logging if available, otherwise fall back to error_log | |
if ( function_exists( 'wc_get_logger' ) ) { | |
$logger = wc_get_logger(); | |
$logger->error( $log_message, array( 'source' => 'wcbo-ticket-linking' ) ); | |
} else { | |
error_log( $log_message ); | |
} | |
} | |
/** | |
* Add admin menu item to manually link tickets | |
*/ | |
add_action( 'admin_menu', 'wcbo_add_link_tickets_menu' ); | |
/** | |
* Add admin menu for ticket linking | |
* | |
* @return void | |
*/ | |
function wcbo_add_link_tickets_menu() { | |
add_submenu_page( | |
'edit.php?post_type=event_ticket', | |
__( 'Link Guest Tickets', 'wcbo-link-tickets' ), | |
__( 'Link Guest Tickets', 'wcbo-link-tickets' ), | |
'manage_woocommerce', | |
'wcbo-link-tickets', | |
'wcbo_link_tickets_page' | |
); | |
} | |
/** | |
* Admin page for linking tickets | |
* | |
* @return void | |
*/ | |
function wcbo_link_tickets_page() { | |
// Security check | |
if ( ! current_user_can( 'manage_woocommerce' ) ) { | |
wp_die( __( 'You do not have sufficient permissions to access this page.', 'wcbo-link-tickets' ) ); | |
} | |
// Additional security: Check for admin referer | |
if ( ! wp_doing_ajax() && ! wp_verify_admin_referer( 'wcbo_link_tickets', 'wcbo_link_nonce' ) ) { | |
wp_die( __( 'Security check failed.', 'wcbo-link-tickets' ) ); | |
} | |
if ( isset( $_POST['wcbo_link_tickets'] ) && wp_verify_nonce( $_POST['wcbo_link_nonce'], 'wcbo_link_tickets' ) ) { | |
// Sanitize and validate input | |
$action = sanitize_text_field( $_POST['wcbo_link_tickets'] ); | |
if ( $action === __( 'Link Guest Tickets', 'wcbo-link-tickets' ) ) { | |
$linked_count = wcbo_link_all_guest_tickets(); | |
if ( $linked_count !== false ) { | |
echo '<div class="notice notice-success"><p>' . sprintf( | |
esc_html__( 'Successfully linked %d guest tickets to registered users.', 'wcbo-link-tickets' ), | |
intval( $linked_count ) | |
) . '</p></div>'; | |
} else { | |
echo '<div class="notice notice-error"><p>' . esc_html__( 'Unable to link tickets. Please try again later.', 'wcbo-link-tickets' ) . '</p></div>'; | |
} | |
} | |
} | |
?> | |
<div class="wrap"> | |
<h1><?php esc_html_e( 'Link Guest Tickets to Registered Users', 'wcbo-link-tickets' ); ?></h1> | |
<p><?php esc_html_e( 'This tool will link tickets purchased as a guest to registered user accounts based on matching email addresses.', 'wcbo-link-tickets' ); ?></p> | |
<form method="post"> | |
<?php wp_nonce_field( 'wcbo_link_tickets', 'wcbo_link_nonce' ); ?> | |
<p class="submit"> | |
<input type="submit" name="wcbo_link_tickets" class="button-primary" value="<?php esc_attr_e( 'Link Guest Tickets', 'wcbo-link-tickets' ); ?>" /> | |
</p> | |
</form> | |
</div> | |
<?php | |
} | |
/** | |
* Cleanup function to remove temporary data | |
* | |
* @return void | |
*/ | |
function wcbo_cleanup_temp_data() { | |
// Clean up old rate limiting data (older than 1 hour) | |
global $wpdb; | |
$wpdb->delete( | |
$wpdb->usermeta, | |
array( | |
'meta_key' => '_wcbo_last_ticket_link_attempt', | |
), | |
array( | |
'meta_value' => time() - 3600, | |
'meta_value_compare' => '<', | |
) | |
); | |
} | |
add_action( 'wp_scheduled_delete', 'wcbo_cleanup_temp_data' ); | |
/** | |
* Plugin activation hook | |
*/ | |
register_activation_hook( __FILE__, 'wcbo_link_tickets_activate' ); | |
/** | |
* Plugin activation function | |
* | |
* @return void | |
*/ | |
function wcbo_link_tickets_activate() { | |
// Add activation timestamp | |
add_option( 'wcbo_link_tickets_activated', time() ); | |
// Clear any existing rate limiting data | |
delete_option( '_wcbo_last_admin_link_run' ); | |
} | |
/** | |
* Plugin deactivation hook | |
*/ | |
register_deactivation_hook( __FILE__, 'wcbo_link_tickets_deactivate' ); | |
/** | |
* Plugin deactivation function | |
* | |
* @return void | |
*/ | |
function wcbo_link_tickets_deactivate() { | |
// Clean up temporary data | |
wcbo_cleanup_temp_data(); | |
// Remove activation timestamp | |
delete_option( 'wcbo_link_tickets_activated' ); | |
delete_option( '_wcbo_last_admin_link_run' ); | |
} | |
/** | |
* Load text domain for internationalization | |
*/ | |
add_action( 'plugins_loaded', 'wcbo_link_tickets_load_textdomain' ); | |
/** | |
* Load plugin text domain | |
* | |
* @return void | |
*/ | |
function wcbo_link_tickets_load_textdomain() { | |
load_plugin_textdomain( 'wcbo-link-tickets', false, dirname( plugin_basename( __FILE__ ) ) . '/languages' ); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment