prefix . 'ticketdrum_tickets'); define('TICKETDRUM_EVENTS_TABLE', $wpdb->prefix . 'ticketdrum_events'); define('TICKETDRUM_PURCHASES_TABLE', $wpdb->prefix . 'ticketdrum_purchases'); define('TICKETDRUM_PROMOCODES_TABLE', $wpdb->prefix . 'ticketdrum_promocodes'); define('TICKETDRUM_WAITLIST_TABLE', $wpdb->prefix . 'ticketdrum_waitlist'); define('TICKETDRUM_STYLE_VERSION', '2.6'); define('TICKETDRUM_DB_VERSION', '2.5'); global $ticketdrum_payment_data; $ticketdrum_payment_data = json_decode(get_option('ticketdrum_payment_data'),true); if (!$ticketdrum_payment_data || count($ticketdrum_payment_data)==0) $ticketdrum_payment_data = false; $ticketdrum_stripe_mode = get_option('ticketdrum_stripe_mode','test')=='test'?'test':'live'; require_once(plugin_dir_path(__FILE__) . 'admin/events.php'); require_once(plugin_dir_path(__FILE__) . 'admin/purchases.php'); require_once(plugin_dir_path(__FILE__) . 'admin/settings.php'); require_once(plugin_dir_path(__FILE__) . 'admin/tickets.php'); require_once(plugin_dir_path(__FILE__) . 'admin/admin_functions.php'); require_once(plugin_dir_path(__FILE__) . 'admin/block.php'); require_once(plugin_dir_path(__FILE__) . 'frontend/functions.php'); require_once(plugin_dir_path(__FILE__) . 'frontend/tickets.php'); require_once(plugin_dir_path(__FILE__) . 'admin/checkin.php'); require_once(plugin_dir_path(__FILE__) . 'admin/dashboard.php'); require_once(plugin_dir_path(__FILE__) . 'frontend/wallet.php'); require_once(plugin_dir_path(__FILE__) . 'admin/promocodes.php'); require_once(plugin_dir_path(__FILE__) . 'includes/ticketdrum-qr.php'); require_once(plugin_dir_path(__FILE__) . 'includes/stripe-php/init.php'); //Basket is read on plugins_loaded: ticketdrum_basket_read() needs wp_salt(), which //WordPress defines in pluggable.php AFTER plugin files are included global $ticketdrum_basket; $ticketdrum_basket = ['tickets'=>[],'promo'=>'','pending'=>0]; add_action('plugins_loaded',function(){ global $ticketdrum_basket; $ticketdrum_basket = ticketdrum_basket_read(); }); //Registration happens on init, deliberately earlier than the enqueue hooks. // //Block themes render post content BEFORE wp_head, so a shortcode or block that calls //wp_add_inline_script() runs before wp_enqueue_scripts has fired. Adding inline script to a //handle that is not registered yet fails silently, which took out the whole checkout on block //themes. Registering here means the handle always exists by the time any content renders, //while enqueueing still happens on the normal hooks below. function ticketdrum_register_assets() { wp_register_script( 'ticketdrum-admin-script', plugins_url( '/js/ticketdrum.js', __FILE__ ), array( 'jquery' ), TICKETDRUM_STYLE_VERSION, true ); wp_register_script( 'ticketdrum-showerror-script', plugins_url( '/js/ticketdrum.js', __FILE__ ), array( 'jquery' ), TICKETDRUM_STYLE_VERSION, true ); wp_register_style( 'ticketdrum-admin-style', plugins_url( '/css/ticketdrum.css', __FILE__ ), [], TICKETDRUM_STYLE_VERSION ); wp_register_style( 'ticketdrum-frontend-style', plugins_url( '/css/ticketdrum.css', __FILE__ ), [], TICKETDRUM_STYLE_VERSION ); //Printable buyer receipt. Registered but not enqueued here: it is only ever wanted on the //standalone ?ticketdrum_invoice= document, which prints it itself. wp_register_style( 'ticketdrum-receipt-style', plugins_url( '/css/ticketdrum-receipt.css', __FILE__ ), [], TICKETDRUM_STYLE_VERSION ); } add_action( 'init', 'ticketdrum_register_assets' ); //Strings used by js/ticketdrum.js, read there through ticketdrumT() with an English fallback. //Front-end pages get only the buyer-facing set; admin screens get both. function ticketdrum_js_strings($admin=false) { $strings = [ 'field_required' => __('This field is required','ticketdrum'), /* translators: %s: minimum number of characters */ 'min_length' => __('Please enter at least %s characters','ticketdrum'), 'password_mismatch' => __('Please enter the same password twice','ticketdrum'), 'invalid_email' => __('Please enter a valid email address','ticketdrum'), 'click_to_close' => __('Click anywhere to close this message','ticketdrum'), 'please_wait_title' => __('Please Wait...','ticketdrum'), 'communicating' => __('communicating with the server.','ticketdrum'), 'close' => __('close','ticketdrum'), 'loading' => __('Loading...','ticketdrum'), 'waitlist_joined' => __('You\'re on the list! We\'ll email you if tickets become available.','ticketdrum'), 'gateway_error' => __('Error initialising payment gateway','ticketdrum'), 'reload_to_retry' => __('Please reload the page to try again','ticketdrum'), 'please_wait' => __('Please wait...','ticketdrum'), 'submit_payment' => __('Submit Payment','ticketdrum'), 'submit' => __('Submit','ticketdrum'), 'details_failed' => __('Could not save your details, please try again','ticketdrum'), 'payment_failed' => __('The payment did not succeed','ticketdrum'), 'order_failed' => __('The order did not succeed','ticketdrum'), ]; if ($admin) $strings+= [ 'variable_name' => __('Variable name','ticketdrum'), 'required_q' => __('Required?','ticketdrum'), 'remove' => __('Remove','ticketdrum'), 'char_limit' => __('Character limit (blank or 0 for no limit):','ticketdrum'), 'select_options' => __('Enter the options for the dropdown list, one per line.','ticketdrum'), /* translators: %s: field type (text, textarea or select) */ 'new_field' => __('New %s field','ticketdrum'), /* translators: 1: buyer name, 2: ticket type, 3: event title */ 'checkin_ok' => __('Checked in - %1$s (%2$s, %3$s)','ticketdrum'), /* translators: 1: date and time of the first check-in, 2: buyer name, 3: ticket type */ 'checkin_dupe' => __('ALREADY CHECKED IN at %1$s - %2$s (%3$s)','ticketdrum'), 'pill_duplicate' => __('duplicate','ticketdrum'), 'pill_checked_in' => __('checked in','ticketdrum'), 'camera_unsupported' => __('Camera scanning isn\'t supported in this browser - type the code instead.','ticketdrum'), 'camera_denied' => __('Could not access the camera.','ticketdrum'), 'stop_camera' => __('Stop camera','ticketdrum'), 'scan_camera' => __('Scan with camera','ticketdrum'), 'view_stripe_settings' => __('Click here to view the updated Stripe settings','ticketdrum'), 'copied' => __('Copied','ticketdrum'), 'press_to_copy' => __('Press Ctrl/Cmd+C to copy','ticketdrum'), 'copy_to_clipboard' => __('Copy to clipboard','ticketdrum'), ]; return $strings; } function ticketdrum_enqueue_admin_scripts() { wp_enqueue_script( 'ticketdrum-admin-script' ); wp_localize_script( 'ticketdrum-admin-script', 'ticketdrum_i18n', ticketdrum_js_strings(true) ); //Drag-to-reorder on the custom checkout field editor. jquery-ui-core comes with it. wp_enqueue_script( 'jquery-ui-sortable' ); wp_enqueue_style( 'ticketdrum-admin-style' ); } add_action( 'admin_enqueue_scripts', 'ticketdrum_enqueue_admin_scripts' ); function ticketdrum_enqueue_frontend_scripts() { //No jQuery UI on the front end: nothing out here uses it, and loading it would cost every //visitor two extra requests for nothing. wp_enqueue_script( 'ticketdrum-showerror-script' ); wp_localize_script( 'ticketdrum-showerror-script', 'ticketdrum_vars', array( 'ajaxurl' => admin_url('admin-ajax.php'), 'nonce' => wp_create_nonce('ticketdrum_frontend'), 'currency_sign' => html_entity_decode(ticketdrum_currency_sign()), )); wp_localize_script( 'ticketdrum-showerror-script', 'ticketdrum_i18n', ticketdrum_js_strings() ); wp_enqueue_style( 'ticketdrum-frontend-style' ); } add_action( 'wp_enqueue_scripts', 'ticketdrum_enqueue_frontend_scripts'); function ticketdrum_display( $atts ){ global $wpdb,$post,$ticketdrum_alert; $viewpage = 'list'; if (isset($_GET['checkout'])) $viewpage = 'checkout'; elseif (is_array($atts) && in_array('details',$atts)) $viewpage = 'details'; elseif (is_array($atts) && in_array('tickets',$atts)) $viewpage = 'tickets'; $event_id = isset($atts['id'])?(int)$atts['id']:0; $ticket_order = isset($atts['order'])?array_map('intval', explode(',',$atts['order'])):false; ob_start(); if ($viewpage=='checkout') require_once(plugin_dir_path(__FILE__) . 'frontend/checkout.php'); elseif ($viewpage=='list') require_once(plugin_dir_path(__FILE__) . 'frontend/event-list.php'); elseif ($viewpage=='details') require_once(plugin_dir_path(__FILE__) . 'frontend/event-details.php'); elseif ($viewpage=='tickets') ticketdrum_display_tickets_for_event($event_id,$ticket_order); if ($event_id>0 && in_array($viewpage,['details','tickets'])) echo wp_kses(ticketdrum_event_jsonld($event_id),['script'=>['type'=>true]]); //SEO: Google event rich results elseif ($viewpage=='list') { //Structured data for every listed event $jsonld_events = $wpdb->get_results($wpdb->prepare("SELECT id FROM %i WHERE status='active' AND end_datetime>=now()",TICKETDRUM_EVENTS_TABLE),ARRAY_A); foreach ($jsonld_events as $jsonld_event) echo wp_kses(ticketdrum_event_jsonld((int)$jsonld_event['id']),['script'=>['type'=>true]]); } return ob_get_clean(); } add_shortcode( 'ticketdrum', 'ticketdrum_display' ); function ticketdrum_ajax() { global $wpdb,$ticketdrum_payment_data,$ticketdrum_stripe_mode; //Every action below is nonce-checked here, once, before any request data is read or used. //A missing or stale nonce ends the request on the spot, so nothing further in this function //can run. Every frontend form and script carries the same 'ticketdrum_frontend' nonce, so //there is one thing to verify, unconditionally, and no branch that can slip past it. $ticketdrum_nonce = isset($_POST['_wpnonce'])?sanitize_text_field(wp_unslash($_POST['_wpnonce'])):''; if (!wp_verify_nonce($ticketdrum_nonce,'ticketdrum_frontend')) die(json_encode(['result'=>false,'error'=>__('Session expired, please reload the page','ticketdrum')])); $action = sanitize_key(wp_unslash($_POST['action'] ?? '')); if ($action=='ticketdrum_events') { ticketdrum_display_tickets_for_event(absint(wp_unslash($_POST['event_id'] ?? 0))); die(); } elseif ($action=='ticketdrum_basket_form') { global $ticketdrum_basket; //Both halves of the posted basket are integers before anything is done with them: the //keys are ticket IDs and the values are quantities, so absint() is the whole of the //validation either can need. Anything that is not a positive ID is dropped. $basket_amend = (sanitize_key(wp_unslash($_POST['basket_action'] ?? ''))=='amend'); $posted_tickets = isset($_POST['tickets'])&&is_array($_POST['tickets'])?map_deep(wp_unslash($_POST['tickets']),'absint'):[]; $posted_quantities = []; foreach ($posted_tickets as $posted_id=>$posted_quantity) { $posted_id = absint($posted_id); if ($posted_id>0 && !is_array($posted_quantity)) $posted_quantities[$posted_id] = absint($posted_quantity); } foreach ($posted_quantities as $ticket_id=>$quantity) { if (!isset($ticketdrum_basket['tickets'][$ticket_id])) $ticketdrum_basket['tickets'][$ticket_id] = 0; if ($basket_amend) $ticketdrum_basket['tickets'][$ticket_id] = $quantity; else $ticketdrum_basket['tickets'][$ticket_id]+= $quantity; //Clamp to the ticket's max purchase quantity $ticket_details = $wpdb->get_row($wpdb->prepare("SELECT id,max_purchase_quantity from %i WHERE id=%d AND active=1",TICKETDRUM_TICKETS_TABLE,$ticket_id),ARRAY_A); if (!$ticket_details) $ticketdrum_basket['tickets'][$ticket_id] = 0; elseif ($ticketdrum_basket['tickets'][$ticket_id] > $ticket_details['max_purchase_quantity']) $ticketdrum_basket['tickets'][$ticket_id] = (int)$ticket_details['max_purchase_quantity']; } foreach ($ticketdrum_basket['tickets'] as $ticket_id=>$quantity) if ($quantity<=0) unset($ticketdrum_basket['tickets'][$ticket_id]); ticketdrum_basket_save($ticketdrum_basket); die(json_encode(['result'=>true,'basket'=>$ticketdrum_basket['tickets']])); } elseif ($action=='ticketdrum_join_waitlist') { $waitlist_email = sanitize_email(wp_unslash($_POST['email'] ?? '')); $waitlist_event = absint(wp_unslash($_POST['event_id'] ?? 0)); if (!is_email($waitlist_email) || $waitlist_event==0) die(json_encode(['result'=>false,'error'=>__('Please enter a valid email address','ticketdrum')])); $wpdb->query($wpdb->prepare("INSERT IGNORE INTO %i SET event_id=%d, name=%s, email=%s, added=%s", TICKETDRUM_WAITLIST_TABLE,$waitlist_event,sanitize_text_field(wp_unslash($_POST['name'] ?? '')),$waitlist_email,gmdate("Y-m-d H:i:s"))); die(json_encode(['result'=>true])); } elseif ($action=='ticketdrum_apply_promo') { global $ticketdrum_basket; if (sanitize_key(wp_unslash($_POST['promo_action'] ?? ''))=='remove') { $ticketdrum_basket['promo'] = ''; ticketdrum_basket_save($ticketdrum_basket); die(json_encode(['result'=>true])); } $basket_data = ticketdrum_calculate_basket_totals(); $promo_check = ticketdrum_promocode_validate(sanitize_text_field(wp_unslash($_POST['promo_code'] ?? '')),$basket_data['event_ids']); if (!$promo_check['result']) die(json_encode($promo_check)); $ticketdrum_basket['promo'] = $promo_check['promo']['code']; ticketdrum_basket_save($ticketdrum_basket); die(json_encode(['result'=>true])); } elseif ($action=='ticketdrum_payment_form') { if (($ticketdrum_payment_data[$ticketdrum_stripe_mode.'_stripe_access_token'] ?? '')=='') die(json_encode(['result'=>false,'error'=>__('Stripe not connected','ticketdrum')])); $basket_data = ticketdrum_calculate_basket_totals(); $fee_data = ticketdrum_calculate_fees($basket_data); //Ensure availability before taking payment foreach ($basket_data['tickets'] as $ticket) { if ($ticket['quantity']==0) continue; $availability = ticketdrum_check_ticket_availability($ticket['id'],$ticket['quantity']); if (!$availability['result']) die(json_encode(['result'=>false,'error'=>($availability['remaining']>0 /* translators: 1: ticket type name, 2: number of tickets left */ ?sprintf(_n('Sorry, "%1$s" no longer has enough tickets available (only %2$s left)','Sorry, "%1$s" no longer has enough tickets available (only %2$s left)',(int)$availability['remaining'],'ticketdrum'),$ticket['title'],number_format_i18n((int)$availability['remaining'])) /* translators: %s: ticket type name */ :sprintf(__('Sorry, "%s" no longer has enough tickets available','ticketdrum'),$ticket['title']))])); } //Create/reuse a pending purchase so a paid-but-unconfirmed order can always be reconciled $purchase_fields = array( 'ticket_data' => (string)json_encode($basket_data), 'event_ids' => ','.implode(',',$basket_data['event_ids']).',', 'ticket_ids' => ','.implode(',',$basket_data['ticket_ids']).',', 'subtotal' => (float)$basket_data['subtotal'], 'tax' => (float)$basket_data['tax'], 'fee' => (float)$fee_data['fee'], 'total' => (float)$fee_data['charge_total'], 'status' => 'pending', 'payment_data' => json_encode(['mode'=>$ticketdrum_stripe_mode,'fee_paid_by'=>$fee_data['fee_paid_by'],'application_fee_cents'=>$fee_data['application_fee_cents']]), ); global $ticketdrum_basket; $purchase_id = 0; if (($ticketdrum_basket['pending'] ?? 0)>0) $purchase_id = (int)$wpdb->get_var($wpdb->prepare("SELECT id FROM %i WHERE id=%d AND status='pending'",TICKETDRUM_PURCHASES_TABLE,$ticketdrum_basket['pending'])); if ($purchase_id>0) $wpdb->update(TICKETDRUM_PURCHASES_TABLE,$purchase_fields,['id'=>$purchase_id]); else { $purchase_fields['salt'] = (string)ticketdrum_generate_password(8,true); $purchase_fields['access_token'] = (string)ticketdrum_generate_password(32,true); $purchase_fields['custom_fields'] = ''; $purchase_fields['attendees'] = ''; $purchase_fields['added'] = gmdate("Y-m-d H:i:s"); $purchase_fields['ipaddress'] = sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'] ?? '')); $wpdb->insert(TICKETDRUM_PURCHASES_TABLE,$purchase_fields); $purchase_id = $wpdb->insert_id; } if ($purchase_id==0) die(json_encode(['result'=>false,'error'=>__('Could not create order','ticketdrum')])); $ticketdrum_basket['pending'] = $purchase_id; ticketdrum_basket_save($ticketdrum_basket); $TicketDrumStripe = new \TicketDrumStripe\TicketDrumStripeClient($ticketdrum_payment_data[$ticketdrum_stripe_mode.'_stripe_access_token']); try { $payment_intent = $TicketDrumStripe->paymentIntents->create([ 'application_fee_amount' => $fee_data['application_fee_cents'], 'payment_method_types' => ['card'], 'amount' => (int)round($fee_data['charge_total']*100), 'currency' => strtolower(get_option('ticketdrum_currency','usd')), 'description' => $basket_data['tickets_description'], 'metadata' => [ 'ticketdrum_purchase_id' => $purchase_id ], ],['stripe_account' => $ticketdrum_payment_data[$ticketdrum_stripe_mode.'_stripe_user_id']]); } catch (\TicketDrumStripe\Exception\ApiErrorException $e) { die(json_encode(['result'=>false,'error'=>__('Failed to create payment intent','ticketdrum')])); } catch (Exception $e) { die(json_encode(['result'=>false,'error'=>__('Unknown error','ticketdrum')])); } $wpdb->update(TICKETDRUM_PURCHASES_TABLE,['payment_intent'=>$payment_intent->id],['id'=>$purchase_id]); die(json_encode(['result'=>true,'payment_intent_client_secret'=>$payment_intent->client_secret])); } elseif ($action=='ticketdrum_payment_details') { //Called just before the card is charged so the pending purchase holds the buyer's details //if the browser dies mid-payment, the reconciliation cron can still complete the order global $ticketdrum_basket; $purchase_id = (int)($ticketdrum_basket['pending'] ?? 0); if ($purchase_id==0 || !$wpdb->get_var($wpdb->prepare("SELECT id FROM %i WHERE id=%d AND status='pending'",TICKETDRUM_PURCHASES_TABLE,$purchase_id))) die(json_encode(['result'=>false,'error'=>__('No pending order found, please reload the page','ticketdrum')])); $wpdb->update(TICKETDRUM_PURCHASES_TABLE,[ 'name' => sanitize_text_field(wp_unslash($_POST['name'] ?? '')), 'email' => sanitize_text_field(wp_unslash($_POST['email'] ?? '')), 'custom_fields' => json_encode(map_deep(wp_unslash($_POST['custom_fields'] ?? []),'sanitize_text_field')), 'attendees' => json_encode(is_array($_POST['attendees'] ?? null)?array_filter(map_deep(wp_unslash($_POST['attendees']),'sanitize_text_field')):[]), ],['id'=>$purchase_id]); die(json_encode(['result'=>true])); } elseif ($action=='ticketdrum_payment_confirmed') { if (($ticketdrum_payment_data[$ticketdrum_stripe_mode.'_stripe_access_token'] ?? '')=='') die(json_encode(['result'=>false,'error'=>__('Stripe not connected','ticketdrum')])); //The payment intent must belong to a pending purchase we created - never trust a bare PI id $stripe_pi = sanitize_text_field(wp_unslash($_POST['stripe_pi'] ?? '')); $purchase = $wpdb->get_row($wpdb->prepare("SELECT * FROM %i WHERE payment_intent=%s AND status='pending' LIMIT 1",TICKETDRUM_PURCHASES_TABLE,$stripe_pi),ARRAY_A); if (!$purchase) die(json_encode(['result'=>false,'error'=>__('Order not found','ticketdrum')])); $TicketDrumStripe = new \TicketDrumStripe\TicketDrumStripeClient($ticketdrum_payment_data[$ticketdrum_stripe_mode.'_stripe_access_token']); try { $payment_intent = $TicketDrumStripe->paymentIntents->retrieve($stripe_pi); } catch (\TicketDrumStripe\Exception\ApiErrorException $e) { die(json_encode(['result'=>false,'error'=>__('Failed to retrieve payment intent','ticketdrum')])); } catch (Exception $e) { die(json_encode(['result'=>false,'error'=>__('Unknown error','ticketdrum')])); } if ($payment_intent->status!='succeeded') die(json_encode(['result'=>false,'error'=>__('Payment unsuccessful','ticketdrum')])); if ((int)$payment_intent->amount != (int)round($purchase['total']*100)) die(json_encode(['result'=>false,'error'=>__('Payment amount mismatch','ticketdrum')])); $purchase_id = (int)$purchase['id']; $wpdb->update(TICKETDRUM_PURCHASES_TABLE,[ 'name' => sanitize_text_field(wp_unslash($_POST['name'] ?? '')), 'email' => sanitize_text_field(wp_unslash($_POST['email'] ?? '')), 'custom_fields' => json_encode(map_deep(wp_unslash($_POST['custom_fields'] ?? []),'sanitize_text_field')), 'attendees' => json_encode(is_array($_POST['attendees'] ?? null)?array_filter(map_deep(wp_unslash($_POST['attendees']),'sanitize_text_field')):[]), 'status' => 'paid', 'payment_data' => json_encode($payment_intent), ],['id'=>$purchase_id]); ticketdrum_basket_save(['tickets'=>[],'promo'=>'','pending'=>0]); ticketdrum_promocode_redeem($purchase_id); try { $create_customer_result = $TicketDrumStripe->customers->create([ 'email' => sanitize_text_field(wp_unslash($_POST['email'] ?? '')), 'name' => sanitize_text_field(wp_unslash($_POST['name'] ?? '')), ]); $TicketDrumStripe->paymentIntents->update($payment_intent->id,[ 'description' => get_bloginfo('name').' - Order #'.$purchase_id, 'customer' => $create_customer_result->id, 'metadata' => ['order_id' => $purchase_id], ]); } catch (\TicketDrumStripe\Exception\ApiErrorException $e) { #die(json_encode(['result'=>false,'error'=>'Failed to create customer'])); } catch (Exception $e) { #die(json_encode(['result'=>false,'error'=>__('Unknown error','ticketdrum')])); } ticketdrum_send_purchase_emails($purchase_id); ticketdrum_basket_save(['tickets'=>[],'promo'=>'','pending'=>0]); die(json_encode(['result'=>true])); } elseif ($action=='ticketdrum_payment_zero') { $basket_data = ticketdrum_calculate_basket_totals(); if ($basket_data['total']>0) die(json_encode(['result'=>false,'error'=>__('Not a zero basket total','ticketdrum')])); $data_array = array( 'salt' => (string)ticketdrum_generate_password(8,true), 'access_token' => (string)ticketdrum_generate_password(32,true), 'ticket_data' => (string)json_encode($basket_data), 'event_ids' => ','.implode(',',$basket_data['event_ids']).',', 'ticket_ids' => ','.implode(',',$basket_data['ticket_ids']).',', 'name' => sanitize_text_field(wp_unslash($_POST['name'] ?? '')), 'email' => sanitize_text_field(wp_unslash($_POST['email'] ?? '')), 'custom_fields' => json_encode(map_deep(wp_unslash($_POST['custom_fields'] ?? []),'sanitize_text_field')), 'attendees' => json_encode(is_array($_POST['attendees'] ?? null)?array_filter(map_deep(wp_unslash($_POST['attendees']),'sanitize_text_field')):[]), 'subtotal' => 0, 'tax' => 0, 'total' => 0, 'status' => 'paid', 'payment_data' => json_encode([]), 'added' => gmdate("Y-m-d H:i:s"), 'ipaddress' => sanitize_text_field(wp_unslash($_SERVER['REMOTE_ADDR'] ?? '')), ); $saveresult = $wpdb->insert(TICKETDRUM_PURCHASES_TABLE,$data_array); $purchase_id = $wpdb->insert_id; //Create variables for the email $email_data_array = []; $ticketdrum_custom_formdata = get_option('ticketdrum_custom_formdata',''); if ($ticketdrum_custom_formdata!='' && json_decode($ticketdrum_custom_formdata,true)) { $custom_formdata = (array)json_decode($ticketdrum_custom_formdata,true); foreach ($custom_formdata as $cfd) { $cfd_clean = trim($cfd['clean']); if ($cfd_clean!='') $email_data_array[$cfd_clean] = sanitize_text_field(wp_unslash($_POST['custom_fields']['checkout'][htmlentities($cfd['title'],ENT_QUOTES)] ?? '')); } } ticketdrum_send_purchase_emails($purchase_id); ticketdrum_basket_save(['tickets'=>[],'promo'=>'','pending'=>0]); die(json_encode(['result'=>true])); } } add_action( 'wp_ajax_ticketdrum_events', 'ticketdrum_ajax' ); add_action( 'wp_ajax_nopriv_ticketdrum_events', 'ticketdrum_ajax' ); add_action( 'wp_ajax_ticketdrum_basket_form', 'ticketdrum_ajax' ); add_action( 'wp_ajax_nopriv_ticketdrum_basket_form', 'ticketdrum_ajax' ); add_action( 'wp_ajax_ticketdrum_join_waitlist', 'ticketdrum_ajax' ); add_action( 'wp_ajax_nopriv_ticketdrum_join_waitlist', 'ticketdrum_ajax' ); add_action( 'wp_ajax_ticketdrum_apply_promo', 'ticketdrum_ajax' ); add_action( 'wp_ajax_nopriv_ticketdrum_apply_promo', 'ticketdrum_ajax' ); add_action( 'wp_ajax_ticketdrum_payment_form', 'ticketdrum_ajax' ); add_action( 'wp_ajax_nopriv_ticketdrum_payment_form', 'ticketdrum_ajax' ); add_action( 'wp_ajax_ticketdrum_payment_details', 'ticketdrum_ajax' ); add_action( 'wp_ajax_nopriv_ticketdrum_payment_details', 'ticketdrum_ajax' ); add_action( 'wp_ajax_ticketdrum_payment_confirmed', 'ticketdrum_ajax' ); add_action( 'wp_ajax_nopriv_ticketdrum_payment_confirmed', 'ticketdrum_ajax' ); add_action( 'wp_ajax_ticketdrum_payment_zero', 'ticketdrum_ajax' ); add_action( 'wp_ajax_nopriv_ticketdrum_payment_zero', 'ticketdrum_ajax' ); //Two different identifiers hang off a purchase, deliberately: // // salt short, human-typable ticket code. It goes in the QR code and is printed under // it so door staff can key it in when a camera won't cooperate. Redeeming it // requires an authenticated admin (see ticketdrum_checkin_lookup), so it does // not have to resist guessing. // access_token 32-character capability token for the public, unauthenticated links emailed to // the buyer (ticket PDF, receipt, wallet pass). Anyone holding it can read that // order, so it has to be long enough that it cannot be found by guessing. // //Orders created before this column existed get a token allocated on first use. function ticketdrum_purchase_token($purchase) { global $wpdb; if (!is_array($purchase) || !isset($purchase['id'])) return ''; if (trim((string)($purchase['access_token'] ?? ''))!='') return $purchase['access_token']; $token = ticketdrum_generate_password(32,true); $wpdb->update(TICKETDRUM_PURCHASES_TABLE,['access_token'=>$token],['id'=>(int)$purchase['id']]); return $token; } //Scratch space for the files that have to exist on disk for a moment: the ticket PDF while //FPDF writes it, the QR images it embeds, the .ics attachment, the wallet manifest. Everything //here is deleted as soon as it has been emailed or streamed. // //It lives under wp_upload_dir() because that is the only location a plugin can rely on being //writable, and it is resolved at runtime rather than hard-coded. The contents are not meant to //be public, so the folder is closed off: an index.php so nothing can be listed, and a //.htaccess denying direct requests on Apache. Names carry a random component as well, so a //file cannot be guessed during the moment it exists on servers that ignore .htaccess. function ticketdrum_tmpdir() { $upload = wp_upload_dir(); $tmpdir = trailingslashit($upload['basedir']).'ticketdrum-tmp/'; if (!is_dir($tmpdir)) wp_mkdir_p($tmpdir); if (!file_exists($tmpdir.'index.php')) file_put_contents($tmpdir.'index.php','\nDeny from all\n\n"); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_file_put_contents -- writing a protective .htaccess into the plugin's own uploads folder return $tmpdir; } //Event start/end are entered with , which posts Y-m-d\TH:i. //The tables store Y-m-d H:i:s. These two convert between the pair without going anywhere near //a timezone: the value is a wall-clock time the organiser typed, and it is stored verbatim. //Turns what posts into the Y-m-d H:i:s the tables store. // //This is an allow-list, not a filter: the value has to match the exact shape below and be a //real point on the calendar and clock, or it is discarded. Anything else - a bad date like //2026-13-99, stray markup, an injection attempt - comes back as '' rather than reaching the //database in some partly-cleaned state. function ticketdrum_normalise_datetime($value) { $value = sanitize_text_field(trim((string)$value)); if ($value=='') return ''; if (!preg_match('/^(\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2}))?$/',$value,$parts)) return ''; list(,$year,$month,$day,$hour,$minute) = $parts; $second = $parts[6] ?? '00'; if (!checkdate((int)$month,(int)$day,(int)$year)) return ''; if ((int)$hour>23 || (int)$minute>59 || (int)$second>59) return ''; return sprintf('%04d-%02d-%02d %02d:%02d:%02d',$year,$month,$day,$hour,$minute,$second); } function ticketdrum_datetime_local_value($value) { $value = (string)$value; if ($value=='' || strpos($value,'0000-00-00')===0) return ''; return str_replace(' ','T',substr($value,0,16)); } //Useful functions function ticketdrum_generate_password($length=20,$simple=false) { if ($simple) $chars = 'abcdefghjkmnpqrstuvwxyz2346789'; else $chars = 'abcdefghjkmnpqrstuvwxyzABCDEFGHJKMNPQRTUVWXYZ2346789'; $count = mb_strlen($chars); for ($i = 0, $result = ''; $i < $length; $i++) { $index = wp_rand(0, $count - 1); $result .= mb_substr($chars, $index, 1); } return $result; } //Various setup/installation bits function ticketdrum_install() { global $wpdb; $charset_collate = $wpdb->get_charset_collate(); require_once( ABSPATH . 'wp-admin/includes/upgrade.php' ); $sql = "CREATE TABLE ".TICKETDRUM_EVENTS_TABLE . " ( `id` int(11) NOT NULL AUTO_INCREMENT, `salt` varchar(20) NOT NULL DEFAULT '', `start_datetime` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `end_datetime` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `title` text NOT NULL, `description` text NOT NULL, `location` varchar(255) NOT NULL DEFAULT '', `max_spaces_available` mediumint(9) NOT NULL DEFAULT 0, `register_hide` tinyint(1) NOT NULL DEFAULT 0, `register_override_url` varchar(255) DEFAULT NULL, `register_override_text` varchar(255) DEFAULT NULL, `status` enum('draft','active','archived') NOT NULL DEFAULT 'draft', `notification_emails` text NOT NULL, `custom_fields` TEXT NOT NULL, `added` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', PRIMARY KEY (`id`), UNIQUE KEY `salt` (`salt`) ) $charset_collate;"; dbDelta( $sql ); $sql = "CREATE TABLE ". TICKETDRUM_PURCHASES_TABLE . " ( `id` mediumint(9) NOT NULL AUTO_INCREMENT, `salt` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', `access_token` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', `ticket_data` text COLLATE utf8mb4_unicode_ci NOT NULL, `event_ids` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', `ticket_ids` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', `custom_fields` text COLLATE utf8mb4_unicode_ci NOT NULL, `attendees` text COLLATE utf8mb4_unicode_ci NOT NULL, `subtotal` float(7,2) NOT NULL DEFAULT 0, `tax` float(7,2) NOT NULL DEFAULT 0, `fee` float(7,2) NOT NULL DEFAULT 0, `total` float(7,2) NOT NULL DEFAULT 0, `status` enum('','pending','paid','refunded') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', `payment_intent` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', `refunded_amount` float(7,2) NOT NULL DEFAULT 0, `checkins` text COLLATE utf8mb4_unicode_ci NOT NULL, `payment_data` text COLLATE utf8mb4_unicode_ci NOT NULL, `added` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', `notified` tinyint(1) NOT NULL DEFAULT 0, `ipaddress` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', PRIMARY KEY (`id`), KEY `access_token` (`access_token`), KEY `event_ids` (`event_ids`), KEY `ticket_ids` (`ticket_ids`), KEY `email` (`email`) ) $charset_collate;"; dbDelta( $sql ); $sql = "CREATE TABLE ".TICKETDRUM_TICKETS_TABLE . " ( `id` mediumint(9) NOT NULL AUTO_INCREMENT, `event_id` int(11) NOT NULL DEFAULT 0, `salt` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', `title` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', `price` float(7,2) NOT NULL DEFAULT 0, `tax` FLOAT(4,2) NULL DEFAULT NULL, `max_available` mediumint(9) NOT NULL DEFAULT 0, `max_purchase_quantity` mediumint(9) NOT NULL DEFAULT 10, `attendee_count` mediumint(9) NOT NULL DEFAULT 0, `active` tinyint(1) NOT NULL DEFAULT 1, `custom_fields` TEXT NOT NULL, `added` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', PRIMARY KEY (`id`), KEY `active` (`active`) ) $charset_collate;"; dbDelta( $sql ); $sql = "CREATE TABLE ".TICKETDRUM_WAITLIST_TABLE . " ( `id` mediumint(9) NOT NULL AUTO_INCREMENT, `event_id` int(11) NOT NULL DEFAULT 0, `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', `notified` tinyint(1) NOT NULL DEFAULT 0, `added` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', PRIMARY KEY (`id`), KEY `event_id` (`event_id`), UNIQUE KEY `event_email` (`event_id`,`email`) ) $charset_collate;"; dbDelta( $sql ); $sql = "CREATE TABLE ".TICKETDRUM_PROMOCODES_TABLE . " ( `id` mediumint(9) NOT NULL AUTO_INCREMENT, `code` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '', `event_id` int(11) NOT NULL DEFAULT 0, `type` enum('percent','fixed') COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'percent', `amount` float(7,2) NOT NULL DEFAULT 0, `max_uses` mediumint(9) NOT NULL DEFAULT 0, `used_count` mediumint(9) NOT NULL DEFAULT 0, `expires` datetime DEFAULT NULL, `active` tinyint(1) NOT NULL DEFAULT 1, `added` datetime NOT NULL DEFAULT '0000-00-00 00:00:00', PRIMARY KEY (`id`), UNIQUE KEY `code` (`code`) ) $charset_collate;"; dbDelta( $sql ); if (get_option('ticketdrum_db_version')!==false) update_option('ticketdrum_db_version',TICKETDRUM_DB_VERSION); else add_option('ticketdrum_db_version',TICKETDRUM_DB_VERSION); } register_activation_hook( __FILE__, 'ticketdrum_install' ); if (!get_option('ticketdrum_db_version') || get_option('ticketdrum_db_version')!=TICKETDRUM_DB_VERSION) ticketdrum_install(); /*** Site connection: nothing is sent to ticketdrum.com until the admin clicks Get Started. *** The account ID identifies this site to the TicketDrum service. It is not a licence: no *** part of the plugin is disabled without it. It exists because Stripe Connect payments are *** processed through TicketDrum's platform account, so the service has to know which *** connected account a payment belongs to. Everything else (events, free tickets, QR ticket *** PDFs, door check-in, promo codes, waitlists, exports) runs entirely locally. Any future *** features authenticate against this same connection rather than a second registration. ***/ function ticketdrum_is_connected() { return trim((string)get_option('ticketdrum_account_id'))!=''; } //Send new installs to the Get Started screen (single-site activations only, once) function ticketdrum_activation_redirect_flag($network_wide=false) { if (!$network_wide) set_transient('ticketdrum_activation_redirect',1,60); } register_activation_hook( __FILE__, 'ticketdrum_activation_redirect_flag' ); add_action('admin_init',function(){ if (!get_transient('ticketdrum_activation_redirect')) return; delete_transient('ticketdrum_activation_redirect'); if (wp_doing_ajax() || is_network_admin() || !current_user_can('manage_options')) return; if (isset($_GET['activate-multi'])) return; //Bulk plugin activation - don't steal the screen wp_safe_redirect(admin_url('admin.php?page=ticketdrum_dashboard')); exit; }); //Get Started: the explicit action that registers this site with ticketdrum.com add_action('admin_init',function(){ global $ticketdrum_api; if (sanitize_key(wp_unslash($_POST['action'] ?? ''))!='ticketdrum_connect') return; if (!current_user_can('manage_options')) return; check_admin_referer('ticketdrum_connect'); if (!ticketdrum_is_connected()) { $register_result = $ticketdrum_api->register(); $account_id = $register_result['account_id'] ?? ($register_result['licence_key'] ?? ''); if ($register_result && !empty($register_result['result']) && $account_id!='') { if (get_option('ticketdrum_account_id')!==false) update_option('ticketdrum_account_id',$account_id); else add_option('ticketdrum_account_id',$account_id); } else { add_action('admin_notices',function(){ echo '

'.esc_html__('Could not reach ticketdrum.com to register your site - please check your connection and try again.','ticketdrum').'

'; }); } } }); //Reconciliation: catch payments that succeeded at Stripe but where the buyer's browser never //confirmed the order (closed tab, network drop), and release expired pending ticket holds function ticketdrum_cron_schedules($schedules) { $schedules['ticketdrum_15min'] = ['interval'=>900,'display'=>'Every 15 minutes (TicketDrum)']; return $schedules; } add_filter('cron_schedules','ticketdrum_cron_schedules'); if (!wp_next_scheduled('ticketdrum_reconcile_pending')) wp_schedule_event(time(),'ticketdrum_15min','ticketdrum_reconcile_pending'); register_deactivation_hook( __FILE__, function(){ wp_clear_scheduled_hook('ticketdrum_reconcile_pending'); } ); function ticketdrum_reconcile_pending_purchases() { global $wpdb,$ticketdrum_payment_data; if (!$ticketdrum_payment_data) return; $pending = $wpdb->get_results($wpdb->prepare("SELECT * FROM %i WHERE status='pending' AND added<%s",TICKETDRUM_PURCHASES_TABLE,gmdate("Y-m-d H:i:s",time()-900)),ARRAY_A); foreach ($pending as $purchase) { $purchase_meta = json_decode($purchase['payment_data'],true); $expired = strtotime($purchase['added'])delete(TICKETDRUM_PURCHASES_TABLE,['id'=>(int)$purchase['id']]); continue; } $mode = (isset($purchase_meta['mode']) && $purchase_meta['mode']=='live')?'live':'test'; if (($ticketdrum_payment_data[$mode.'_stripe_access_token'] ?? '')=='') continue; $TicketDrumStripe = new \TicketDrumStripe\TicketDrumStripeClient($ticketdrum_payment_data[$mode.'_stripe_access_token']); try { $payment_intent = $TicketDrumStripe->paymentIntents->retrieve($purchase['payment_intent']); } catch (Exception $e) { continue; } if ($payment_intent->status=='succeeded' && (int)$payment_intent->amount==(int)round($purchase['total']*100)) { $wpdb->update(TICKETDRUM_PURCHASES_TABLE,[ 'status' => 'paid', 'payment_data' => json_encode($payment_intent), ],['id'=>(int)$purchase['id']]); ticketdrum_promocode_redeem((int)$purchase['id']); ticketdrum_send_purchase_emails((int)$purchase['id']); } elseif ($expired && $payment_intent->status!='succeeded') { $wpdb->delete(TICKETDRUM_PURCHASES_TABLE,['id'=>(int)$purchase['id']]); //Release the ticket hold foreach (array_filter(explode(',',$purchase['event_ids'])) as $freed_event_id) ticketdrum_waitlist_notify((int)$freed_event_id); } } } add_action('ticketdrum_reconcile_pending','ticketdrum_reconcile_pending_purchases'); class ticketdrum_api { function register() { return $this->api_request('/register',['domain'=>preg_replace('#^https?://#','',untrailingslashit(home_url()))]); } function fetch_stripe_credentials() { return $this->api_request('/stripe/credentials'); } function disconnect_stripe($environment) { return $this->api_request('/stripe/disconnect',['environment'=>$environment]); } function api_request($request_uri,$data=[]){ //Nothing talks to the service until the site is connected (registration is the one exception) if ($request_uri!='/register' && !ticketdrum_is_connected()) return false; $headers = array(); $headers[] = 'Accept: application/json'; $headers[] = 'Content-Type: application/json'; $headers['X-TicketDrum-Account'] = get_option('ticketdrum_account_id'); $args = [ 'method' => 'POST', 'data_format' => 'body', 'body' => wp_json_encode($data), 'headers' => $headers, 'sslverify' => defined('TICKETDRUM_API_SSLVERIFY')?TICKETDRUM_API_SSLVERIFY:true, ]; $response = wp_remote_post( TICKETDRUM_API_URL.$request_uri, $args ); if (is_wp_error($response)) return false; return json_decode(wp_remote_retrieve_body($response),true); } } global $ticketdrum_api; $ticketdrum_api = new ticketdrum_api(); ?>