$name ) {
$provider = call_user_func( array( $provider_class, 'get_instance' ) );
$event = $provider->get_event_for_ticket( $ticket_id );
if ( ! $event ) {
continue;
}
$ticket_object = $provider->get_ticket( $event->ID, $ticket_id );
if ( $ticket_object ) {
return $ticket_object;
}
}
return null;
}
/**
* Returns the event post corresponding to the possible ticket object/ticket ID.
*
* This is used to help differentiate between products which act as tickets for an
* event and those which do not. If $possible_ticket is not related to any events
* then boolean false will be returned.
*
* This stub method should be treated as if it were an abstract method - ie, the
* concrete class ought to provide the implementation.
*
* @todo convert to abstract method in 4.0
*
* @param $possible_ticket
*
* @return bool|WP_Post
*/
public function get_event_for_ticket( $possible_ticket ) {
return false;
}
/**
* Deletes a ticket
*
* @abstract
*
* @param $event_id
* @param $ticket_id
*
* @return mixed
*/
abstract public function delete_ticket( $event_id, $ticket_id );
/**
* Saves a ticket
*
* @abstract
*
* @param int $event_id
* @param int $ticket
* @param array $raw_data
*
* @return mixed
*/
abstract public function save_ticket( $event_id, $ticket, $raw_data = array() );
/**
* Get all the tickets for an event
*
* @abstract
*
* @param int $event_id
*
* @return array mixed
*/
abstract protected function get_tickets( $event_id );
/**
* Get all the attendees (sold tickets) for an event
* @abstract
*
* @param $event_id
*
* @return mixed
*/
abstract protected function get_attendees( $event_id );
/**
* Mark an attendee as checked in
*
* @abstract
*
* @param $attendee_id
* @param $qr true if from QR checkin process
*
* @return mixed
*/
abstract public function checkin( $attendee_id );
/**
* Mark an attendee as not checked in
*
* @abstract
*
* @param $attendee_id
*
* @return mixed
*/
abstract public function uncheckin( $attendee_id );
/**
* Renders the advanced fields in the new/edit ticket form.
* Using the method, providers can add as many fields as
* they want, specific to their implementation.
*
* @abstract
*
* @param $event_id
* @param $ticket_id
*
* @return mixed
*/
abstract public function do_metabox_advanced_options( $event_id, $ticket_id );
/**
* Renders the front end form for selling tickets in the event single page
*
* @abstract
*
* @param $content
*
* @return mixed
*/
abstract public function front_end_tickets_form( $content );
/**
* Returns the markup for the price field
* (it may contain the user selected currency, etc)
*
* @param object|int $product
*
* @return string
*/
public function get_price_html( $product ) {
return '';
}
/**
* Indicates if the module/ticket provider supports a concept of global stock.
*
* For backward compatibility reasons this method has not been declared abstract but
* implementaions are still expected to override it.
*
* @return bool
*/
public function supports_global_stock() {
return false;
}
/**
* Returns instance of the child class (singleton)
*
* @static
* @abstract
* @return mixed
*/
public static function get_instance() {}
// end API Definitions
/**
*
*/
public function __construct() {
// Start the singleton with the generic functionality to all providers.
Tribe__Tickets__Tickets_Handler::instance();
// As this is an abstract class, we want to know which child instantiated it
$this->className = get_class( $this );
$this->parentPath = trailingslashit( dirname( dirname( dirname( __FILE__ ) ) ) );
$this->parentUrl = trailingslashit( plugins_url( '', $this->parentPath ) );
// Register all Tribe__Tickets__Tickets api consumers
self::$active_modules[ $this->className ] = $this->pluginName;
add_filter( 'tribe_events_tickets_modules', array( $this, 'modules' ) );
add_action( 'tribe_events_tickets_metabox_advanced', array( $this, 'do_metabox_advanced_options' ), 10, 2 );
// Admin AJAX actions for each provider
add_action( 'wp_ajax_tribe-ticket-add-' . $this->className, array( $this, 'ajax_handler_ticket_add' ) );
add_action( 'wp_ajax_tribe-ticket-delete-' . $this->className, array( $this, 'ajax_handler_ticket_delete' ) );
add_action( 'wp_ajax_tribe-ticket-edit-' . $this->className, array( $this, 'ajax_handler_ticket_edit' ) );
add_action( 'wp_ajax_tribe-ticket-checkin-' . $this->className, array( $this, 'ajax_handler_attendee_checkin' ) );
add_action( 'wp_ajax_tribe-ticket-uncheckin-' . $this->className, array( $this, 'ajax_handler_attendee_uncheckin' ) );
// Front end
$ticket_form_hook = $this->get_ticket_form_hook();
if ( ! empty( $ticket_form_hook ) ) {
add_action( $ticket_form_hook, array( $this, 'front_end_tickets_form' ), 5 );
}
add_action( 'tribe_events_single_event_after_the_meta', array( $this, 'show_tickets_unavailable_message' ), 6 );
add_filter( 'the_content', array( $this, 'front_end_tickets_form_in_content' ), 11 );
add_filter( 'the_content', array( $this, 'show_tickets_unavailable_message_in_content' ), 12 );
// Ensure ticket prices and event costs are linked
add_filter( 'tribe_events_event_costs', array( $this, 'get_ticket_prices' ), 10, 2 );
}
public function has_permission( $post, $data, $nonce_action ) {
if ( ! $post instanceof WP_Post ) {
if ( ! is_numeric( $post ) ) {
return false;
}
$post = get_post( $post );
}
return ! empty( $data['nonce'] ) && wp_verify_nonce( $data['nonce'], $nonce_action ) && current_user_can( get_post_type_object( $post->post_type )->cap->edit_posts );
}
/* AJAX Handlers */
/**
* Sanitizes the data for the new/edit ticket ajax call,
* and calls the child save_ticket function.
*/
final public function ajax_handler_ticket_add() {
if ( ! isset( $_POST['formdata'] ) ) {
$this->ajax_error( 'Bad post' );
}
if ( ! isset( $_POST['post_ID'] ) )
$this->ajax_error( 'Bad post' );
/*
This is needed because a provider can implement a dynamic set of fields.
Each provider is responsible for sanitizing these values.
*/
$data = wp_parse_args( $_POST['formdata'] );
$post_id = $_POST['post_ID'];
if ( ! $this->has_permission( $post_id, $_POST, 'add_ticket_nonce' ) ) {
$this->ajax_error( "Cheatin' huh?" );
}
if ( ! isset( $data['ticket_provider'] ) || ! $this->module_is_valid( $data['ticket_provider'] ) ) {
$this->ajax_error( 'Bad module' );
}
$return = $this->ticket_add( $post_id, $data );
// Successful?
if ( $return ) {
// Let's create a tickets list markup to return
$tickets = $this->get_event_tickets( $post_id );
$return = Tribe__Tickets__Tickets_Handler::instance()->get_ticket_list_markup( $tickets );
$return = $this->notice( esc_html__( 'Your ticket has been saved.', 'event-tickets' ) ) . $return;
/**
* Fire action when a ticket has been added
*
* @param $post_id
*/
do_action( 'tribe_tickets_ticket_added', $post_id );
}
$return = array( 'html' => $return );
/**
* Filters the return data for ticket add
*
* @var array Array of data to return to the ajax call
*/
$return = apply_filters( 'event_tickets_ajax_ticket_add_data', $return, $post_id );
$this->ajax_ok( $return );
}
/**
* Creates a ticket object and calls the child save_ticket function
*
* @param int $post_id WP_Post ID the ticket is being attached to
* @param array $data Raw post data
*
* @return boolean
*/
final public function ticket_add( $post_id, $data ) {
$ticket = new Tribe__Tickets__Ticket_Object();
$ticket->ID = isset( $data['ticket_id'] ) ? absint( $data['ticket_id'] ) : null;
$ticket->name = isset( $data['ticket_name'] ) ? esc_html( $data['ticket_name'] ) : null;
$ticket->description = isset( $data['ticket_description'] ) ? esc_html( $data['ticket_description'] ) : null;
$ticket->price = ! empty( $data['ticket_price'] ) ? trim( $data['ticket_price'] ) : 0;
$ticket->purchase_limit = isset( $data['ticket_purchase_limit'] ) ? absint( $data['ticket_purchase_limit' ] ) : apply_filters( 'tribe_tickets_default_purchase_limit', 0, $ticket->ID );
if ( ! empty( $ticket->price ) ) {
// remove non-money characters
$ticket->price = preg_replace( '/[^0-9\.\,]/Uis', '', $ticket->price );
}
if ( ! empty( $data['ticket_start_date'] ) ) {
$meridian = ! empty( $data['ticket_start_meridian'] ) ? ' ' . $data['ticket_start_meridian'] : '';
$ticket->start_date = date( Tribe__Date_Utils::DBDATETIMEFORMAT, strtotime( $data['ticket_start_date'] . ' ' . $data['ticket_start_hour'] . ':' . $data['ticket_start_minute'] . ':00' . $meridian ) );
}
if ( ! empty( $data['ticket_end_date'] ) ) {
$meridian = ! empty( $data['ticket_end_meridian'] ) ? ' ' . $data['ticket_end_meridian'] : '';
$ticket->end_date = date( Tribe__Date_Utils::DBDATETIMEFORMAT, strtotime( $data['ticket_end_date'] . ' ' . $data['ticket_end_hour'] . ':' . $data['ticket_end_minute'] . ':00' . $meridian ) );
}
$ticket->provider_class = $this->className;
/**
* Fired once a ticket has been created and added to a post
*
* @var $post_id Post ID
* @var $ticket Ticket object
* @var $data Submitted post data
*/
do_action( 'tribe_tickets_ticket_add', $post_id, $ticket, $data );
// Pass the control to the child object
return $this->save_ticket( $post_id, $ticket, $data );
}
/**
* Handles the check-in ajax call, and calls the checkin method.
*
* @todo use of 'order_id' in this method is misleading (we're working with the attendee id)
* we should consider revising in a back-compat minded way
*/
final public function ajax_handler_attendee_checkin() {
if ( ! isset( $_POST['order_ID'] ) || intval( $_POST['order_ID'] ) == 0 ) {
$this->ajax_error( 'Bad post' );
}
if ( ! isset( $_POST['provider'] ) || ! $this->module_is_valid( $_POST['provider'] ) ) {
$this->ajax_error( 'Bad module' );
}
if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'checkin' ) || ! $this->user_can( 'edit_posts', $_POST['order_ID'] ) ) {
$this->ajax_error( "Cheatin' huh?" );
}
$order_id = $_POST['order_ID'];
// Pass the control to the child object
$did_checkin = $this->checkin( $order_id );
$this->maybe_update_attendees_cache( $did_checkin );
$this->ajax_ok( $did_checkin );
}
/**
* Handles the check-in ajax call, and calls the uncheckin method.
*
* @todo use of 'order_id' in this method is misleading (we're working with the attendee id)
* we should consider revising in a back-compat minded way
*/
final public function ajax_handler_attendee_uncheckin() {
if ( ! isset( $_POST['order_ID'] ) || intval( $_POST['order_ID'] ) == 0 ) {
$this->ajax_error( 'Bad post' );
}
if ( ! isset( $_POST['provider'] ) || ! $this->module_is_valid( $_POST['provider'] ) ) {
$this->ajax_error( 'Bad module' );
}
if ( empty( $_POST['nonce'] ) || ! wp_verify_nonce( $_POST['nonce'], 'uncheckin' ) || ! $this->user_can( 'edit_posts', $_POST['order_ID'] ) ) {
$this->ajax_error( "Cheatin' huh?" );
}
$order_id = $_POST['order_ID'];
// Pass the control to the child object
$did_uncheckin = $this->uncheckin( $order_id );
if ( class_exists( 'Tribe__Events__Main' ) ) {
$this->maybe_update_attendees_cache( $did_uncheckin );
}
$this->ajax_ok( $did_uncheckin );
}
/**
* Sanitizes the data for the delete ticket ajax call, and calls the child delete_ticket
* function.
*
* @todo use of 'order_id' in this method is misleading (we're working with the attendee id)
* we should consider revising in a back-compat minded way
*/
final public function ajax_handler_ticket_delete() {
if ( ! isset( $_POST['post_ID'] ) ) {
$this->ajax_error( 'Bad post' );
}
if ( ! isset( $_POST['ticket_id'] ) ) {
$this->ajax_error( 'Bad post' );
}
$post_id = $_POST['post_ID'];
if ( ! $this->has_permission( $post_id, $_POST, 'remove_ticket_nonce' ) ) {
$this->ajax_error( "Cheatin' huh?" );
}
$ticket_id = $_POST['ticket_id'];
// Pass the control to the child object
$return = $this->delete_ticket( $post_id, $ticket_id );
// Successfully deleted?
if ( $return ) {
// Let's create a tickets list markup to return
$tickets = $this->get_event_tickets( $post_id );
$return = Tribe__Tickets__Tickets_Handler::instance()->get_ticket_list_markup( $tickets );
$return = $this->notice( esc_html__( 'Your ticket has been deleted.', 'event-tickets' ) ) . $return;
/**
* Fire action when a ticket has been deleted
*
* @param $post_id
*/
do_action( 'tribe_tickets_ticket_deleted', $post_id );
}
$this->ajax_ok( $return );
}
/**
* Returns the data from a single ticket to populate
* the edit form.
*/
final public function ajax_handler_ticket_edit() {
if ( ! isset( $_POST['post_ID'] ) ) {
$this->ajax_error( 'Bad post' );
}
if ( ! isset( $_POST['ticket_id'] ) ) {
$this->ajax_error( 'Bad post' );
}
$post_id = $_POST['post_ID'];
if ( ! $this->has_permission( $post_id, $_POST, 'edit_ticket_nonce' ) ) {
$this->ajax_error( "Cheatin' huh?" );
}
$ticket_id = $_POST['ticket_id'];
$ticket = $this->get_ticket( $post_id, $ticket_id );
$return = get_object_vars( $ticket );
/**
* Allow for the prevention of updating ticket price on update.
*
* @var boolean
* @var WP_Post
*/
$can_update_price = apply_filters( 'tribe_tickets_can_update_ticket_price', true, $ticket );
$return['can_update_price'] = $can_update_price;
if ( ! $can_update_price ) {
/**
* Filter the no-update message that is displayed when updating the price is disallowed
*
* @var string
* @var WP_Post
*/
$return['disallow_update_price_message'] = apply_filters( 'tribe_tickets_disallow_update_ticket_price_message', esc_html__( 'Editing the ticket price is currently disallowed.', 'event-tickets' ), $ticket );
}
// Prevent HTML elements from been escaped
$return['name'] = html_entity_decode( $return['name'], ENT_QUOTES );
$return['name'] = htmlspecialchars_decode( $return['name'] );
$return['description'] = html_entity_decode( $return['description'], ENT_QUOTES );
$return['description'] = htmlspecialchars_decode( $return['description'] );
ob_start();
/**
* Fired to allow for the insertion of extra form data in the ticket admin form
*
* @var $post_id Post ID
* @var $ticket_id Ticket ID
*/
do_action( 'tribe_events_tickets_metabox_advanced', $post_id, $ticket_id );
$extra = ob_get_contents();
ob_end_clean();
$return['advanced_fields'] = $extra;
/**
* Provides an opportunity for final adjustments to the data used to populate
* the edit-ticket form.
*
* @var array $return data returned to the client
* @var Tribe__Events__Tickets $ticket_object
*/
$return = (array) apply_filters( 'tribe_events_tickets_ajax_ticket_edit', $return, $this );
$this->ajax_ok( $return );
}
/**
* Returns the markup for a notice in the admin
*
* @param string $msg Text for the notice
*
* @return string Notice with markup
*/
protected function notice( $msg ) {
return sprintf( '
%s
', $msg );
}
// end AJAX Handlers
// start Attendees
/**
* Returns all the attendees for an event. Queries all registered providers.
*
* @static
*
* @param $event_id
*
* @return array
*/
public static function get_event_attendees( $event_id ) {
$attendees = array();
if ( ! is_admin() ) {
$post_transient = Tribe__Post_Transient::instance();
$attendees = $post_transient->get( $event_id, self::ATTENDEES_CACHE );
if ( ! $attendees ) {
$attendees = array();
}
}
if ( empty( $attendees ) ) {
foreach ( self::modules() as $class => $module ) {
$obj = call_user_func( array( $class, 'get_instance' ) );
$attendees = array_merge( $attendees, $obj->get_attendees( $event_id ) );
}
// Set the `ticket_exists` flag on attendees if the ticket they are associated with
// does not exist.
foreach ( $attendees as &$attendee ) {
$attendee['ticket_exists'] = ! empty( $attendee['product_id'] ) && get_post( $attendee['product_id'] );
}
if ( ! is_admin() ) {
$expire = apply_filters( 'tribe_tickets_attendees_expire', HOUR_IN_SECONDS );
$post_transient->set( $event_id, self::ATTENDEES_CACHE, $attendees, $expire );
}
}
/**
* Filters the return data for event attendees.
*
* @since 4.4
*
* @param array $attendees Array of event attendees.
* @param int $event_id Event post ID.
*/
return apply_filters( 'tribe_tickets_event_attendees', $attendees, $event_id );
}
/**
* Returns an array of attendees for the specified event, in relation to
* this ticketing provider.
*
* Implementation note: this is just a public wrapper around the get_attendees() method.
* The reason we don't simply make that same method public is to avoid breakages in other
* ticket provider plugins which have already implemented that method with protected
* accessibility.
*
* @param $event_id
*
* @return array
*/
public function get_attendees_array( $event_id ) {
return $this->get_attendees( $event_id );
}
/**
* Returns the total number of attendees for an event (regardless of provider).
*
* @param int $event_id
*
* @return int
*/
public static function get_event_attendees_count( $event_id ) {
$attendees = self::get_event_attendees( $event_id );
return count( $attendees );
}
/**
* Returns all tickets for an event (all providers are queried for this information).
*
* @param $event_id
*
* @return array
*/
public static function get_all_event_tickets( $event_id ) {
$tickets = array();
$modules = self::modules();
foreach ( $modules as $class => $module ) {
$obj = call_user_func( array( $class, 'get_instance' ) );
$tickets = array_merge( $tickets, $obj->get_tickets( $event_id ) );
}
return $tickets;
}
/**
* Tests to see if the provided object/ID functions as a ticket for the event
* and returns the corresponding event if so (or else boolean false).
*
* All registered providers are asked to perform this test.
*
* @param $possible_ticket
* @return bool
*/
public static function find_matching_event( $possible_ticket ) {
foreach ( self::modules() as $class => $module ) {
$obj = call_user_func( array( $class, 'get_instance' ) );
$event = $obj->get_event_for_ticket( $possible_ticket );
if ( false !== $event ) return $event;
}
return false;
}
/**
* Returns the sum of all checked-in attendees for an event. Queries all registered providers.
*
* @static
*
* @param $event_id
*
* @return mixed
*/
final public static function get_event_checkedin_attendees_count( $event_id ) {
$checkedin = self::get_event_attendees( $event_id );
return array_reduce( $checkedin, array( 'Tribe__Tickets__Tickets', '_checkedin_attendees_array_filter' ), 0 );
}
/**
* Internal function to use as a callback for array_reduce in
* get_event_checkedin_attendees_count. It increments the counter
* if the attendee is checked-in.
*
* @static
*
* @param $result
* @param $item
*
* @return mixed
*/
private static function _checkedin_attendees_array_filter( $result, $item ) {
if ( ! empty( $item['check_in'] ) )
return $result + 1;
return $result;
}
// end Attendees
// start Helpers
/**
* Indicates if any of the currently available providers support global stock.
*
* @return bool
*/
public static function global_stock_available() {
foreach ( self::modules() as $class => $module ) {
$provider = call_user_func( array( $class, 'get_instance' ) );
if ( method_exists( $provider, 'supports_global_stock' ) && $provider->supports_global_stock() ) {
return true;
}
}
return false;
}
/**
* Returns whether a class name is a valid active module/provider.
*
* @param $module
*
* @return bool
*/
private function module_is_valid( $module ) {
return array_key_exists( $module, self::modules() );
}
/**
* Echos the class for the
in the tickets list admin
*/
protected function tr_class() {
echo 'ticket_advanced ticket_advanced_' . $this->className;
}
/**
* Generates a select element listing the available global stock mode options.
*
* @param string $current_option
*
* @return string
*/
protected function global_stock_mode_selector( $current_option = '' ) {
$output = "";
}
/**
* Returns an array of standard global stock mode options that can be
* reused by implementations.
*
* Format is: [ 'identifier' => 'Localized name', ... ]
*
* @return array
*/
protected function global_stock_mode_options() {
return array(
Tribe__Tickets__Global_Stock::GLOBAL_STOCK_MODE => __( 'Use global stock', 'event-tickets' ),
Tribe__Tickets__Global_Stock::CAPPED_STOCK_MODE => __( 'Use global stock but cap sales', 'event-tickets' ),
Tribe__Tickets__Global_Stock::OWN_STOCK_MODE => __( 'Independent (do not use global stock)', 'event-tickets' ),
);
}
/**
* Tries to make data about global stock levels and global stock-enabled ticket objects
* available to frontend scripts.
*
* @param array $tickets
*/
public static function add_frontend_stock_data( array $tickets ) {
// Add the frontend ticket form script as needed (we do this lazily since right now
// it's only required for certain combinations of event/ticket
if ( ! self::$frontend_script_enqueued ) {
$url = Tribe__Tickets__Main::instance()->plugin_url . 'src/resources/js/frontend-ticket-form.js';
$url = Tribe__Template_Factory::getMinFile( $url, true );
wp_enqueue_script( 'tribe_tickets_frontend_tickets', $url, array( 'jquery' ), Tribe__Tickets__Main::VERSION, true );
add_action( 'wp_footer', array( __CLASS__, 'enqueue_frontend_stock_data' ), 1 );
}
self::$frontend_ticket_data += $tickets;
}
/**
* Takes any global stock data and makes it available via a wp_localize_script() call.
*/
public static function enqueue_frontend_stock_data() {
$data = array(
'tickets' => array(),
'events' => array(),
);
foreach ( self::$frontend_ticket_data as $ticket ) {
/**
* @var Tribe__Tickets__Ticket_Object $ticket
*/
$event_id = $ticket->get_event()->ID;
$global_stock = new Tribe__Tickets__Global_Stock( $event_id );
$stock_mode = $ticket->global_stock_mode();
$data[ 'tickets' ][ $ticket->ID ] = array(
'event_id' => $event_id,
'mode' => $stock_mode,
);
if ( Tribe__Tickets__Global_Stock::CAPPED_STOCK_MODE === $stock_mode ) {
$data[ 'tickets' ][ $ticket->ID ][ 'cap' ] = $ticket->global_stock_cap();
}
if (
Tribe__Tickets__Global_Stock::OWN_STOCK_MODE === $stock_mode
&& $ticket->managing_stock()
) {
$data[ 'tickets' ][ $ticket->ID ][ 'stock' ] = $ticket->stock();
}
$data[ 'events' ][ $event_id ] = array(
'stock' => $global_stock->get_stock_level(),
);
}
wp_localize_script( 'tribe_tickets_frontend_tickets', 'tribe_tickets_stock_data', $data );
}
/**
* Returns the array of active modules/providers.
*
* @static
* @return array
*/
public static function modules() {
/**
* Filters the available tickets modules
*
* @var string[] ticket modules
*/
return apply_filters( 'tribe_tickets_get_modules', self::$active_modules );
}
/**
* Get all the tickets for an event. Queries all active modules/providers.
*
* @static
*
* @param $event_id
*
* @return array
*/
final public static function get_event_tickets( $event_id ) {
$tickets = array();
foreach ( self::modules() as $class => $module ) {
$obj = call_user_func( array( $class, 'get_instance' ) );
$tickets = array_merge( $tickets, $obj->get_tickets( $event_id ) );
}
return $tickets;
}
/**
* Sets an AJAX error, returns a JSON array and ends the execution.
*
* @param string $message
*/
final protected function ajax_error( $message = '' ) {
header( 'Content-type: application/json' );
echo json_encode(
array(
'success' => false,
'message' => $message,
)
);
exit;
}
/**
* Sets an AJAX response, returns a JSON array and ends the execution.
*
* @param $data
*/
final protected function ajax_ok( $data ) {
$return = array();
if ( is_object( $data ) ) {
$return = get_object_vars( $data );
} elseif ( is_array( $data ) || is_string( $data ) ) {
$return = $data;
} elseif ( is_bool( $data ) && ! $data ) {
$this->ajax_error( 'Something went wrong' );
}
header( 'Content-type: application/json' );
echo json_encode(
array(
'success' => true,
'data' => $return,
)
);
exit;
}
/**
* Generates and returns the email template for a group of attendees.
*
* @param $tickets
*
* @return string
*/
public function generate_tickets_email_content( $tickets ) {
ob_start();
$file = $this->getTemplateHierarchy( 'tickets/email.php' );
if ( ! file_exists( $file ) ) {
$file = Tribe__Tickets__Main::instance()->plugin_path . 'src/views/tickets/email.php';
}
include $file;
return ob_get_clean();
}
/**
* Gets the view from the plugin's folder, or from the user's theme if found.
*
* @param $template
*
* @return mixed|void
*/
public function getTemplateHierarchy( $template ) {
if ( substr( $template, - 4 ) != '.php' ) {
$template .= '.php';
}
if ( $theme_file = locate_template( array( 'tribe-events/' . $template ) ) ) {
$file = $theme_file;
} else {
$file = $this->pluginPath . 'src/views/' . $template;
}
return apply_filters( 'tribe_events_tickets_template_' . $template, $file );
}
/**
* Queries ticketing providers to establish the range of tickets/pricepoints for the specified
* event and ensures those costs are included in the $costs array.
*
* @param array $prices
* @param int $event_id
*
* @return array
*/
public function get_ticket_prices( array $prices, $event_id ) {
// Iterate through all tickets from all providers
foreach ( self::get_all_event_tickets( $event_id ) as $ticket ) {
// No need to add the pricepoint if it is already in the array
if ( in_array( $ticket->price, $prices ) ) {
continue;
}
// An empty price property can be ignored (but do add if the price is explicitly set to zero)
elseif ( isset( $ticket->price ) && is_numeric( $ticket->price ) ) {
$prices[] = $ticket->price;
}
}
return $prices;
}
/**
* Tests if the user has the specified capability in relation to whatever post type
* the attendee object relates to.
*
* For example, if the attendee was generated for a ticket set up in relation to a
* post of the banana type, the generic capability "edit_posts" will be mapped to
* "edit_bananas" or whatever is appropriate.
*
* @internal for internal plugin use only (in spite of having public visibility)
* @see Tribe__Tickets__Tickets_Handler::user_can()
*
* @param string $generic_cap
* @param int $attendee_id
* @return boolean
*/
public function user_can( $generic_cap, $attendee_id ) {
$event_id = $this->get_event_id_from_attendee_id( $attendee_id );
if ( empty( $event_id ) ) {
return false;
}
return Tribe__Tickets__Tickets_Handler::instance()->user_can( $generic_cap, $event_id );
}
/**
* Given a valid attendee ID, returns the event ID it relates to or else boolean false
* if it cannot be determined.
*
* @param int $attendee_id
* @return mixed int|bool
*/
public function get_event_id_from_attendee_id( $attendee_id ) {
$provider_class = new ReflectionClass( $this );
$attendee_event_key = $this->get_attendee_event_key( $provider_class );
if ( empty( $attendee_event_key ) ) {
return false;
}
$event_id = get_post_meta( $attendee_id, $attendee_event_key, true );
if ( empty( $event_id ) ) {
return false;
}
return (int) $event_id;
}
/**
* Given a valid order ID, returns the event ID it relates to or else boolean false
* if it cannot be determined.
*
* @param int $order_id
* @return mixed int|bool
*/
public function get_event_id_from_order_id( $order_id ) {
$provider_class = new ReflectionClass( $this );
$attendee_order_key = $this->get_attendee_order_key( $provider_class );
$attendee_event_key = $this->get_attendee_event_key( $provider_class );
$attendee_object = $this->get_attendee_object( $provider_class );
if ( empty( $attendee_order_key ) || empty( $attendee_event_key ) || empty( $attendee_object ) ) {
return false;
}
$first_matched_attendee = get_posts( array(
'post_type' => $attendee_object,
'meta_key' => $attendee_order_key,
'meta_value' => $order_id,
'posts_per_page' => 1,
) );
if ( empty( $first_matched_attendee ) ) {
return false;
}
return $this->get_event_id_from_attendee_id( $first_matched_attendee[0]->ID );
}
/**
* Returns the meta key used to link attendees with orders.
*
* This method provides backwards compatibility with older ticketing providers
* that do not define the expected class constants. Once a decent period has
* elapsed we can kill this method and access the class constants directly.
*
* @param ReflectionClass $provider_class representing the concrete ticket provider
* @return string
*/
protected function get_attendee_order_key( $provider_class ) {
$attendee_order_key = $provider_class->getConstant( 'ATTENDEE_ORDER_KEY' );
if ( empty( $attendee_order_key ) ) {
switch ( $this->className ) {
case 'Tribe__Events__Tickets__Woo__Main': return '_tribe_wooticket_order'; break;
case 'Tribe__Events__Tickets__EDD__Main': return '_tribe_eddticket_order'; break;
case 'Tribe__Events__Tickets__Shopp__Main': return '_tribe_shoppticket_order'; break;
case 'Tribe__Events__Tickets__Wpec__Main': return '_tribe_wpecticket_order'; break;
}
}
return (string) $attendee_order_key;
}
/**
* Returns the attendee object post type.
*
* This method provides backwards compatibility with older ticketing providers
* that do not define the expected class constants. Once a decent period has
* elapsed we can kill this method and access the class constants directly.
*
* @param ReflectionClass $provider_class representing the concrete ticket provider
* @return string
*/
protected function get_attendee_object( $provider_class ) {
$attendee_object = $provider_class->getConstant( 'ATTENDEE_OBJECT' );
if ( empty( $attendee_order_key ) ) {
switch ( $this->className ) {
case 'Tribe__Events__Tickets__Woo__Main': return 'tribe_wooticket'; break;
case 'Tribe__Events__Tickets__EDD__Main': return 'tribe_eddticket'; break;
case 'Tribe__Events__Tickets__Shopp__Main': return 'tribe_shoppticket'; break;
case 'Tribe__Events__Tickets__Wpec__Main': return 'tribe_wpecticket'; break;
}
}
return (string) $attendee_object;
}
/**
* Returns the meta key used to link attendees with the base event.
*
* This method provides backwards compatibility with older ticketing providers
* that do not define the expected class constants. Once a decent period has
* elapsed we can kill this method and access the class constants directly.
*
* If the meta key cannot be determined the returned string will be empty.
*
* @param ReflectionClass $provider_class representing the concrete ticket provider
* @return string
*/
protected function get_attendee_event_key( $provider_class ) {
$attendee_event_key = $provider_class->getConstant( 'ATTENDEE_EVENT_KEY' );
if ( empty( $attendee_event_key ) ) {
switch ( $this->className ) {
case 'Tribe__Events__Tickets__Woo__Main': return '_tribe_wooticket_event'; break;
case 'Tribe__Events__Tickets__EDD__Main': return '_tribe_eddticket_event'; break;
case 'Tribe__Events__Tickets__Shopp__Main': return '_tribe_shoppticket_event'; break;
case 'Tribe__Events__Tickets__Wpec__Main': return '_tribe_wpecticket_event'; break;
}
}
return (string) $attendee_event_key;
}
/**
* Returns the meta key used to link ticket types with the base event.
*
* If the meta key cannot be determined the returned string will be empty.
* Subclasses can override this if they use a key other than 'event_key'
* for this purpose.
*
* @internal
*
* @return string
*/
public function get_event_key() {
if ( property_exists( $this, 'event_key' ) ) {
return $this->event_key;
}
return '';
}
/**
* Returns an availability slug based on all tickets in the provided collection
*
* The availability slug is used for CSS class names and filter helper strings
*
* @since 4.2
*
* @param array $tickets Collection of tickets
* @param string $datetime Datetime string
*
* @return string
*/
public function get_availability_slug_by_collection( $tickets, $datetime = null ) {
if ( ! $tickets ) {
return;
}
if ( is_numeric( $datetime ) ) {
$timestamp = $datetime;
} elseif ( $datetime ) {
$timestamp = strtotime( $datetime );
} else {
$timestamp = current_time( 'timestamp' );
}
$collection_availability_slug = 'available';
$tickets_available = false;
$slugs = array();
foreach ( $tickets as $ticket ) {
$availability_slug = $ticket->availability_slug( $timestamp );
// if any ticket is available for this event, consider the availability slug as 'available'
if ( 'available' === $availability_slug ) {
// reset the collected slugs to "available" only
$slugs = array( 'available' );
break;
}
// track unique availability slugs
if ( ! in_array( $availability_slug, $slugs ) ) {
$slugs[] = $availability_slug;
}
}
if ( 1 === count( $slugs ) ) {
$collection_availability_slug = $slugs[0];
} else {
$collection_availability_slug = 'availability-mixed';
}
/**
* Filters the availability slug for a collection of tickets
*
* @var string Availability slug
* @var array Collection of tickets
* @var string Datetime string
*/
return apply_filters( 'event_tickets_availability_slug_by_collection', $collection_availability_slug, $tickets, $datetime );
}
/**
* Returns a tickets unavailable message based on the availability slug of a collection of tickets
*
* @since 4.2
*
* @param array $tickets Collection of tickets
*
* @return string
*/
public function get_tickets_unavailable_message( $tickets ) {
$availability_slug = $this->get_availability_slug_by_collection( $tickets );
$message = null;
$post_type = get_post_type();
if ( 'tribe_events' == $post_type && function_exists( 'tribe_is_past_event' ) && tribe_is_past_event() ) {
$events_label_singular_lowercase = tribe_get_event_label_singular_lowercase();
$message = sprintf( esc_html__( 'Tickets are not available as this %s has passed.', 'event-tickets' ), $events_label_singular_lowercase );
} elseif ( 'availability-future' === $availability_slug ) {
$message = __( 'Tickets are not yet available.', 'event-tickets' );
} elseif ( 'availability-past' === $availability_slug ) {
$message = __( 'Tickets are no longer available.', 'event-tickets' );
} elseif ( 'availability-mixed' === $availability_slug ) {
$message = __( 'There are no tickets available at this time.', 'event-tickets' );
}
/**
* Filters the unavailability message for a ticket collection
*
* @var string Unavailability message
* @var array Collection of tickets
*/
$message = apply_filters( 'event_tickets_unvailable_message', $message, $tickets );
return $message;
}
/**
* Indicates that, from an individual ticket provider's perspective, the only tickets for the
* event are currently unavailable and unless a different ticket provider reports differently
* the "tickets unavailable" message should be displayed.
*
* @param array $tickets
* @param int $post_id = null (defaults to the current post)
*/
public function maybe_show_tickets_unavailable_message( $tickets, $post_id = null ) {
if ( null === $post_id ) {
$post_id = get_the_ID();
}
$existing_tickets = ! empty( self::$currently_unavailable_tickets[ (int) $post_id ] )
? self::$currently_unavailable_tickets[ (int) $post_id ]
: array();
self::$currently_unavailable_tickets[ (int) $post_id ] = array_merge( $existing_tickets, $tickets );
}
/**
* Indicates that, from an individual ticket provider's perspective, the event does have some
* currently available tickets and so the "tickets unavailable" message should probably not
* be displayed.
*
* @param null $post_id
*/
public function do_not_show_tickets_unavailable_message( $post_id = null ) {
if ( null === $post_id ) {
$post_id = get_the_ID();
}
self::$posts_with_available_tickets[] = (int) $post_id;
}
/**
* If appropriate, displayed a "tickets unavailable" message.
*/
public function show_tickets_unavailable_message() {
$post_id = (int) get_the_ID();
// So long as at least one ticket provider has tickets available, do not show an unavailability message
if ( in_array( $post_id, self::$posts_with_available_tickets ) ) {
return;
}
// Bail if no ticket providers reported that all their tickets for the event were unavailable
if ( empty( self::$currently_unavailable_tickets[ $post_id ] ) ) {
return;
}
// Prepare the message
$message = '
';
/**
* Sets the tickets unavailable message.
*
* @param string $message
* @param int $post_id
* @param array $unavailable_event_tickets
*/
echo apply_filters( 'tribe_tickets_unavailable_message', $message, $post_id, self::$currently_unavailable_tickets[ $post_id ] );
// Remove the record of unavailable tickets to avoid duplicate messages being rendered for the same event
unset( self::$currently_unavailable_tickets[ $post_id ] );
}
/**
* Takes care of adding a "tickets unavailable" message by injecting it into the post content
* (where the template settings require such an approach).
*
* @param string $content
*
* @return string
*/
public function show_tickets_unavailable_message_in_content( $content ) {
if ( ! $this->should_inject_ticket_form_into_post_content() ) {
return $content;
}
ob_start();
$this->show_tickets_unavailable_message();
$form = ob_get_clean();
$content .= $form;
return $content;
}
// end Helpers
/**
* Associates an attendee record with a user, typically the purchaser.
*
* The $user_id param is optional and when not provided it will default to the current
* user ID.
*
* @param int $attendee_id
* @param int $user_id
*/
protected function record_attendee_user_id( $attendee_id, $user_id = null ) {
if ( null === $user_id ) {
$user_id = get_current_user_id();
}
update_post_meta( $attendee_id, self::ATTENDEE_USER_ID, (int) $user_id );
}
public function front_end_tickets_form_in_content( $content ) {
if ( ! $this->should_inject_ticket_form_into_post_content() ) {
return $content;
}
ob_start();
$this->front_end_tickets_form( $content );
$form = ob_get_clean();
$content .= $form;
return $content;
}
/**
* Determines if this is a suitable opportunity to inject ticket form content into a post.
* Expects to run within "the_content".
*
* @return bool
*/
protected function should_inject_ticket_form_into_post_content() {
global $post;
// Prevents firing more then it needs too outside of the loop
$in_the_loop = isset( $GLOBALS['wp_query']->in_the_loop ) && $GLOBALS['wp_query']->in_the_loop;
if ( is_admin() || ! $in_the_loop ) {
return false;
}
// if this isn't a post for some reason, bail
if ( ! $post instanceof WP_Post ) {
return false;
}
// if this isn't a supported post type, bail
if ( ! in_array( $post->post_type, Tribe__Tickets__Main::instance()->post_types() ) ) {
return false;
}
// if this is a tribe_events post, let's bail because those post types are handled with a different hook
if ( 'tribe_events' === $post->post_type ) {
return false;
}
// if there aren't any tickets, bail
$tickets = $this->get_tickets( $post->ID );
if ( empty( $tickets ) ) {
return false;
}
return true;
}
/**
* Indicates if the user must be logged in in order to obtain tickets.
*
* This should be regarded as an abstract method to be overridden by subclasses:
* the reason it is not formally declared as abstract is to avoid breakages upon
* update (for example, where Event Tickets is updated first but a dependent plugin
* not yet implementing the abstract method remains at an earlier version).
*
* @return bool
*/
protected function login_required() {
return false;
}
/**
* Provides a URL that can be used to direct users to the login form.
*
* @return string
*/
public static function get_login_url() {
$post_id = get_the_ID();
$login_url = get_site_url( null, 'wp-login.php' );
if ( $post_id ) {
$login_url = add_query_arg( 'redirect_to', get_permalink( $post_id ), $login_url );
}
/**
* Provides an opportunity to modify the login URL used within frontend
* ticket forms (typically when they need to login before they can proceed).
*
* @param string $login_url
*/
return apply_filters( 'tribe_tickets_ticket_login_url', $login_url );
}
/**
* @param $operation_did_complete
*/
private function maybe_update_attendees_cache( $operation_did_complete ) {
if ( $operation_did_complete && ! empty( $_POST['event_ID'] ) ) {
$post_transient = Tribe__Post_Transient::instance();
$post_transient->delete( $_POST['event_ID'], self::ATTENDEES_CACHE );
}
}
/**
* Returns the action tag that should be used to print the front-end ticket form.
*
* This value is set in the Events > Settings > Tickets tab and is distinct between RSVP
* tickets and commerce provided tickets.
*
* @return string
*/
protected function get_ticket_form_hook() {
if ( is_a( $this, 'Tribe__Tickets__RSVP' ) ) {
$ticket_form_hook = Tribe__Settings_Manager::get_option( 'ticket-rsvp-form-location',
'tribe_events_single_event_after_the_meta' );
/**
* Filters the position of the RSVP tickets form.
*
* While this setting can be handled using the Events > Settings > Tickets > "Location of RSVP form"
* setting this filter allows developers to override the general setting in particular cases.
* Returning an empty value here will prevent the ticket form from printing on the page.
*
* @param string $ticket_form_hook The set action tag to print front-end RSVP tickets form.
* @param Tribe__Tickets__Tickets $this The current instance of the class that's hooking its front-end ticket form.
*/
$ticket_form_hook = apply_filters( 'tribe_tickets_rsvp_tickets_form_hook', $ticket_form_hook, $this );
} else {
$ticket_form_hook = Tribe__Settings_Manager::get_option( 'ticket-commerce-form-location',
'tribe_events_single_event_after_the_meta' );
/**
* Filters the position of the commerce-provided tickets form.
*
* While this setting can be handled using the Events > Settings > Tickets > "Location of Tickets form"
* setting this filter allows developers to override the general setting in particular cases.
* Returning an empty value here will prevent the ticket form from printing on the page.
*
* @param string $ticket_form_hook The set action tag to print front-end commerce tickets form.
* @param Tribe__Tickets__Tickets $this The current instance of the class that's hooking its front-end ticket form.
*/
$ticket_form_hook = apply_filters( 'tribe_tickets_commerce_tickets_form_hook', $ticket_form_hook, $this );
}
return $ticket_form_hook;
}
}
}