<?php
use TEC\Events\Custom_Tables\V1\Models\Occurrence;
use Tribe__Utils__Array as Arr;
if ( ! class_exists( 'Tribe__Tickets__Tickets' ) ) {
/**
* Class with the API definition and common functionality for Tribe Tickets. Providers for this functionality need
* to extend this class.
*
* The relationship between orders, attendees, and event posts is
* maintained through post meta fields set for the attendee object.
* Implementing classes are expected to provide the following class
* constants detailing those meta keys:
*
* ATTENDEE_ORDER_KEY
* ATTENDEE_EVENT_KEY
* ATTENDEE_PRODUCT_KEY
*
* The post type name used for the attendee object should also be
* made available via:
*
* ATTENDEE_OBJECT
*
* @since 4.5.0.1 Due to a fatal between Event Ticket Plus extending commerces and this class,
* we changed this from an Abstract to a normal parent class.
*/
class Tribe__Tickets__Tickets {
/**
* Flag used to track if the registration form link has been displayed or not.
*
* @var boolean
*/
private static $have_displayed_reg_link = false;
/**
* Function that is used to store the cache of a specific post associated with a set of tickets, where %d is the
* ID of the post being affected.
*
* @since 4.7.1
*
* @var string
*/
private static $cache_key_prefix = 'tribe_event_tickets_from_';
/**
* All Tribe__Tickets__Tickets api consumers. It's static, so it's shared across all children.
*
* @var array
*/
protected static $active_modules = [];
/**
* Default Tribe__Tickets__Tickets ecommerce module.
* It's static, so it's shared across all children.
*
* @var string
*/
protected static $default_module = 'Tribe__Tickets__RSVP';
/**
* Indicates if the frontend ticket form script has already been enqueued (or not).
*
* @var bool
*/
public static $frontend_script_enqueued = false;
/**
* Collection of ticket objects for which we wish to make global stock data available
* on the frontend.
*
* @var array
*/
protected static $frontend_ticket_data = [];
/**
* Name of this class. Note that it refers to the child class.
*
* @var string
*/
public $class_name;
/**
* Path of the parent class
*
* @var string
*/
private $parent_path;
/**
* URL of the parent class
*
* @var string
*/
private $parent_url;
/**
* Records batches of tickets that are currently unavailable (used for
* displaying the correct "tickets are unavailable" message).
*
* @var array
*/
protected static $currently_unavailable_tickets = [];
/**
* Records posts for which tickets *are* available (used to determine if
* a "tickets are unavailable" message should even display).
*
* @var array
*/
protected static $posts_with_available_tickets = [];
// start API Definitions
// Child classes must implement all these functions / properties
/**
* Name of the provider
*
* @var string
*/
public $plugin_name;
/**
* Path of the child class
*
* @var string
*/
protected $plugin_path;
/**
* URL of the child class
*
* @var string
*/
protected $plugin_url;
/**
* The name of the post type representing a ticket.
*
* @var string
*/
public $ticket_object = '';
/**
* The name of the meta key used to store whether an attendee is subscribed to updates.
*
* @since 5.0.3
*
* @var string
*/
public $attendee_subscribed = '_tribe_tickets_subscribed';
/* Deprecated vars */
/**
* Name of this class. Note that it refers to the child class.
* deprecated - use $class_name
*
* @deprecated 4.6
*
* @var string
*/
public $className;
/**
* Path of the parent class
* deprecated - use $parent_path
*
* @deprecated 4.6
*
* @var string
*/
private $parentPath;
/**
* URL of the parent class
* deprecated - use $parent_url
*
* @deprecated 4.6
*
* @var string
*/
private $parentUrl;
/**
* Name of the provider
* deprecated - use $plugin_name
*
* @deprecated 4.6
*
* @var string
*/
public $pluginName;
/**
* Path of the child class
* deprecated - use $plugin_path
*
* @deprecated 4.6
*
* @var string
*/
protected $pluginPath;
/**
* URL of the child class
* deprecated - use $plugin_url
*
* @deprecated 4.6
*
* @var string
*/
protected $pluginUrl;
/**
* Constant with the Transient Key for Attendees Cache
*/
const ATTENDEES_CACHE = 'tribe_attendees';
/**
* Meta key that contains the user id
*
* @deprecated 4.7 Use the $attendee_user_id variable instead
*
* @var string
*/
const ATTENDEE_USER_ID = '_tribe_tickets_attendee_user_id';
/**
* Meta key that contains the user id
*
* @var string
*/
public $attendee_user_id = '_tribe_tickets_attendee_user_id';
/**
* Name of the CPT that holds Orders
*/
public $order_object = '';
/**
* Name of the CPT that holds Attendees.
*
* @var string
*/
public $attendee_object = '';
/**
* Meta key that relates Attendees and Events.
*
* @var string
*/
public $attendee_event_key = '';
/**
* Meta key that relates Attendees and Products.
*
* @var string
*/
public $attendee_product_key = '';
/**
* Indicates if a ticket for this attendee was sent out via email.
*
* @var boolean
*/
public $attendee_ticket_sent = '_tribe_attendee_ticket_sent';
/**
* Logs the attendee notification email activity.
*
* @var array
*
* @since 5.1.0
*/
public $attendee_activity_log = '_tribe_attendee_activity_log';
/**
* Meta key that if this attendee wants to show on the attendee list
*
* @var string
*/
public $attendee_optout_key = '';
/**
* Meta key that holds the full name of the ticket attendee.
*
* @since 5.0.3
*
* @var string
*/
public $full_name = '_tribe_tickets_full_name';
/**
* Meta key that holds the email of the ticket attendee.
*
* @since 5.0.3
*
* @var string
*/
public $email = '_tribe_tickets_email';
/**
* Meta key that holds the security code that is used for printed tickets and QR codes.
*
* @since 5.0.3
*
* @var string
*/
public $security_code = '_tribe_tickets_security_code';
/**
* Meta key that holds the price paid for the ticket.
*
* @since 5.1.0
*
* @var string
*/
public $price_paid = '_paid_price';
/**
* Meta key that holds the price currency symbol used during payment.
*
* @since 5.1.0
*
* @var string
*/
public $price_currency = '_price_currency_symbol';
/**
* The provider used for Attendees and Tickets ORM.
*
* @var string
*/
public $orm_provider = 'default';
/**
* Meta key that stores if an attendee has checked in to a ticketed post.
*
* @since 5.8.2
*
* @var string
*/
public $checkin_key = '';
/**
* The key used to store the event ID in the ticket post meta.
*
* @since 5.14.0
*
* @var string
*/
public $event_key;
/**
* Returns link to the report interface for sales for an event or
* null if the provider doesn't have reporting capabilities.
*
* @abstract
*
* @param int $post_id ID of parent "event" post
* @return mixed
*/
public function get_event_reports_link( $post_id ) {}
/**
* Returns link to the report interface for sales for a single ticket or
* null if the provider doesn't have reporting capabilities.
* As of 4.6 we reversed the params and deprecated $post_id as it was never used
*
* @abstract
*
* @param deprecated $post_id ID of parent "event" post
* @param int $ticket_id ID of ticket post
* @return mixed
*/
public function get_ticket_reports_link( $post_id_deprecated, $ticket_id ) {}
/**
* Returns a single ticket.
*
* @param int $post_id ID of parent "event" post.
* @param int $ticket_id ID of ticket post.
*
* @return Tribe__Tickets__Ticket_Object|null
*/
public function get_ticket( $post_id, $ticket_id ) {
return null;
}
/**
* Set the Query args to fetch all the Tickets.
*
* @since 5.5.0 refactored to use the tickets ORM.
* @since 4.6
* @since 5.5.2 Set default query args.
* @since 5.8.0 Added the `$context` parameter.
* @since 5.24.1 Correct the docblock to reflect the method's behavior.
*
* @param int $post_id Build the args to query only
* for tickets related to this post ID.
* @param string|null $context The context of the query.
*
* @return Tribe__Repository__Interface
*/
public function set_tickets_query_args( $post_id = null, string $context = null ) {
$repository = tribe_tickets( $this->orm_provider );
$repository->set_request_context( $context );
$repository->by( 'event', $post_id );
$repository->by( 'status', 'publish' );
$repository->by( 'posts_per_page', -1 );
$repository->order_by( 'menu_order' );
$repository->order( 'ASC' );
$default_args = $repository->get_query();
/**
* Filters the query arguments that will be used to fetch tickets.
*
* @since 4.8
*
* @param array $args
*/
$vars = apply_filters( 'tribe_tickets_get_tickets_query_args', $default_args->query_vars );
if ( $default_args->query_vars !== $vars ) {
$repository->by_args( $vars );
}
return $repository;
}
/**
* Retrieve the ID numbers of all tickets assigned to an event.
*
* @since 4.6
* @since 5.5.0 refactored to use the tickets ORM.
* @since 5.8.0 Added the `$context` parameter.
*
* @param int|WP_Post $post Only get tickets assigned to this post ID.
*
* @return array|false
*/
public function get_tickets_ids( $post = 0, string $context = null ) {
$post_id = 0;
if ( is_numeric( $post ) ) {
$post_id = (int) $post;
}
if ( ! empty( $post ) ) {
if ( ! $post instanceof WP_Post ) {
$post = get_post( $post );
}
if ( ! $post instanceof WP_Post ) {
return false;
}
if ( class_exists( Occurrence::class, false ) ) {
/**
* Filters the post ID to use when fetching tickets for an Occurrence.
*
* @since 5.8.0
*
* @param int $post_id The post ID to use when fetching tickets for an Occurrence; this might
* be a real post ID, or a provisional one.
*/
$post_id = apply_filters( 'tec_tickets_normalize_occurrence_id', Occurrence::normalize_id( $post->ID ) );
}
}
return $this->set_tickets_query_args( $post_id, $context )->get_ids();
}
/**
* Returns the html for the delete ticket link
*
* @since 4.6
*
* @param object $ticket Ticket object
*
* @return string HTML link
*/
public function get_ticket_delete_link( $ticket = null ) {
if ( empty( $ticket ) ) {
return '';
}
$delete_text = _x( 'Delete %s', 'delete link', 'event-tickets' );
$button_text = ( 'Tribe__Tickets__RSVP' === $ticket->provider_class )
? sprintf( $delete_text, tribe_get_rsvp_label_singular( 'delete_link' ) )
: sprintf( $delete_text, tribe_get_ticket_label_singular( 'delete_link' ) );
/**
* Allows for the filtering and testing if a user can delete tickets
*
* @since 4.6
*
* @param bool true
* @param int ticket post ID
*
* @return string HTML link | void HTML link
*/
if ( apply_filters( 'tribe_tickets_current_user_can_delete_ticket', true, $ticket->ID, $ticket->provider_class ) ) {
$delete_link = sprintf(
'<span><a href="#" attr-provider="%1$s" attr-ticket-id="%2$s" id="ticket_delete_%2$s" class="ticket_delete">%3$s</a></span>',
$ticket->provider_class,
$ticket->ID,
esc_html( $button_text )
);
return $delete_link;
}
$delete_link = sprintf(
'<span><a href="#" attr-provider="%1$s" attr-ticket-id="%2$s" id="ticket_delete_%2$s" class="ticket_delete">%3$s</a></span>',
$ticket->provider_class,
$ticket->ID,
esc_html__( $button_text )
);
return $delete_link;
}
/**
* Returns the url for the move ticket link
*
* @since 4.6
*
* @param int $post_id ID of parent "event" post
* @param object $ticket Ticket object
*
* @return string HTML link | void HTML link
*/
public function get_ticket_move_url( $post_id, $ticket = null ) {
if ( empty( $ticket ) || empty( $post_id ) ) {
return '';
}
$post_url = get_edit_post_link( $post_id, 'admin' );
$move_type_url = add_query_arg(
[
'dialog' => Tribe__Tickets__Main::instance()->move_ticket_types()->dialog_name(),
'ticket_type_id' => $ticket->ID,
'check' => wp_create_nonce( 'move_tickets' ),
'TB_iframe' => 'true',
],
$post_url
);
return $move_type_url;
}
/**
* Returns the html for the move ticket link
*
* @since 4.6
*
* @param int $post_id ID of parent "event" post
* @param object $ticket Ticket object
*
* @return string HTML link | void HTML link
*/
public function get_ticket_move_link( $post_id, $ticket = null ) {
if ( empty( $ticket ) ) {
return '';
}
$move_text = __( 'Move %s', 'event-tickets' );
$button_text = ( 'Tribe__Tickets__RSVP' === $ticket->provider_class ) ? sprintf( $move_text, tribe_get_rsvp_label_singular( 'move_ticket_button_text' ) ) : sprintf( $move_text, tribe_get_ticket_label_singular( 'move_ticket_button_text' ) ) ;
$move_url = $this->get_ticket_move_url( $post_id, $ticket );
if ( empty( $move_url ) ) {
return '';
}
// Make sure Thickbox is available regardless of which admin page we're on.
add_thickbox();
$move_link = sprintf( '<a href="%1$s" class="thickbox tribe-ticket-move-link">%2$s</a>', $move_url, esc_html( $button_text ) );
return $move_link;
}
/**
* Get the controls (move, delete) as a string and add to our ajax return
*
* @deprecated 4.6.2
* @since 4.6
*
* @param array $return the ajax return data
* @return array $return modified data
*/
public function ajax_ticket_edit_controls( $return ) {
$ticket = $this->get_ticket( $return['post_id'], $return['ID'] );
if ( empty( $ticket ) ) {
return $return;
}
$controls = [];
if ( tribe_is_truthy( tribe_get_request_var( 'is_admin' ) ) ) {
$controls[] = $this->get_ticket_move_link( $return['post_id'], $ticket );
}
$controls[] = $this->get_ticket_delete_link( $ticket );
if ( ! empty( $controls ) ) {
$return['controls'] = join( ' | ', $controls );
}
return $return;
}
/**
* Attempts to load the specified ticket type post object.
*
* @param int $ticket_id ID of ticket post
* @return Tribe__Tickets__Ticket_Object|null
*/
public static function load_ticket_object( $ticket_id ) {
foreach ( self::modules() as $provider_class => $name ) {
$provider = static::get_ticket_provider_instance( $provider_class );
if ( empty( $provider ) ) {
continue;
}
$event = $provider->get_event_for_ticket( $ticket_id );
if ( empty( $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.
*
* @param object $ticket_product The ticket product.
*
* @return bool|WP_Post
*/
public function get_event_for_ticket( $ticket_product ) {
if ( is_object( $ticket_product ) && isset( $ticket_product->ID ) ) {
$ticket_product = $ticket_product->ID;
}
if ( null === get_post( $ticket_product ) ) {
return false;
}
$event_id = get_post_meta( $ticket_product, $this->get_event_key(), true );
if ( ! empty( $this->attendee_event_key ) && ! $event_id && '' === ( $event_id = get_post_meta( $ticket_product, $this->attendee_event_key, true ) ) ) {
return false;
}
$post_types = Tribe__Tickets__Main::instance()->post_types();
if ( in_array( get_post_type( $event_id ), $post_types ) ) {
return get_post( $event_id );
}
return false;
}
/**
* Deletes a ticket
*
* @abstract
*
* @param int $post_id ID of parent "event" post
* @param int $ticket_id ID of ticket post
* @return mixed
*/
public function delete_ticket( $post_id, $ticket_id ) {
/**
* Trigger action when any attendee is deleted.
*
* @since 5.1.5
*
* @param int $post_id Post or Event ID.
* @param int $ticket_id Attendee ID.
*/
do_action( 'event_tickets_attendee_ticket_deleted', $post_id, $ticket_id );
$this->clear_ticket_cache_for_post( $post_id );
$this->clear_attendees_cache( $post_id );
}
/**
* Saves a ticket.
*
* @abstract
*
* @param int $post_id Post ID.
* @param Tribe__Tickets__Ticket_Object $ticket Ticket object.
* @param array $raw_data Ticket data.
*
* @return int|false The updated/created ticket post ID or false if no ticket ID.
*/
public function save_ticket( $post_id, $ticket, $raw_data = [] ) {
$this->clear_ticket_cache_for_post( $post_id );
return false;
}
/**
* Whether a post has tickets from this provider, even if this provider is not the default provider.
*
* @since 4.12.3
*
* @param int|WP_Post $post
*
* @return bool True if this post has any tickets from this provider.
*/
public function post_has_tickets( $post ) {
$post_id = Tribe__Main::post_id_helper( $post );
if ( empty( $post_id ) ) {
return false;
}
return ! empty( $this->get_tickets_ids( $post_id ) );
}
/**
* Clear the ticket cache for a specific post ID.
*
* @since 5.1.0
*
* @param int $post_id The post ID.
*/
public function clear_ticket_cache_for_post( $post_id ) {
/** @var Tribe__Cache $cache */
$cache = tribe( 'cache' );
$class = __CLASS__;
$methods = [
'get_tickets',
];
foreach ( $methods as $method ) {
$key = $class . '::' . $method . '-' . $this->orm_provider . '-' . $post_id;
unset( $cache[ $key ] );
}
$static_methods = [
'get_all_event_tickets',
'get_event_attendees_count',
];
foreach ( $static_methods as $method ) {
$key = $class . '::' . $method . '-' . $post_id;
unset( $cache[ $key ] );
}
$ticket_ids = $this->get_tickets_ids( $post_id );
foreach ( (array) $ticket_ids as $ticket_id ) {
clean_post_cache( $ticket_id );
}
}
/**
* Returns all the tickets for an event, of the active ticket providers.
*
* @since 4.12.0 Changed from protected abstract to public with duplicated child classes' logic consolidated here.
* @since 5.8.0 Added the `$context` parameter.
*
* @param int $post_id ID of parent "event" post.
* @param string|null $context The context of the request.
*
* @return Tribe__Tickets__Ticket_Object[] List of ticket objects.
*/
public function get_tickets( $post_id, string $context = null ) {
/** @var Tribe__Cache $cache */
$cache = tribe( 'cache' );
$key = __METHOD__ . '-' . $this->orm_provider . '-' . $post_id;
if ( isset( $cache[ $key ] ) && is_array( $cache[ $key ] ) ) {
return $cache[ $key ];
}
$default_provider = static::get_event_ticket_provider( $post_id );
if ( empty( $default_provider ) ) {
return [];
}
// If the post's provider doesn't match.
if (
$this->class_name !== $default_provider
&& ! is_admin()
) {
return [];
}
$ticket_ids = $this->get_tickets_ids( $post_id, $context );
if ( ! $ticket_ids ) {
return [];
}
$tickets = [];
foreach ( $ticket_ids as $post ) {
$ticket = $this->get_ticket( $post_id, $post );
if (
! $ticket instanceof Tribe__Tickets__Ticket_Object
|| $this->class_name !== $ticket->provider_class
) {
continue;
}
$tickets[] = $ticket;
}
$cache[ $key ] = $tickets;
return $tickets;
}
/**
* Get attendees for a Post ID / Post type.
*
* @param int $post_id Post ID.
* @param null|string $post_type Post type.
*
* @return array List of attendees.
*/
public function get_attendees_by_id( $post_id, $post_type = null ) {
return $this->get_attendees_by_post_id( $post_id );
}
/**
* Get attendees for an event ID.
*
* @param int $event_id Event post ID.
*
* @return array List of attendees.
*/
protected function get_attendees_by_post_id( $event_id ) {
/** @var Tribe__Tickets__Attendee_Repository $repository */
$repository = tribe_attendees( $this->orm_provider );
return $this->get_attendees_from_module( $repository->by( 'event', $event_id )->all(), $event_id );
}
/**
* Get attendees for a ticket ID.
*
* @since 4.10.6
*
* @param int $ticket_id Ticket ID.
*
* @return array List of attendees.
*/
protected function get_attendees_by_ticket_id( $ticket_id ) {
/** @var Tribe__Tickets__Attendee_Repository $repository */
$repository = tribe_attendees( $this->orm_provider );
return $this->get_attendees_from_module( $repository->by( 'ticket', $ticket_id )->all() );
}
/**
* Get attendees for a ticket ID.
*
* @since 4.10.6
*
* @param int $ticket_id Ticket ID.
*
* @return array List of attendees.
*/
protected function get_attendees_by_product_id( $ticket_id ) {
return $this->get_attendees_by_ticket_id( $ticket_id );
}
/**
* Get attendees for a ticket by order ID, optionally by ticket ID.
*
* @since 4.6
*
* @param int|string $order_id Order ID.
* @param null|int $ticket_id (optional) Ticket ID.
*
* @return array List of attendees.
*/
public function get_attendees_by_order_id( $order_id ) {
$ticket_id = null;
// Support an optional second argument while not causing warnings from other ticket provider classes.
if ( 1 < func_num_args() ) {
$ticket_id = func_get_arg( 1 );
}
/** @var Tribe__Tickets__Attendee_Repository $repository */
$repository = tribe_attendees( $this->orm_provider );
$repository->by( 'order', $order_id );
if ( $ticket_id ) {
$repository->by( 'ticket', $ticket_id );
}
return $this->get_attendees_from_module( $repository->all() );
}
/**
* Get attendees for a ticket by attendee ID.
*
* @since 4.6
*
* @param int $attendee_id Attendee ID.
*
* @return array List of attendees.
*/
protected function get_attendees_by_attendee_id( $attendee_id ) {
/** @var Tribe__Tickets__Attendee_Repository $repository */
$repository = tribe_attendees( $this->orm_provider );
return $this->get_attendees_from_module( $repository->by( 'id', $attendee_id )->all() );
}
/**
* Get attendees for a ticket by user ID.
*
* @since 4.10.6
*
* @param int $user_id User ID.
* @param int $post_id Post or Event ID.
*
* @return array List of attendees.
*/
public function get_attendees_by_user_id( $user_id, $post_id = 0 ) {
/** @var Tribe__Tickets__Attendee_Repository $repository */
$repository = tribe_attendees( $this->orm_provider );
$repository->by( 'user', $user_id );
if ( $post_id ) {
$repository->by( 'event', $post_id );
}
return $this->get_attendees_from_module( $repository->all() );
}
/**
* Get All Attendees by ticket/attendee ID
*
* @since 4.8.0
*
* @param int $attendee_id
* @return array
*/
public function get_all_attendees_by_attendee_id( $attendee_id ) {
return $this->get_attendees_by_attendee_id( $attendee_id );
}
/**
* Get attendees from provided query
*
* @param WP_Query $attendees_query
* @param int $post_id ID of parent "event" post
* @return mixed
*/
protected function get_attendees( $attendees_query, $post_id ) {
$attendees = [];
foreach ( $attendees_query->posts as $attendee ) {
$attendee_data = $this->get_attendee( $attendee, $post_id );
if ( ! $attendee_data ) {
continue;
}
$attendees[] = $attendee_data;
}
return $attendees;
}
/**
* Whether a specific attendee is valid toward inventory decrease or not.
*
* @since 4.7
*
* @param array $attendee
*
* @return bool
*/
public function attendee_decreases_inventory( array $attendee ) {
return true;
}
/**
* Handles if email sending is allowed.
*
* @since 5.2.1
*
* @param WP_Post|null $ticket The ticket post object if available, otherwise null.
* @param array|null $attendee The attendee information if available, otherwise null.
*
* @return boolean
*/
public function allow_resending_email( $ticket = null, $attendee = null ) {
/**
*
* Shared filter between Woo, EDD, and the default logic.
* This filter allows the admin to control the re-send email option when an attendee's email is updated per a payment type (EDD, Woo, etc).
* True means allow email resend, false means disallow email resend.
*
* @since 5.2.1
*
* @param WP_Post|null $ticket The ticket post object if available, otherwise null.
* @param array|null $attendee The attendee information if available, otherwise null.
*
*/
return (bool) apply_filters( 'tribe_tickets_my_tickets_allow_email_resend_on_attendee_email_update', true, $ticket, $attendee );
}
/**
* Mark an Attendee as checked in.
*
* @since 3.1.2
* @since 5.8.2 Add the `tec_tickets_attendee_checkin` filter to override the checkin process. Update the method
* signature to include the `$qr` and `$eveent_id` parameters.
* @since - Add the optional `$details` parameter to be able to set the checkin time and device_id.
*
* @param int $attendee_id The ID of the attendee that's being checked in.
* @param bool|null $qr Whether the check-in comes from a QR code scan or not.
* @param int|null $event_id The ID of the ticket-able post the Attendee is being checked into.
* @param array<string|mixed> $details Check-out details including timestamp and device_id information.
*
* @return bool Whether the Attendee was checked in or not.
*/
public function checkin( $attendee_id, $qr = null, $event_id = null, $details = [] ) {
/**
* Allows filtering the Attendee check-in action before the default logic does it.
* Returning a non-null value from this filter will prevent the default logic from running.
*
* @since 5.8.2
*
* @param int $attendee_id The post ID of the Attendee being checked-in.
* @param int|null $event_id The ID of the ticket-able post the Attendee is being checked into.
* @param bool $qr Whether the check-in comes from a QR code scan or not.
*/
$checkin = apply_filters( 'tec_tickets_attendee_checkin', null, (int) $attendee_id, (int) $event_id, (bool) $qr );
if ( $checkin !== null ) {
return (bool) $checkin;
}
update_post_meta( $attendee_id, $this->checkin_key, 1 );
if ( isset( $qr ) && $qr = (bool) $qr ) {
update_post_meta( $attendee_id, '_tribe_qr_status', 1 );
}
$this->save_checkin_details( $attendee_id, $qr, $details );
/**
* Fires a checkin action
*
* @since 4.7
* @since 5.8.2 Add the `$event_id` argument to the filter data.
*
* @param int $attendee_id he post ID of the attendee that's being checked-in.
* @param bool|null $qr Whether the check-in is from a QR code.
* @param int|null $event_id The ID of the ticket-able post the Attendee is being checked into.
*/
do_action( 'event_tickets_checkin', $attendee_id, $qr, $event_id );
return true;
}
/**
* Save the attendee checkin details.
*
* @since 5.5.2
* @since 5.5.11 Update attendee scan count via tec_tickets_plus_app_attendees_checked_in option.
* @since - Add the optional `$details` parameter to be able to set the checkin time and device_id.
*
* @param int $attendee_id The ID of the attendee that's being checked-in.
* @param mixed $qr True if the check-in is from a QR code.
* @param array<string|mixed> $details Check-out details including timestamp and device_id information.
*/
public function save_checkin_details( $attendee_id, $qr, $details = [] ) {
$checkin_details = [
'date' => (string) ! empty( $details['timestamp'] ) ? $details['timestamp'] : current_time( 'mysql' ),
'source' => ! empty( $qr ) ? 'app' : 'site',
'author' => get_current_user_id(),
'device_id' => $details['device_id'] ?? null,
];
if ( ! empty( $qr ) ) {
// Save the latest date in which a ticket was scanned with the APP.
tribe_update_option( 'tec_tickets_plus_app_last_checkin_time', time() );
// Save the attendee scan count.
$attendee_scan_count = (int) tribe_get_option( 'tec_tickets_plus_app_attendees_checked_in' );
tribe_update_option( 'tec_tickets_plus_app_attendees_checked_in', ++$attendee_scan_count );
}
/**
* Filters the checkin details for this attendee checkin.
*
* @since 5.5.2
*
* @param array $checkin_details The check-in details.
* @param int $attendee_id The ID of the attendee that's being checked-in.
* @param mixed $qr True if the check-in is from a QR code.
*/
$checkin_details = apply_filters( 'tec_tickets_checkin_details', $checkin_details, $attendee_id, $qr );
update_post_meta( $attendee_id, $this->checkin_key . '_details', $checkin_details );
}
/**
* Mark an attendee as not checked in
*
* @abstract
* @since 5.25.0 - Add optional $app parameter to allow for bulk checkin process when using RSVP.
*
* @param int $attendee_id The ID of the attendee that's being checkedin.
* @param bool $app True if from bulk checkin process.
*
* @return mixed
*/
public function uncheckin( $attendee_id, $app = false ) { // phpcs:ignore Generic.CodeAnalysis.UnusedFunctionParameter
$context_id = tribe_get_request_var( 'event_ID', null );
/**
* Allows filtering the Attendee uncheck-in action before the default logic does it.
* Returning a non-null value from this filter will prevent the default logic from running.
*
* @since 5.8.3
*
* @param bool|null $uncheckin Whether the Attendee uncheckin action was handled by the filter or not.
* @param int $attendee_id The post ID of the Attendee being unchecked-in.
* @param int|null $context_id The post ID context of the Attendee uncheckin request.
*/
$uncheckin = apply_filters( 'tec_tickets_attendee_uncheckin', null, $attendee_id, $context_id );
if ( null !== $uncheckin ) {
return (bool) $uncheckin;
}
delete_post_meta( $attendee_id, $this->checkin_key );
delete_post_meta( $attendee_id, $this->checkin_key . '_details' );
delete_post_meta( $attendee_id, '_tribe_qr_status' );
/**
* Fires an uncheckin action
*
* @since 4.7
*
* @param int $attendee_id
*/
do_action( 'event_tickets_uncheckin', $attendee_id );
return true;
}
/**
* 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 int $post_id ID of parent "event" post
* @param int $ticket_id ID of ticket post
* @return mixed
*/
public function do_metabox_capacity_options( $post_id, $ticket_id ) {}
/**
* Renders the front end form for selling tickets in the event single page
*
* @param string $content The content.
*
* @return string|null
*/
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
* @param array|boolean $attendee
*
* @return string
*/
public function get_price_html( $product, $attendee = false ) {
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 class instance. Child classes must overload this.
*
* @static
*
* @return static
*/
public static function get_instance() {}
// end API Definitions
/**
*
*/
public function __construct() {
// As this is an abstract class, we want to know which child instantiated it
$this->class_name = $this->className = get_class( $this );
$this->parent_path = $this->parentPath = trailingslashit( dirname( dirname( dirname( __FILE__ ) ) ) );
$this->parent_url = $this->parentUrl = trailingslashit( plugins_url( '', $this->parent_path ) );
// Register all Tribe__Tickets__Tickets api consumers
self::$active_modules[ $this->class_name ] = $this->plugin_name;
add_action( 'wp', [ $this, 'hook' ] );
/**
* Priority set to 11 to force a specific display order
*
* @since 4.6
*/
add_action( 'tribe_events_tickets_metabox_edit_main', [ $this, 'do_metabox_capacity_options' ], 11, 2 );
// Ensure ticket prices and event costs are linked
add_filter( 'tribe_events_event_costs', [ $this, 'get_ticket_prices' ], 10, 2 );
add_filter( 'tribe_get_event_meta', [ $this, 'exclude_past_tickets_from_cost_range' ], 10, 4 );
add_action( 'event_tickets_checkin', [ $this, 'purge_attendees_transient' ] );
add_action( 'event_tickets_uncheckin', [ $this, 'purge_attendees_transient' ] );
add_action( 'template_redirect', [ $this, 'maybe_redirect_to_attendees_registration_screen' ], 0 );
// Event cost may need to be formatted to the provider's currency settings.
add_filter( 'tribe_currency_cost', [ $this, 'maybe_format_event_cost' ], 10, 2 );
add_action( 'init', [ $this, 'add_admin_tickets_hooks' ] );
}
/**
* Most Commerce Providers needs this to be setup later than when the actual class is actually loaded
*
* For Frontend Hooks, admin ones need to be loaded earlier
*
* @since 4.7.5
*
* @return void
*/
public function hook() {
// Front end
$ticket_form_hook = $this->get_ticket_form_hook();
if ( ! empty( $ticket_form_hook ) ) {
add_action( $ticket_form_hook, [ $this, 'maybe_add_front_end_tickets_form' ], 5 );
add_filter( $ticket_form_hook, [ $this, 'show_tickets_unavailable_message' ], 6 );
}
add_filter( 'the_content', [ $this, 'front_end_tickets_form_in_content' ], 11 );
add_filter( 'the_content', [ $this, 'show_tickets_unavailable_message_in_content' ], 12 );
/**
* Trigger an action every time a new ticket instance has been created
*
* @since 4.9
*
* @param Tribe__Tickets__Tickets $ticket_handler
*/
do_action( 'tribe_tickets_tickets_hook', $this );
}
/**
* Add all the hooks for the Admin Tickets page.
*
* @since 5.14.0
*
* @return void
*/
public function add_admin_tickets_hooks() {
add_filter( 'tec_tickets_admin_tickets_table_provider_info', [ $this, 'filter_admin_tickets_table_provider_info' ] );
}
/**
* Remove the attendees transient when a Ticket change its state
*
* @since 4.7.4
*
* @param int $attendee_id
* @return void
*/
public function purge_attendees_transient( $attendee_id ) {
$event_id = $this->get_event_id_from_attendee_id( $attendee_id );
if ( $event_id ) {
tribe( 'post-transient' )->delete( $event_id, self::ATTENDEES_CACHE );
}
}
/**
* Maybe add the Tickets Form as shouldn't be added if is unchecked from the settings
*
* @since 4.7.3
*
* @param string $content
*/
public function maybe_add_front_end_tickets_form( $content ) {
if ( ! tribe_tickets_post_type_enabled( get_post_type() ) ) {
return;
}
if ( post_password_required( get_the_ID() ) ) {
return;
}
return $this->front_end_tickets_form( $content );
}
// start Attendees
/**
* Returns all the attendees for an event. Queries all registered providers.
*
* @static
*
* @param int $post_id ID of parent "event" post.
* @param array $args List of arguments to filter by.
*
* @return array List of attendees.
*/
public static function get_event_attendees( $post_id, $args = [] ) {
$attendees = [];
/**
* Filter to skip all empty $post_ID otherwise will fallback to the current global post ID
*
* @since 4.9
* @since 4.10.6 Added $args parameter.
*
* @param bool $skip_empty_post If the empty post should be skipped or not
* @param int $post_id ID of the post being affected
* @param array $args List of arguments to filter by.
*/
$skip_empty_post = apply_filters( 'tribe_tickets_event_attendees_skip_empty_post', true, $post_id, $args );
/**
* Process an attendee only if:
*
* - $skip_empty_post is true and $post_id is not empty => ( true && false ) => ! false => true
* - $skip_empty_post is false and $post_id is empty => ( false && true ) => ! false => true
* - $skip_empty_post is false and $post_id is not empty => ( false && false ) => ! false => true
*
* Is not executed if:
*
* - $skip_empty_post is true and $post_id is empty => ( true && true ) => ! true => false
*/
if ( ! ( $skip_empty_post && empty( $post_id ) ) ) {
/**
* Filters the cache expiration when this function is called from an admin screen.
*
* Returning a falsy value here will force a fetch each time.
*
* @since 4.7
* @since 4.10.6 Added $args parameter.
*
* @param int $admin_expire The cache expiration in seconds; defaults to 2 minutes.
* @param int $post_id The ID of the post attendees are being fetched for.
* @param array $args List of arguments to filter by.
*/
$admin_expire = apply_filters( 'tribe_tickets_attendees_admin_expire', 120, $post_id, $args );
/**
* Filters the cache expiration when this function is called from a non admin screen.
*
* Returning a falsy value here will force a refetch each time.
*
* @since 4.7
* @since 4.10.6 Added $args parameter.
*
* @param int $admin_expire The cache expiration in seconds, defaults to an hour.
* @param int $post_id The ID of the post attendees are being fetched for.
* @param array $args List of arguments to filter by.
*/
$expire = apply_filters( 'tribe_tickets_attendees_expire', HOUR_IN_SECONDS, $post_id, $args );
$expire = is_admin() ? (int) $admin_expire : (int) $expire;
$attendees_from_cache = false;
$post_transient = null;
$cache_key = false;
if ( empty( $args ) && 0 < $post_id ) {
$cache_key = (int) $post_id;
}
if ( 0 !== $expire && $cache_key ) {
/** @var Tribe__Post_Transient $post_transient */
$post_transient = tribe( 'post-transient' );
$attendees_from_cache = $post_transient->get( $cache_key, self::ATTENDEES_CACHE );
// if there is a valid transient, we'll use the value from that and note
// that we have fetched from cache
if ( false !== $attendees_from_cache ) {
$attendees = empty( $attendees_from_cache ) ? [] : $attendees_from_cache;
$attendees_from_cache = true;
}
}
// if we haven't grabbed attendees from cache, then attempt to fetch attendees
if ( false === $attendees_from_cache && empty( $attendees ) ) {
$attendee_data = self::get_event_attendees_by_args( $post_id, $args );
if ( ! empty( $attendee_data['attendees'] ) ) {
$attendees = $attendee_data['attendees'];
}
if ( 0 !== $expire && $cache_key ) {
$post_transient->set( $cache_key, self::ATTENDEES_CACHE, $attendees, $expire );
}
}
}
/**
* Filters the return data for event attendees.
*
* @since 4.4
* @since 4.10.6 Added $args parameter.
*
* @param array $attendees Array of event attendees.
* @param int $post_id Event post ID.
* @param array $args List of arguments to filter by.
*/
return apply_filters( 'tribe_tickets_event_attendees', $attendees, $post_id, $args );
}
/**
* Returns all the attendees for an event with filtered by arguments. Queries all registered providers.
*
* @since 4.10.6
* @since 5.5.9 Move the logic to `get_attendees_by_args` and use the method to return the attendees.
*
* @static
*
* @param int $post_id ID of parent "event" post.
* @param array $args {
* List of arguments to filter attendees by.
*
* @type boolean $return_total_found Whether to return total_found count in an array along with list of
* attendees. Default is off.
* @type int $page Page number of attendees to return. Default is page 1.
* @type int $per_page How many attendees to return per page. Default is all.
* @type string $fields Which fields to return. Default is all.
* @type array $by List of ORM->by() filters to use. [what=>[args...]], [what=>arg], or
* [[what,args...]] format.
* @type array $where_multi List of ORM->where_multi() filters to use. [[what,args...]] format.
* }
*
* @return array List of attendees and total_found.
*/
public static function get_event_attendees_by_args( $post_id, $args = [] ) {
$attendee_data = [
'total_found' => 0,
'attendees' => [],
];
if ( empty( $post_id ) ) {
return $attendee_data;
}
return self::get_attendees_by_args( $args, $post_id );
}
/**
* Returns all the attendees with filtered by arguments. Queries all registered providers.
*
* @since 5.5.9
*
* @static
*
* @param int $post_id ID of parent "event" post.
* @param array $args {
* List of arguments to filter attendees by.
*
* @type boolean $return_total_found Whether to return total_found count in an array along with list of
* attendees. Default is off.
* @type int $page Page number of attendees to return. Default is page 1.
* @type int $per_page How many attendees to return per page. Default is all.
* @type string $fields Which fields to return. Default is all.
* @type array $by List of ORM->by() filters to use. [what=>[args...]], [what=>arg], or
* [[what,args...]] format.
* @type array $where_multi List of ORM->where_multi() filters to use. [[what,args...]] format.
* }
*
* @return array List of attendees and total_found.
*/
public static function get_attendees_by_args( $args = [], $post_id = 0 ) {
$attendee_data = [
'total_found' => 0,
'attendees' => [],
];
$provider = 'default';
if ( ! empty( $args['provider'] ) ) {
$provider = $args['provider'];
}
/** @var Tribe__Tickets__Attendee_Repository $repository */
$repository = tribe_attendees( $provider );
// Limit by post ID.
if ( ! empty( $post_id ) ) {
$repository->by( 'event', $post_id );
}
self::pass_args_to_repository( $repository, $args );
if ( ! empty( $args['return_total_found'] ) ) {
$repository->set_found_rows( true );
}
$attendee_posts = $repository->all();
if ( ! empty( $args['return_total_found'] ) ) {
$attendee_data['total_found'] = $repository->found();
}
$attendee_data['attendees'] = self::get_attendees_from_modules( $attendee_posts, $post_id );
return $attendee_data;
}
/**
* Pass arguments to repository object with dynamic support for by() and where_multi().
*
* @since 4.10.6
*
* @param Tribe__Repository $repository Repository object.
* @param array $args {
* List of arguments to filter by.
*
* @type int $page Page number of results to return. Default is page 1.
* @type int $per_page How many results to return per page. Default is all.
* @type string $fields Which fields to return. Default is all.
* @type array $by List of ORM->by() filters to use. [what=>[args...]], [what=>arg], or
* [[what,args...]] format.
* @type array $where_multi List of ORM->where_multi() filters to use. [[what,args...]] format.
* }
*/
protected static function pass_args_to_repository( $repository, $args ) {
// Only return specific fields.
if ( ! empty( $args['fields'] ) ) {
$repository->fields( $args['fields'] );
}
// Handle filtering.
if ( ! empty( $args['by'] ) ) {
foreach ( $args['by'] as $by => $by_args ) {
$by_args = (array) $by_args;
if ( is_string( $by ) ) {
array_unshift( $by_args, $by );
}
call_user_func_array( [ $repository, 'by' ], $by_args );
}
}
// Handle post__in.
if ( ! empty( $args['in'] ) ) {
$repository->in( (array) $args['in'] );
}
// Handle post__not_in.
if ( ! empty( $args['not_in'] ) ) {
$repository->not_in( (array) $args['not_in'] );
}
// Handle multi filtering.
if ( ! empty( $args['where_multi'] ) ) {
foreach ( $args['where_multi'] as $where_multi_args ) {
call_user_func_array( [ $repository, 'where_multi' ], $where_multi_args );
}
}
// Set current page.
if ( ! empty( $args['page'] ) ) {
$repository->page( absint( $args['page'] ) );
}
// Limit results per page.
if ( ! empty( $args['per_page'] ) ) {
$repository->per_page( absint( $args['per_page'] ) );
}
if ( ! empty( $args['orderby'] ) ) {
$repository->order_by( strval( $args['orderby'] ) );
}
if ( ! empty( $args['order'] ) ) {
$repository->order( strval( $args['order'] ) );
}
}
/**
* Get attendee data for attendees from the associated modules.
*
* @since 4.10.6
*
* @param array $attendees Attendee objects or IDs.
* @param int $post_id Parent post ID.
*
* @return array The attendee data for attendees.
*/
public static function get_attendees_from_modules( $attendees, $post_id = 0 ) {
$attendees_from_modules = [];
foreach ( $attendees as $attendee ) {
/** @var Tribe__Tickets__Tickets $provider */
$provider = tribe_tickets_get_ticket_provider( $attendee );
// Could be `false`, such as ticket for a disabled commerce provider.
if ( empty( $provider ) ) {
continue;
}
$attendee_data = $provider->get_attendee( $attendee, $post_id );
if ( ! $attendee_data ) {
continue;
}
// Set the `ticket_exists` flag on attendees if the ticket they are associated with does not exist.
$attendee_data['ticket_exists'] = ! empty( $attendee_data['product_id'] ) && get_post( $attendee_data['product_id'] );
// Set the ticket type from the ticket oject, if possible.
$attendee_data['ticket_type'] = 'default';
if ( isset( $attendee_data['event_id'], $attendee_data['product_id'] )
&& $ticket = $provider->get_ticket( $attendee_data['event_id'], $attendee_data['product_id'] ) ) {
$attendee_data['ticket_type'] = $ticket->type();
}
$attendees_from_modules[] = $attendee_data;
}
return $attendees_from_modules;
}
/**
* Get attendee data for attendees from the current module.
*
* @since 4.10.6
*
* @param array $attendees Attendee objects or IDs.
* @param int $post_id Parent post ID.
*
* @return array The attendee data for attendees.
*/
public function get_attendees_from_module( $attendees, $post_id = 0 ) {
$attendees_from_module = [];
foreach ( $attendees as $attendee ) {
$attendee_data = $this->get_attendee( $attendee, $post_id );
if ( ! $attendee_data ) {
continue;
}
// Set the `ticket_exists` flag on attendees if the ticket they are associated with does not exist.
$attendee_data['ticket_exists'] = ! empty( $attendee_data['product_id'] ) && get_post( $attendee_data['product_id'] );
$attendees_from_module[] = $attendee_data;
}
return $attendees_from_module;
}
/**
* Get attendee data for attendee.
*
* @since 4.10.6
*
* @param WP_Post|int $attendee Attendee object or ID.
* @param int $post_id Parent post ID.
*
* @return array|false The attendee data or false if the ticket is invalid.
*/
public function get_attendee( $attendee, $post_id = 0 ) {
return false;
}
/**
* Returns an array of attendees for the specified event, in relation to
* this ticketing provider.
*
* @param int $post_id ID of parent "event" post
* @return array
*/
public function get_attendees_array( $post_id ) {
return $this->get_attendees_by_post_id( $post_id );
}
/**
* Returns total count of attendees for the specified event, in relation to
* this ticketing provider.
*
* @since 4.10.6
*
* @param int $post_id ID of parent "event" post
*
* @return int Total count of attendees.
*/
public function get_attendees_count( $post_id ) {
/** @var Tribe__Tickets__Attendee_Repository $repository */
$repository = tribe_attendees( $this->orm_provider );
return $repository->by( 'event', $post_id )->found();
}
/**
* Returns total count of attendees for the specified event, in relation to
* this ticketing provider.
*
* @since 4.10.6
*
* @param int $post_id ID of parent "event" post.
* @param int $user_id ID of user.
*
* @return int Total count of attendees.
*/
public function get_attendees_count_by_user( $post_id, $user_id ) {
/** @var Tribe__Tickets__Attendee_Repository $repository */
$repository = tribe_attendees( $this->orm_provider );
return $repository->by( 'event', $post_id )->by( 'user', $user_id )->found();
}
/**
* Returns the total number of attendees for an event (regardless of provider).
*
* @param int $post_id ID of parent "event" post.
* @param array $args {
* List of arguments to filter attendees by.
*
* @type array $by List of ORM->by() filters to use. [what=>[args...]], [what=>arg], or
* [[what,args...]] format.
* @type array $where_multi List of ORM->where_multi() filters to use. [[what,args...]] format.
* }
*
* @return int Total count of attendees.
*/
public static function get_event_attendees_count( $post_id, $args = [] ) {
// Post ID is required.
if ( empty( $post_id ) ) {
return 0;
}
/** @var Tribe__Cache $cache */
$cache = tribe( 'cache' );
$key = __METHOD__ . '-' . $post_id;
if ( empty( $args ) && isset( $cache[ $key ] ) ) {
return $cache[ $key ];
}
$provider = 'default';
if ( ! empty( $args['provider'] ) ) {
$provider = $args['provider'];
}
/** @var Tribe__Tickets__Attendee_Repository $repository */
$repository = tribe_attendees( $provider );
$repository->by( 'event', $post_id );
self::pass_args_to_repository( $repository, $args );
$found = $repository->found();
if ( empty( $args ) ) {
$cache[ $key ] = $found;
}
return $found;
}
/**
* Returns all tickets for an event (all providers are queried for this information).
*
* @since 5.8.0 Added the `$context` parameter.
*
* @param int $post_id ID of parent "event" post
* @param string|null $context The context of the request.
*
* @return array
*/
public static function get_all_event_tickets( $post_id, string $context = null ) {
/** @var Tribe__Cache $cache */
$cache = tribe( 'cache' );
$key = __METHOD__ . '-' . $post_id;
if ( is_array( $cache[ $key ] ) ) {
return $cache[ $key ];
}
$tickets = [];
$modules = self::modules();
foreach ( $modules as $class => $module ) {
$obj = call_user_func( [ $class, 'get_instance' ] );
$provider_tickets = $obj->get_tickets( $post_id, $context );
if ( is_array( $provider_tickets ) && ! empty( $provider_tickets ) ) {
$tickets[] = $provider_tickets;
}
}
$tickets = empty( $tickets ) ? [] : call_user_func_array( 'array_merge', $tickets );
$cache[ $key ] = $tickets;
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 object|int $possible_ticket
*
* @return WP_Post|false
*/
public static function find_matching_event( $possible_ticket ) {
foreach ( self::modules() as $class => $module ) {
$obj = call_user_func( [ $class, 'get_instance' ] );
$event = $obj->get_event_for_ticket( $possible_ticket );
if ( $event instanceof WP_Post ) {
return $event;
}
}
return false;
}
/**
* Returns the sum of all checked-in attendees for an event. Queries all registered providers.
*
* @static
*
* @param int $post_id ID of parent "event" post
* @return mixed
*/
final public static function get_event_checkedin_attendees_count( $post_id ) {
/** @var Tribe__Tickets__Attendee_Repository $repository */
$repository = tribe_attendees();
return $repository->by( 'event', $post_id )->by( 'checkedin', true )->found();
}
// 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( [ $class, 'get_instance' ] );
if ( method_exists( $provider, 'supports_global_stock' ) && $provider->supports_global_stock() ) {
return true;
}
}
return false;
}
/**
* Echos the class for the <tr> in the tickets list admin
*/
protected function tr_class() {
echo 'ticket_advanced_' . sanitize_html_class( $this->class_name );
}
/**
* Generates a set of radio buttons listing the available global stock mode options.
*
* @param string (empty string) $current_option
* @return string
*/
protected function global_stock_mode_selector( $current_option = '' ) {
$output = "<fieldset id='ticket_global_stock' class='input_block' >";
$output .= "<legend class='ticket_form_label'>Capacity:</legend>";
// Default to using own stock unless the user explicitly specifies otherwise (important
// to avoid assuming global stock mode if global stock is enabled/disabled accidentally etc)
if ( empty( $current_option ) ) {
$current_option = Tribe__Tickets__Global_Stock::OWN_STOCK_MODE;
}
foreach ( $this->global_stock_mode_options() as $identifier => $name ) {
$output .= '<label for="' . esc_attr( $identifier ) . '" class="ticket_field"><input type="radio" id="' . esc_attr( $identifier ) . '" class=" name="ticket_global_stock" value="' . esc_attr( $identifier ) . '" ' . selected( $identifier === $current_option ) . '> ' . esc_html( $name ) . " </label>\n";
}
return $output;
}
/**
* Returns an array of standard stock mode options that can be reused by implementations.
*
* Format is: ['identifier' => 'Localized name', ... ]
*
* @return array
*/
protected function global_stock_mode_options() {
return [
Tribe__Tickets__Global_Stock::GLOBAL_STOCK_MODE => __( 'Shared capacity with other tickets', 'event-tickets' ),
Tribe__Tickets__Global_Stock::OWN_STOCK_MODE => __( 'Set capacity for this ticket only', 'event-tickets' ),
];
}
/**
* Get JS localize data for ticket options.
*
* @since 4.11.0.1
*
* @return array JS localize data for ticket options.
*/
public static function get_asset_localize_data_for_ticket_options() {
$availability_check_interval = MINUTE_IN_SECONDS * 1000;
/*
* Prevent availability check AJAX errors because we don't currently
* run our AJAX hook if this conditional fails.
*
* A temporary fix for ET-730 which will need to be followed up with.
*
* @see \Tribe__Tickets__Editor__Provider::register()
* @see \Tribe__Tickets__Editor__Blocks__Tickets::hook()
*/
if ( ! tribe( 'editor' )->should_load_blocks() ) {
$availability_check_interval = 0;
}
/**
* Allow filtering how often tickets availability is checked (in milliseconds).
*
* @since 4.11.0
*
* @param int $availability_check_interval How often to check availability for tickets (in milliseconds).
*/
$availability_check_interval = apply_filters( 'tribe_tickets_availability_check_interval', $availability_check_interval );
$post_id = get_the_ID();
if ( empty( $post_id ) && get_queried_object() instanceof WP_Post ) {
$post_id = get_queried_object_id();
}
return [
'post_id' => $post_id,
'ajaxurl' => admin_url( 'admin-ajax.php', ( is_ssl() ? 'https' : 'http' ) ),
'availability_check_interval' => $availability_check_interval,
];
}
/**
* Get JS localize data for currencies.
*
* @since 4.11.0.1
*
* @return array JS localize data for currencies.
*/
public static function get_asset_localize_data_for_currencies() {
/** @var Tribe__Tickets__Commerce__Currency $currency */
$currency = tribe( 'tickets.commerce.currency' );
$currencies = $currency->get_currency_config_for_providers();
return [
'formatting' => json_encode( $currencies ),
];
}
/**
* Get JS localize data for cart/checkout URLs.
*
* @since 4.11.0.1
*
* @return array JS localize data for cart/checkout URLs.
*/
public static function get_asset_localize_data_for_cart_checkout_urls() {
$cart_urls = [];
$checkout_urls = [];
/**
* Allow providers to add their own checkout URL to the localized list.
*
* @since 4.11.0
*
* @param array $checkout_urls An array to add urls to.
*/
$checkout_urls = apply_filters( 'tribe_tickets_checkout_urls', $checkout_urls );
/**
* Allow providers to add their own cart URL to the localized list.
*
* @since 4.11.0
*
* @param array $cart_urls An array to add urls to.
*/
$cart_urls = apply_filters( 'tribe_tickets_cart_urls', $cart_urls );
return [
'cart' => $cart_urls,
'checkout' => $checkout_urls,
];
}
/**
* Get RSVP and Ticket counts for an event if tickets are currently available.
*
* @param int $post_id ID of parent "event" post
*
* @return array
*/
public static function get_ticket_counts( $post_id ) {
// if no post id return empty array
if ( empty( $post_id ) ) {
return [];
}
$tickets = self::get_all_event_tickets( $post_id );
// if no tickets or rsvp return empty array
if ( ! $tickets ) {
return [];
}
/**
* This order is important so that tickets overwrite RSVP on
* the Buy Now Button on the front-end
*/
$types['rsvp'] = [
'count' => 0,
'stock' => 0,
'unlimited' => 0,
'available' => 0,
];
$types['tickets'] = [
'count' => 0, // count of ticket types currently for sale
'stock' => 0, // current stock of tickets available for sale
'global' => 0, // numeric boolean if tickets share global stock
'unlimited' => 0, // numeric boolean if any ticket has unlimited stock
'available' => 0,
];
/** @var Tribe__Tickets__Ticket_Object $ticket */
foreach ( $tickets as $ticket ) {
// If a ticket is not current for sale do not count it
if ( ! tribe_events_ticket_is_on_sale( $ticket ) ) {
continue;
}
if ( 'Tribe__Tickets__RSVP' === $ticket->provider_class ) {
$types = static::process_rsvp_counts( $ticket, $types );
continue;
}
// we have a ticket type so increasing the ticket count.
$types['tickets']['count'] ++;
$global_stock_mode = $ticket->global_stock_mode();
// Handle tickets with unlimited capacity.
if ( empty( $global_stock_mode ) ) {
if ( ! $ticket->manage_stock() || -1 === $ticket->capacity ) {
$types['tickets']['unlimited'] ++;
$types['tickets']['available'] ++;
}
continue;
}
// for individual tickets.
if ( Tribe__Tickets__Global_Stock::OWN_STOCK_MODE === $global_stock_mode ) {
$stock_level = $ticket->available();
$types['tickets']['stock'] += $stock_level;
$types['tickets']['available'] += $stock_level;
if ( ! $ticket->manage_stock() || -1 === $ticket->capacity ) {
$types['tickets']['unlimited'] ++;
}
continue;
}
// flag if we have any shared capacity tickets.
if ( Tribe__Tickets__Global_Stock::GLOBAL_STOCK_MODE === $global_stock_mode ) {
$types['tickets']['global'] = 1;
continue;
}
$stock_level = Tribe__Tickets__Global_Stock::CAPPED_STOCK_MODE === $global_stock_mode ? $ticket->global_stock_cap() : $ticket->available();
// whether the stock level is negative because it represents unlimited stock (`-1`)
// or because it's oversold we normalize to `0` for the sake of displaying
$stock_level = max( 0, (int) $stock_level );
$types['tickets']['stock'] += $stock_level;
// If current availability is unlimited (available = -1) and the ticket has stock, set it to 0.
if ( $types['tickets']['available'] < 0 && 0 !== $types['tickets']['stock'] ) {
$types['tickets']['available'] = 0;
}
}
/*
* The Tickets that should be displayed on a post might not all be directly attached to this post.
* We'll use the Ticket information to get the ID of the post the Ticket is attached to and
* then get the Global Stock for that post.
*/
$ticket_post_ids = array_reduce( $tickets, static function ( array $post_ids, Tribe__Tickets__Ticket_Object $ticket ) {
$ticket_event_id = (int) $ticket->get_event_id();
if ( ! in_array( $ticket_event_id, $post_ids, true ) ) {
$post_ids[] = $ticket_event_id;
}
return $post_ids;
}, [] );
foreach ( $ticket_post_ids as $ticket_post_id ) {
$global_stock = new Tribe__Tickets__Global_Stock( $ticket_post_id );
$global_stock = $global_stock->is_enabled() ? $global_stock->get_stock_level() : 0;
$types['tickets']['available'] += $global_stock;
// If there's at least one ticket with shared capacity add the global stock to the stock total.
if ( ! self::tickets_own_stock( $ticket_post_id ) ) {
$types['tickets']['stock'] += $global_stock;
}
}
/**
* Allow filtering of ticket counts by event.
*
* @since 5.5.10
*
* @param array $types An array of ticket types.
* @param int $post_id The event post ID.
*/
return apply_filters( 'tec_tickets_get_ticket_counts', $types, $post_id );
}
/**
* Process RSVP counts.
*
* @since 5.5.9
*
* @param Tribe__Tickets__Ticket_Object $rsvp RSVP ticket object.
* @param array $types Array of ticket types.
*
* @return array
*/
public static function process_rsvp_counts( $rsvp, $types ) {
$types['rsvp']['count'] ++;
$types['rsvp']['stock'] += $rsvp->stock;
if ( 0 !== $types['rsvp']['stock'] ) {
$types['rsvp']['available'] ++;
}
if ( ! $rsvp->manage_stock() ) {
$types['rsvp']['unlimited'] ++;
$types['rsvp']['available'] ++;
}
return $types;
}
/**
* Returns if the all the tickets for an event
* have own stock
*
* @param int $post_id ID of parent "event" post
* @return bool
*/
public static function tickets_own_stock( $post_id ) {
$tickets = self::get_all_event_tickets( $post_id );
// if no tickets or rsvp return false
if ( ! $tickets ) {
return false;
}
foreach ( $tickets as $ticket ) {
// if ticket and not RSVP
if ( 'Tribe__Tickets__RSVP' !== $ticket->provider_class ) {
$global_stock_mode = $ticket->global_stock_mode();
if ( Tribe__Tickets__Global_Stock::OWN_STOCK_MODE !== $global_stock_mode ) {
return false;
}
}
}
return true;
}
/**
* Tries to make data about global stock levels and global stock-enabled ticket objects
* available to frontend scripts.
*
* @deprecated 4.11.3
*
* @param array $tickets
*/
public static function add_frontend_stock_data( array $tickets ) {
_deprecated_function( __METHOD__, '4.11.3', 'tribe( "tickets.editor.blocks.tickets" )->assets()' );
if ( is_admin() ) {
return;
}
/*
* 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 ( ! empty( self::$frontend_script_enqueued ) ) {
return;
}
$plugin = Tribe__Tickets__Main::instance();
wp_register_script(
'wp-util-not-in-footer',
includes_url( '/js/wp-util.js' ),
[ 'jquery', 'underscore' ],
false,
false
);
wp_enqueue_script( 'wp-util-not-in-footer' );
// Check whether we use v1 or v2. We need to update this when we deprecate tickets v1.
$tickets_js = tribe_tickets_new_views_is_enabled() ? 'v2/tickets-block.js' : 'tickets-block.js';
tec_asset(
$plugin,
'tribe-tickets-block',
$tickets_js,
[
'jquery',
'tribe-common',
'jquery-ui-datepicker',
'wp-util-not-in-footer',
'wp-i18n',
'wp-hooks',
],
null,
[
'type' => 'js',
'groups' => [ 'tribe-tickets-block-assets' ],
'localize' => [
[
'name' => 'TribeTicketOptions',
'data' => [ __CLASS__, 'get_asset_localize_data_for_ticket_options' ],
],
[
'name' => 'TribeCurrency',
'data' => [ __CLASS__, 'get_asset_localize_data_for_currencies' ],
],
[
'name' => 'TribeCartEndpoint',
'data' => [
'url' => tribe_tickets_rest_url( '/cart/' ),
],
],
[
'name' => 'TribeMessages',
'data' => self::set_messages(),
],
[
'name' => 'TribeTicketsURLs',
'data' => [ __CLASS__, 'get_asset_localize_data_for_cart_checkout_urls' ],
],
],
]
);
tribe_asset_enqueue_group( 'tribe-tickets-block-assets' );
self::$frontend_script_enqueued = true;
}
/**
* Takes any global stock data and makes it available via a wp_localize_script() call.
*
* @deprecated 4.11.0
*/
public static function enqueue_frontend_stock_data() {
$data = [
'tickets' => [],
'events' => [],
];
foreach ( self::$frontend_ticket_data as $ticket ) {
$post = $ticket->get_event();
if ( empty( $post ) ) {
continue;
}
$post_id = $post->ID;
$global_stock = new Tribe__Tickets__Global_Stock( $post_id );
$stock_mode = $ticket->global_stock_mode();
$ticket_data = [
'event_id' => $post_id,
'mode' => $stock_mode,
'cap' => $ticket->capacity(),
];
if ( $ticket->managing_stock() ) {
$ticket_data['stock'] = $ticket->available();
}
$data['events'][ $post_id ] = [
'stock' => $global_stock->get_stock_level(),
];
$data['tickets'][ $ticket->ID ] = $ticket_data;
}
wp_localize_script( 'tribe-tickets-block', 'tribe_tickets_stock_data', $data );
}
/**
* Returns the array of active modules/providers.
*
* @static
*
* @return array $active_modules {
* Ticket modules
*
* @param mixed $module A class which extends this one, acts as a ticket provider.
* }
*/
public static function modules() {
/**
* Filters the available tickets modules
*
* @param array $active_modules {
* Ticket modules
*
* @param mixed $module A class which extends this one, acts as a ticket provider.
* }
*/
return apply_filters( 'tribe_tickets_get_modules', self::$active_modules );
}
/**
* Returns the class name of the default module/provider.
*
* @since 4.6
* @since 5.19.1 Update default to Tickets Commerce.
*
* @return string
*/
public static function get_default_module() {
$modules = array_keys( self::modules() );
if ( 1 === count( $modules ) ) {
// There's only one, just return it.
Tribe__Tickets__Tickets::$default_module = array_shift( $modules );
} else {
// Remove RSVP and PayPal tickets for this part
unset(
$modules[ array_search( 'Tribe__Tickets__RSVP', $modules ) ]
);
if ( ! empty( $modules ) ) {
// We just return the first, so we don't show favoritism
$sliced = array_slice( $modules, 0, 1 );
self::$default_module = reset( $sliced );
} else {
// Set Tickets Commerce as the default module.
self::$default_module = 'TEC\\Tickets\\Commerce\\Module';
}
}
/**
* Filters the default commerce module (provider)
*
* @since 4.6
*
* @param string default ticket module class name
* @param array array of ticket module class names
*/
return apply_filters( 'tribe_tickets_get_default_module', self::$default_module, $modules );
}
/**
* Get all the tickets for an event. Queries all active modules/providers.
*
* @static
*
* @param int $post_id ID of parent "event" post
*
* @return array
*/
final public static function get_event_tickets( $post_id ) {
$tickets = [];
foreach ( self::modules() as $class => $module ) {
/** @var Tribe__Tickets__Tickets $obj */
$obj = call_user_func( [ $class, 'get_instance' ] );
$provider_tickets = $obj->get_tickets( $post_id );
if ( ! empty( $provider_tickets ) && is_array( $provider_tickets ) ) {
$tickets[] = $provider_tickets;
}
}
return ! empty( $tickets ) ? call_user_func_array( 'array_merge', $tickets ) : [];
}
/**
* Generates and returns the email template for a group of attendees.
*
* @param array $tickets
* @return string
*/
public function generate_tickets_email_content( $tickets ) {
return tribe_tickets_get_template_part( 'tickets/email', null, [ 'tickets' => $tickets ], false );
}
/**
* Send RSVPs/tickets email for attendees.
*
* @since 5.0.3
*
* @param array $attendees List of attendees.
* @param array $args {
* The list of arguments to use for sending ticket emails.
*
* @type string $subject The email subject.
* @type string $content The email content.
* @type string $from_name The name to send tickets from.
* @type string $from_email The email to send tickets from.
* @type array|string $headers The list of headers to send.
* @type array $attachments The list of attachments to send.
* @type string $provider The provider slug (rsvp, tpp, woo, edd).
* @type int $post_id The post/event ID to send the emails for.
* @type string|int $order_id The order ID to send the emails for.
* }
*
* @return int The number of emails sent successfully.
*/
public function send_tickets_email_for_attendees( $attendees, $args = [] ) {
$unique_attendees = [];
// Collect the unique emails for attendees.
foreach ( $attendees as $attendee ) {
// If the attendee data is not provided, get it from the provider.
if ( ! is_array( $attendee ) ) {
$attendee = $this->get_attendee( $attendee );
}
// If invalid attendee is set, skip it.
if ( ! $attendee ) {
continue;
}
if ( ! isset( $unique_attendees[ $attendee['holder_email'] ] ) ) {
$unique_attendees[ $attendee['holder_email'] ] = [];
}
$unique_attendees[ $attendee['holder_email'] ][] = $attendee;
}
$emails_sent = 0;
// Handle purchaser emails.
if ( ! empty( $args['send_purchaser_all'] ) ) {
// Get the purchaser email from the first attendee.
$first_attendee = reset( $attendees );
$purchaser_email = $first_attendee['purchaser_email'];
// Make sure purchaser gets a list of all of the attendee tickets.
$unique_attendees[ $purchaser_email ] = $attendees;
}
// Send an email with all RSVPs/tickets for each unique attendee.
foreach ( $unique_attendees as $to => $tickets ) {
$emails_sent += (int) $this->send_tickets_email_for_attendee( $to, $tickets, $args );
}
return 0 < $emails_sent;
}
/**
* Send RSVPs/tickets email for an attendee.
*
* @since 5.0.3
* @since 5.5.10 Adjusted the method to use the new Tickets Emails Handler.
* @since 5.6.0 Reverted the methods back to before 5.5.10, new Tickets Emails Handler via filters.
*
* @param string $to The email to send the tickets to.
* @param array $tickets The list of tickets to send.
* @param array $args {
* The list of arguments to use for sending ticket emails.
*
* @type string $subject The email subject.
* @type string $content The email content.
* @type string $from_name The name to send tickets from.
* @type string $from_email The email to send tickets from.
* @type array|string $headers The list of headers to send.
* @type array $attachments The list of attachments to send.
* @type string $provider The provider slug (rsvp, tpp, woo, edd).
* @type int $post_id The post/event ID to send the emails for.
* @type string|int $order_id The order ID to send the emails for.
* }
*
* @return bool Whether email was sent to attendees.
*/
public function send_tickets_email_for_attendee( $to, $tickets, $args = [] ) {
/**
* Allows the short-circuiting of the sending of emails to the Attendees.
*
* @since 5.6.0
*
* @param null|mixed $pre Determine if we should continue.
* @param string $to The email to send the tickets to.
* @param array $tickets The list of tickets to send.
* @param array $args The list of arguments to use for sending ticket emails.
* @param static $module Instance of the Tickets Module.
*/
$pre = apply_filters( 'tec_tickets_send_tickets_email_for_attendee_pre', null, $to, $tickets, $args, $this );
if ( null !== $pre ) {
return $pre;
}
// If no tickets to send for, do not send email.
if ( empty( $tickets ) ) {
return false;
}
$defaults = [
'subject' => '',
'content' => '',
'from_name' => '',
'from_email' => '',
'headers' => [],
'attachments' => [],
'provider' => 'ticket',
'post_id' => 0,
'order_id' => '',
'send_callback' => 'wp_mail',
];
// Set up the default arguments.
$args = array_merge( $defaults, $args );
$subject = trim( (string) $args['subject'] );
$content = trim( (string) $args['content'] );
$from_name = trim( (string) $args['from_name'] );
$from_email = trim( (string) $args['from_email'] );
$headers = $args['headers'];
$attachments = $args['attachments'];
$provider = $args['provider'];
$post_id = $args['post_id'];
$order_id = $args['order_id'];
$send_callback = $args['send_callback'];
// If invalid send callback, do not send the email.
if ( ! is_callable( $send_callback ) ) {
return false;
}
// Set up default content.
if ( empty( $content ) ) {
$content = $this->generate_tickets_email_content( $tickets );
}
// Set up default subject.
if ( empty( $subject ) ) {
$site_name = stripslashes_deep( html_entity_decode( get_bloginfo( 'name' ), ENT_QUOTES ) );
$is_rsvp = 'rsvp' === $provider;
$singular = $is_rsvp
? tribe_get_rsvp_label_singular( 'RSVP email send' )
: tribe_get_ticket_label_singular_lowercase( 'ticket email send' );
$plural = $is_rsvp
? tribe_get_rsvp_label_plural( 'RSVPs email send' )
: tribe_get_ticket_label_plural_lowercase( 'tickets email send' );
// translators: %1$s: The singular of "RSVP" or "ticket", %2$s: The plural of "RSVPs" or "tickets", %3$s: The site name.
$subject_string = _nx( 'Your %1$s from %3$s', 'Your %2$s from %3$s', count( $tickets ), 'The default RSVP/ticket email subject', 'event-tickets' );
$subject = sprintf(
$subject_string,
$singular,
$plural,
$site_name
);
}
// Enforce headers array.
if ( ! is_array( $headers ) ) {
$headers = explode( "\r\n", $headers );
}
// Add From name/email to headers if no headers set yet and we have a valid From email address.
if ( empty( $headers ) && ! empty( $from_name ) && ! empty( $from_email ) && is_email( $from_email ) ) {
$from_email = filter_var( $from_email, FILTER_SANITIZE_EMAIL );
$headers[] = sprintf(
'From: %1$s <%2$s>',
stripcslashes( $from_name ),
$from_email
);
$headers[] = sprintf(
'Reply-To: %s',
$from_email
);
}
// Enforce text/html content type header.
if ( ! in_array( 'Content-type: text/html', $headers, true ) || ! in_array( 'Content-type: text/html; charset=utf-8', $headers, true ) ) {
$headers[] = 'Content-type: text/html; charset=utf-8';
}
/**
* Allow filtering the email recipient for a provider. Backwards compatible with previous provider filter.
*
* The dynamic portion of the filter hook, `$provider`, refers to the provider slug (rsvp, tpp, woo, edd).
*
* @deprecated 5.0.3 Use the tribe_tickets_ticket_email_recipient filter instead.
*
* @since 4.7.6
*
* @since 5.0.3
*
* @param string $to The email to send to.
* @param int $post_id The post/event ID to send the email for.
* @param string|int $order_id The order ID to send the email for.
* @param array $tickets The list of tickets to send.
*/
$to = apply_filters( "tribe_{$provider}_email_recipient", $to, $post_id, $order_id, $tickets );
/**
* Allow filtering the email recipient.
*
* @since 5.0.3
*
* @param string $to The email to send to.
* @param int $post_id The post/event ID to send the email for.
* @param string|int $order_id The order ID to send the email for.
* @param array $tickets The list of tickets to send.
* @param string $provider The provider slug.
* @param array $args The full list of ticket email arguments as sent to the function.
*/
$to = apply_filters( 'tribe_tickets_ticket_email_recipient', $to, $post_id, $order_id, $tickets, $provider, $args );
// If no email set or invalid email is used, do not send the email.
if ( empty( $to ) || ! is_email( $to ) ) {
return false;
}
/**
* Allow filtering the email subject for a provider. Backwards compatible with previous provider filter.
*
* The dynamic portion of the filter hook, `$provider`, refers to the provider slug (rsvp, tpp, woo, edd).
*
* @deprecated 5.0.3 Use the tribe_tickets_ticket_email_subject filter instead.
*
* @since 4.7.6
*
* @param string $subject The email subject.
* @param int $post_id The post/event ID to send the email for.
* @param string|int $order_id The order ID to send the email for.
* @param array $tickets The list of tickets to send.
*/
$subject = apply_filters( "tribe_{$provider}_email_subject", $subject, $post_id, $order_id, $tickets );
/**
* Allow filtering the email subject.
*
* @since 5.0.3
*
* @param string $subject The email subject.
* @param int $post_id The post/event ID to send the email for.
* @param string|int $order_id The order ID to send the email for.
* @param array $tickets The list of tickets to send.
* @param string $provider The provider slug.
* @param array $args The full list of ticket email arguments as sent to the function.
*/
$subject = apply_filters( 'tribe_tickets_ticket_email_subject', $subject, $post_id, $order_id, $tickets, $provider, $args );
// If no subject to use for the email, do not send the email.
if ( empty( $subject ) ) {
return false;
}
// Generate the email content for the tickets.
$content = $this->generate_tickets_email_content( $tickets );
/**
* Allow filtering the email content for a provider. Backwards compatible with previous provider filter.
*
* The dynamic portion of the filter hook, `$provider`, refers to the provider slug (rsvp, tpp, woo, edd).
*
* @deprecated 5.0.3 Use the tribe_tickets_ticket_email_content filter instead.
*
* @since 4.7.6
*
* @param array $content The content to send the email with.
* @param int $post_id The post/event ID to send the email for.
* @param string|int $order_id The order ID to send the email for.
* @param array $tickets The list of tickets to send.
*/
$content = apply_filters( "tribe_{$provider}_email_content", $content, $post_id, $order_id, $tickets );
/**
* Allow filtering the email content.
*
* @since 5.0.3
*
* @param array $content The content to send the email with.
* @param int $post_id The post/event ID to send the email for.
* @param string|int $order_id The order ID to send the email for.
* @param array $tickets The list of tickets to send.
* @param string $provider The provider slug.
* @param array $args The full list of ticket email arguments as sent to the function.
*/
$content = apply_filters( 'tribe_tickets_ticket_email_content', $content, $post_id, $order_id, $tickets, $provider, $args );
// If no content to use for the email, do not send the email.
if ( empty( $content ) ) {
return false;
}
/**
* Allow filtering the email headers for a provider. Backwards compatible with previous provider filter.
*
* The dynamic portion of the filter hook, `$provider`, refers to the provider slug (rsvp, tpp, woo, edd).
*
* @deprecated 5.0.3 Use the tribe_tickets_ticket_email_headers filter instead.
*
* @since 4.7.6
*
* @param array $headers List of email headers.
* @param int $post_id The post/event ID to send the email for.
* @param string|int $order_id The order ID to send the email for.
* @param array $tickets The list of tickets to send.
*/
$headers = apply_filters( "tribe_{$provider}_email_headers", $headers, $post_id, $order_id, $tickets );
/**
* Allow filtering the email headers.
*
* @since 5.0.3
*
* @param array $headers List of email headers.
* @param int $post_id The post/event ID to send the email for.
* @param string|int $order_id The order ID to send the email for.
* @param array $tickets The list of tickets to send.
* @param string $provider The provider slug.
* @param array $args The full list of ticket email arguments as sent to the function.
*/
$headers = apply_filters( 'tribe_tickets_ticket_email_headers', $headers, $post_id, $order_id, $tickets, $provider, $args );
/**
* Allow filtering the email attachments for a provider. Backwards compatible with previous provider filter.
*
* The dynamic portion of the filter hook, `$provider`, refers to the provider slug (rsvp, tpp, woo, edd).
*
* @deprecated 5.0.3 Use the tribe_tickets_ticket_email_attachments filter instead.
*
* @since 4.7.6
*
* @param array $attachments The list of attachments to send.
* @param int $post_id The post/event ID to send the email for.
* @param string|int $order_id The order ID to send the email for.
* @param array $tickets The list of tickets to send.
*/
$attachments = apply_filters( "tribe_{$provider}_email_attachments", $attachments, $post_id, $order_id, $tickets );
/**
* Allow filtering the email attachments.
*
* @since 5.0.3
*
* @param array $attachments The list of attachments to send.
* @param int $post_id The post/event ID to send the email for.
* @param string|int $order_id The order ID to send the email for.
* @param array $tickets The list of tickets to send.
* @param string $provider The provider slug.
* @param array $args The full list of ticket email arguments as sent to the function.
*/
$attachments = apply_filters( 'tribe_tickets_ticket_email_attachments', $attachments, $post_id, $order_id, $tickets, $provider, $args );
$sent = $send_callback( $to, $subject, $content, $headers, $attachments );
// Handle marking the attendee ticket email as being sent.
if ( $sent ) {
// Mark attendee ticket email as being sent for each attendee ticket.
foreach ( $tickets as $attendee ) {
$this->update_ticket_sent_counter( $attendee['attendee_id'] );
$this->update_attendee_activity_log(
$attendee['attendee_id'],
[
'type' => 'email',
'name' => $attendee['holder_name'],
'email' => $attendee['holder_email'],
]
);
}
}
return $sent;
}
/**
* Update the email sent counter for attendee by increasing it +1.
*
* @since 5.1.0
*
* @param int $attendee_id The attendee ID.
*/
public function update_ticket_sent_counter( $attendee_id ) {
$prev_val = (int) get_post_meta( $attendee_id, $this->attendee_ticket_sent, true );
update_post_meta( $attendee_id, $this->attendee_ticket_sent, $prev_val + 1 );
}
/**
* Update the attendee activity log data.
*
* @param int $attendee_id Attendee ID.
* @param array $data Data that needs to be logged.
*
* @since 5.1.0
*/
public function update_attendee_activity_log( $attendee_id, $data = [] ) {
$activity = get_post_meta( $attendee_id, $this->attendee_activity_log, true );
if ( ! is_array( $activity ) ) {
$activity = [];
}
/**
* Filter the activity log data for attendee.
*
* @since 5.1.0
*
* @param array $data Activity data.
* @param int $attendee_id Attendee ID.
*/
$data = apply_filters( 'tribe_tickets_attendee_activity_log_data', $data, $attendee_id );
$data['time'] = time();
$activity[] = $data;
update_post_meta( $attendee_id, $this->attendee_activity_log, $activity );
}
/**
* Gets the view from the plugin's folder, or from the user's theme if found.
*
* @param string $template
* @return mixed|void
*/
public function getTemplateHierarchy( $template ) {
if ( substr( $template, - 4 ) != '.php' ) {
$template .= '.php';
}
if ( $theme_file = locate_template( [ 'tribe-events/' . $template ] ) ) {
$file = $theme_file;
} else {
$file = $this->plugin_path . 'src/views/' . $template;
}
return apply_filters( 'tribe_events_tickets_template_' . $template, $file );
}
/**
* Formats the cost based on the provider of a ticket of an event.
*
* @param float|string $cost
* @param int $post_id
*
* @return string
*/
public function maybe_format_event_cost( $cost, $post_id ) {
$tickets = self::get_all_event_tickets( $post_id );
// If $cost isn't a number or there are no tickets, no filter needed.
if ( ! is_numeric( $cost ) || empty( $tickets ) ) {
return $cost;
}
$currency = tribe( 'tickets.commerce.currency' );
// We will convert to the format of the first ticket's provider class.
return $currency->get_formatted_currency( $cost, null, $tickets[0]->provider_class );
}
/**
* 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 $post_id
* @return array
*/
public function get_ticket_prices( array $prices, $post_id ) {
// If value already exists, do not override it. Return it.
if ( ! empty( $prices ) ) {
return $prices;
}
// Iterate through all tickets from all providers
foreach ( self::get_all_event_tickets( $post_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).
if ( isset( $ticket->price ) && is_numeric( $ticket->price ) ) {
$prices[] = $ticket->price;
}
}
return $prices;
}
/**
* Filter past tickets from showing up in cost range.
*
* @since 5.1.5
*
* @param array $costs List of ticket costs.
* @param int $post_id Target Event's ID.
* @param string $meta Meta key name.
* @param bool $single determines if the requested meta should be a single item or an array of items.
*
* @return array The list of ticket costs with past tickets excluded possibly.
*/
public function exclude_past_tickets_from_cost_range( $costs, $post_id, $meta, $single ) {
if ( '_EventCost' != $meta || $single || empty( $costs ) ) {
return $costs;
}
/**
* Allow filtering of whether to exclude past tickets in the event cost range.
*
* @since 5.1.4
*
* @param bool $exclude_past_tickets Whether to exclude past tickets in the event cost range.
* @param array $costs Which costs are going to be displayed.
* @param int $post_id Which Event/Post we are dealign with.
*/
$exclude_past_tickets = apply_filters( 'event_tickets_exclude_past_tickets_from_cost_range', false, $costs, $post_id );
if ( ! $exclude_past_tickets ) {
return $costs;
}
$tickets = self::get_all_event_tickets( $post_id );
$wp_timezone = Tribe__Timezones::wp_timezone_string();
if ( Tribe__Timezones::is_utc_offset( $wp_timezone ) ) {
$wp_timezone = Tribe__Timezones::generate_timezone_string_from_utc_offset( $wp_timezone );
}
$timezone = new DateTimeZone( $wp_timezone );
foreach ( $tickets as $ticket ) {
$now = Tribe__Date_Utils::build_date_object( 'now', $timezone );
$start_date = Tribe__Date_Utils::build_date_object( $ticket->start_date . ' ' . $ticket->start_time, $timezone );
$end_date = Tribe__Date_Utils::build_date_object( $ticket->end_date . ' ' . $ticket->end_time, $timezone );
// If the ticket has not yet become available for sale or has already ended.
if ( $now < $start_date || $end_date < $now ) {
// Try to find the ticket price in the list of costs.
$key = array_search( $ticket->price, $costs );
// Remove the value from the list of costs if we found it.
if ( false !== $key ) {
unset( $costs[ $key ] );
}
continue;
}
}
return $costs;
}
/**
* 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;
}
$post_id = get_post_meta( $attendee_id, $attendee_event_key, true );
if ( empty( $post_id ) ) {
return false;
}
return (int) $post_id;
}
/**
* Given a valid order ID, returns a single event ID it relates to or else boolean false
* if it cannot be determined.
*
* @see Use tribe_tickets_get_event_ids() to return an array of all event ids for an order
*
* @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( [
'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 ) ) {
return (string) $attendee_order_key;
}
switch ( $this->class_name ) {
case 'Tribe__Events__Tickets__Woo__Main':
return '_tribe_wooticket_order';
case 'Tribe__Events__Tickets__EDD__Main':
return '_tribe_eddticket_order';
case 'Tribe__Events__Tickets__Shopp__Main':
return '_tribe_shoppticket_order';
case 'Tribe__Events__Tickets__Wpec__Main':
return '_tribe_wpecticket_order';
default:
return '';
}
}
/**
* 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_object ) ) {
return (string) $attendee_object;
}
switch ( $this->class_name ) {
case 'Tribe__Events__Tickets__Woo__Main':
return 'tribe_wooticket';
case 'Tribe__Events__Tickets__EDD__Main':
return 'tribe_eddticket';
case 'Tribe__Events__Tickets__Shopp__Main':
return 'tribe_shoppticket';
case 'Tribe__Events__Tickets__Wpec__Main':
return 'tribe_wpecticket';
default:
return '';
}
}
/**
* Given a ticket provider, get its Attendee Optout Meta Key from its class property (or constant if legacy).
*
* @since 4.12.3
*
* @param self|string $provider Examples: 'Tribe__Tickets_Plus__Commerce__WooCommerce__Main', 'woo', 'rsvp', etc.
*
* @return string The meta key or an empty string if passed an invalid or inactive ticket provider.
*/
public static function get_attendee_optout_key( $provider ) {
$provider = static::get_ticket_provider_instance( $provider );
if ( empty( $provider ) ) {
return '';
}
/**
* Not all classes have this static method.
*
* @see \Tribe__Tickets__Commerce__PayPal__Main::get_key() Does have this static method.
*/
if ( method_exists( $provider, 'get_key' ) ) {
$key = $provider::get_key( 'attendee_optout_key' );
}
if ( ! empty( $key ) ) {
return $key;
}
if ( ! empty( $provider->attendee_optout_key ) ) {
return $provider->attendee_optout_key;
}
$key = constant( "{$provider->class_name}::ATTENDEE_OPTOUT_KEY" );
return (string) $key;
}
/**
* 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 ) ) {
return (string) $attendee_event_key;
}
switch ( $this->class_name ) {
case 'Tribe__Events__Tickets__Woo__Main':
return '_tribe_wooticket_event';
case 'Tribe__Events__Tickets__EDD__Main':
return '_tribe_eddticket_event';
case 'Tribe__Events__Tickets__Shopp__Main':
return '_tribe_shoppticket_event';
case 'Tribe__Events__Tickets__Wpec__Main':
return '_tribe_wpecticket_event';
default:
return '';
}
}
/**
* Process the attendee meta into an array with value, slug, and label
*
* @param int $product_id
* @param array $meta
* @return array
*/
public function process_attendee_meta( $product_id, $meta ) {
$meta_values = [];
if ( ! class_exists( 'Tribe__Tickets_Plus__Main' ) ) {
return $meta_values;
}
$meta_field_objects = Tribe__Tickets_Plus__Main::instance()->meta()->get_meta_fields_by_ticket( $product_id );
foreach ( $meta_field_objects as $field ) {
$value = null;
if ( 'checkbox' === $field->type ) {
$field_prefix = $field->slug . '_';
$value = [];
foreach ( $meta as $full_key => $check_value ) {
if ( 0 === strpos( $full_key, $field_prefix ) ) {
$short_key = substr( $full_key, strlen( $field_prefix ) );
$value[ $short_key ] = $check_value;
}
}
if ( empty( $value ) ) {
$value = null;
}
} elseif ( isset( $meta[ $field->slug ] ) ) {
$value = $meta[ $field->slug ];
}
$meta_values[ $field->slug ] = [
'slug' => $field->slug,
'label' => $field->label,
'value' => $value,
];
}
return $meta_values;
}
/**
* Returns the meta key used to link ticket types with the base event.
*
* Subclasses can override this if they use a key other than 'event_key'
* for this purpose.
*
* @since 5.14.0 Removed check for static property. All static properties were removed over 2 major versions ago.
*
* @return string
*/
public function get_event_key() {
return $this->event_key;
}
/**
* 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;
}
$collection_availability_slug = 'available';
$tickets_available = false;
$slugs = [];
/** @var Tribe__Tickets__Ticket_Object $ticket */
foreach ( $tickets as $ticket ) {
$availability_slug = $ticket->availability_slug( $datetime );
// 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 = [ 'available' ];
break;
}
// track unique availability slugs
if ( ! in_array( $availability_slug, $slugs, true ) ) {
$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
*
* @param string Availability slug
* @param array Collection of tickets
* @param 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
* @since 4.10.9 Use customizable ticket name functions.
*
* @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 = esc_html( sprintf( __( '%s are not available as this %s has passed.', 'event-tickets' ), tribe_get_ticket_label_plural( 'unavailable_past_tribe_events' ), $events_label_singular_lowercase ) );
} elseif ( 'availability-future' === $availability_slug ) {
/**
* Allows inclusion of ticket start sale date in unavailability message
*
* @since 4.7.6
*
* @param bool $display_date
*/
$display_date = apply_filters( 'tribe_tickets_unvailable_message_date', $display_date = true );
/**
* Allows inclusion of ticket start sale time in unavailability message
*
* @since 4.7.6
*
* @param bool $display_time
*/
$display_time = apply_filters( 'tribe_tickets_unvailable_message_time', $display_time = false );
// build message
if ( $display_date ) {
$start_sale_date = '';
$start_sale_time = '';
foreach ( $tickets as $ticket ) {
// get the earliest start sale date
if ( '' == $start_sale_date || $ticket->start_date < $start_sale_date ) {
$start_sale_date = $ticket->start_date;
$start_sale_time = $ticket->start_time;
}
}
$date_format = tribe_get_date_format( true );
$start_sale_date = Tribe__Date_Utils::build_date_object( $start_sale_date )->format_i18n( $date_format );
$message = esc_html( sprintf( __( '%s will be available on ', 'event-tickets' ), tribe_get_ticket_label_plural( 'unavailable_future_display_date' ) ) );
$message .= $start_sale_date;
if ( $display_time ) {
$time_format = tribe_get_time_format();
$start_sale_time = Tribe__Date_Utils::build_date_object( $start_sale_time )->format_i18n( $time_format );
$message .= __( ' at ', 'event_tickets' ) . $start_sale_time;
}
} else {
$message = esc_html( sprintf( __( '%s are not yet available', 'event-tickets' ), tribe_get_ticket_label_plural( 'unavailable_future_without_date' ) ) );
}
} elseif ( 'availability-past' === $availability_slug ) {
$message = esc_html( sprintf( __( '%s are no longer available.', 'event-tickets' ), tribe_get_ticket_label_plural( 'unavailable_past' ) ) );
} elseif ( 'availability-mixed' === $availability_slug ) {
$message = esc_html( sprintf( __( 'There are no %s available at this time.', 'event-tickets' ), tribe_get_ticket_label_plural( 'unavailable_mixed' ) ) );
}
/**
* Filters the unavailability message for a ticket collection
*
* @param string Unavailability message
* @param 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 ID of parent "event" post (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();
}
$unavailable_tickets = self::$currently_unavailable_tickets;
$existing_tickets = ! empty( $unavailable_tickets[ (int) $post_id ] )
? $unavailable_tickets[ (int) $post_id ]
: [];
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, display 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, true ) ) {
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 = '<div class="tickets-unavailable">'
. $this->get_tickets_unavailable_message( self::$currently_unavailable_tickets[ $post_id ] )
. '</div>';
/**
* 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, $this->attendee_user_id, (int) $user_id );
}
/**
* Prints the front-end tickets form in the post content.
*
* @param string $content The post original content.
*
* @return string The updated content.
*/
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".
*
* @since 5.0.1 Bail if $post->ID is zero, such as from BuddyPress' "Activity" page.
*
* @return bool
*/
protected function should_inject_ticket_form_into_post_content() {
global $post;
// Prevents firing more then it needs to 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 ( ! is_singular() ) {
return false;
}
// Bail if this isn't a post for some reason.
// Empty check is for BuddyPress having a WP Post with ID of zero.
if (
! $post instanceof WP_Post
|| empty( $post->ID )
) {
return false;
}
// Bail if this isn't a supported post type.
if ( ! tribe_tickets_post_type_enabled( $post->post_type ) ) {
return false;
}
// User is currently viewing/editing their existing tickets.
if ( Tribe__Tickets__Tickets_View::instance()->is_edit_page() ) {
return false;
}
// Bail if a tribe_events post because those post types are handled with a different hook.
if (
class_exists( 'Tribe__Events__Main' )
&& defined( 'Tribe__Events__Main::POSTTYPE' )
&& Tribe__Events__Main::POSTTYPE === $post->post_type
) {
return false;
}
// Bail if there aren't any tickets.
$tickets = $this->get_tickets( $post->ID );
if ( empty( $tickets ) ) {
return false;
}
/** @var Tribe__Editor $editor */
$editor = tribe( 'editor' );
// Blocks and ticket templates merged - bail if we should be seeing blocks.
if ( has_blocks( $post->ID ) ) {
return false;
}
return true;
}
/**
* Indicates if the user must be logged in in order to obtain tickets.
*
* @since 4.7
*
* @return bool
*/
public function login_required() {
$requirements = (array) tribe_get_option( 'ticket-authentication-requirements', [] );
return in_array( 'event-tickets_all', $requirements, true );
}
/**
* Provides a URL that can be used to direct users to the login form.
*
* @param int $post_id - the ID of the post to redirect to
*
* @return string
*/
public static function get_login_url( $post_id = null ) {
if ( is_null( $post_id ) ) {
$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 );
}
/**
* Adds or updates the capacity for a ticket.
*
* @since 4.7
*
* @param WP_Post|int $ticket
* @param array $raw_data
* @param string $save_type
*/
public function update_capacity( $ticket, $data, $save_type ) {
if ( empty( $data ) ) {
return;
}
// set the default capacity to that of the event, if set, or to unlimited
$default_capacity = (int) Tribe__Utils__Array::get( $data, 'event_capacity', -1 );
// Fetch capacity field, if we don't have it use default (defined above)
$data['capacity'] = trim( Tribe__Utils__Array::get( $data, 'capacity', $default_capacity ) );
// If empty we need to modify to the default
if ( '' === $data['capacity'] ) {
$data['capacity'] = $default_capacity;
}
// The only available value lower than zero is -1 which is unlimited
if ( 0 > $data['capacity'] ) {
$data['capacity'] = -1;
}
// Fetch the stock if defined, otherwise use Capacity field
$data['stock'] = trim( Tribe__Utils__Array::get( $data, 'stock', $data['capacity'] ) );
// If empty we need to modify to what every capacity was
if ( '' === $data['stock'] ) {
$data['stock'] = $data['capacity'];
}
// The only available value lower than zero is -1 which is unlimited
if ( 0 > $data['stock'] ) {
$data['stock'] = -1;
}
if ( -1 !== $data['capacity'] ) {
if ( 'update' === $save_type ) {
/** @var Tribe__Tickets__Tickets_Handler $tickets_handler */
$tickets_handler = tribe( 'tickets.handler' );
$totals = $tickets_handler->get_ticket_totals( $ticket->ID );
$data['stock'] -= $totals['pending'] + $totals['sold'];
}
update_post_meta( $ticket->ID, '_manage_stock', 'yes' );
update_post_meta( $ticket->ID, '_stock', $data['stock'] );
} else {
// unlimited stock
delete_post_meta( $ticket->ID, '_stock_status' );
update_post_meta( $ticket->ID, '_manage_stock', 'no' );
delete_post_meta( $ticket->ID, '_stock' );
delete_post_meta( $ticket->ID, Tribe__Tickets__Global_Stock::TICKET_STOCK_MODE );
delete_post_meta( $ticket->ID, Tribe__Tickets__Global_Stock::TICKET_STOCK_CAP );
}
tribe_tickets_update_capacity( $ticket, $data['capacity'] );
}
/**
* @param bool $operation_did_complete
*/
protected function maybe_update_attendees_cache( $operation_did_complete ) {
if ( $operation_did_complete && ! empty( $_POST['event_ID'] ) ) {
$this->clear_attendees_cache( $_POST['event_ID'] );
}
}
/**
* Clears the attendees cache for a given post
*
* @param int|WP_Post $post_id The parent post or ID
*
* @return bool Was the operation successful?
*/
public function clear_attendees_cache( $post_id ) {
if ( $post_id instanceof WP_Post ) {
$post_id = $post_id->ID;
}
/** @var Tribe__Post_Transient $post_transient */
$post_transient = tribe( 'post-transient' );
$cache_key = (int) $post_id;
return $post_transient->delete( $cache_key, self::ATTENDEES_CACHE );
}
/**
* Clears the ticket cache for a given ticket ID.
*
* @since 5.1.0
*
* @param int|object $ticket_id The ticket ID.
*/
public function clear_ticket_cache( $ticket_id ) {
if ( is_object( $ticket_id ) ) {
$ticket_id = $ticket_id->ID;
}
$methods = [
'Tribe__Tickets__Ticket_Object::is_in_stock',
'Tribe__Tickets__Ticket_Object::inventory',
'Tribe__Tickets__Ticket_Object::available',
'Tribe__Tickets__Ticket_Object::capacity',
];
/** @var Tribe__Cache $cache */
$cache = tribe( 'cache' );
foreach ( $methods as $method ) {
$key = $method . '-' . $ticket_id;
unset( $cache[ $key ] );
}
}
/**
* 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
*/
public function get_ticket_form_hook() {
if ( $this instanceof 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 $tickets_obj 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 $tickets_obj 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;
}
/**
* Creates a duplicate ticket based on post id and ticket id.
*
* @since 5.2.3
*
* @param int $post_id ID of parent "event" post.
* @param int $ticket_id ID of ticket to duplicate.
*
* @return int|boolean $duplicate_ticket_id New ticket ID or false, if unable to create duplicate.
*/
public function duplicate_ticket( $post_id, $ticket_id ) {
// Get ticket data.
$ticket = $this->get_ticket( $post_id, $ticket_id );
if ( ! $ticket instanceof Tribe__Tickets__Ticket_Object ) {
return false;
}
// Create data for duplicate ticket.
$data = [
'ticket_name' => $ticket->name . __( '(copy)', 'event-tickets' ),
'ticket_description' => $ticket->description,
'ticket_price' => $ticket->price,
'ticket_show_description' => $ticket->show_description,
'ticket_start_date' => $ticket->start_date,
'ticket_start_time' => $ticket->start_time,
'ticket_end_date' => $ticket->end_date,
'ticket_end_time' => $ticket->end_time,
'tribe-ticket' => [
'capacity' => $ticket->capacity(),
'mode' => $ticket->global_stock_mode(),
]
];
// Add the ticket.
$duplicate_ticket_id = $this->ticket_add( $post_id, $data );
if ( ! $duplicate_ticket_id ) {
return false;
}
// Copy ticket meta from old ticket to new ticket.
$ignore_meta = [
'_sku',
'_tribe_ticket_manual_updated',
'_wp_old_slug',
'total_sales',
];
$ticket_meta = get_post_meta( $ticket->ID );
if ( $ticket_meta ) {
foreach ( $ticket_meta as $meta_key => $meta_values ) {
// Skip meta we don't want to duplicate.
if ( false !== strpos( $meta_key, '_tec_tc_ticket_status_count' ) ){
continue;
}
if ( in_array( $meta_key, $ignore_meta ) ) {
continue;
}
// Delete duplicate tickets meta before adding new meta.
delete_post_meta( $duplicate_ticket_id, $meta_key );
foreach ( $meta_values as $meta_value ) {
// Maybe convert to object, in case meta is serialized.
$meta_value_obj = maybe_unserialize( $meta_value );
add_post_meta( $duplicate_ticket_id, $meta_key, $meta_value_obj );
}
}
}
// Update SKU of new ticket to remove '(COPY)'.
$old_sku = get_post_meta( $duplicate_ticket_id, '_sku', true );
$new_sku = str_replace( '(COPY)', '', $old_sku );
update_post_meta( $duplicate_ticket_id, '_sku', $new_sku, $old_sku );
return $duplicate_ticket_id;
}
/**
* Clones a ticket to a new post.
*
* @since 5.6.3
*
* @param int $original_post_id ID of the original "event" post.
* @param int $new_post_id ID of the new "event" post.
* @param int $ticket_id ID of ticket to duplicate.
*
* @return int|boolean $duplicate_ticket_id New ticket ID or false, if unable to create duplicate.
*/
public function clone_ticket_to_new_post( $original_post_id, $new_post_id, $ticket_id ) {
// Get ticket data.
$ticket = $this->get_ticket( $original_post_id, $ticket_id );
if ( ! $ticket instanceof Tribe__Tickets__Ticket_Object ) {
return false;
}
// Create data for duplicate ticket.
$data = [
'ticket_name' => $ticket->name,
'ticket_description' => $ticket->description,
'ticket_price' => $ticket->price,
'ticket_show_description' => $ticket->show_description,
'ticket_start_date' => $ticket->start_date,
'ticket_start_time' => $ticket->start_time,
'ticket_end_date' => $ticket->end_date,
'ticket_end_time' => $ticket->end_time,
'tribe-ticket' => [
'capacity' => $ticket->capacity(),
'mode' => $ticket->global_stock_mode(),
]
];
// Add the ticket.
$duplicate_ticket_id = $this->ticket_add( $new_post_id, $data );
if ( ! $duplicate_ticket_id ) {
return false;
}
return $duplicate_ticket_id;
}
/**
* Creates a ticket object and calls the child save_ticket function
*
* @param int $post_id ID of parent "event" post
* @param array $data Raw post data
*
* @return boolean
*/
public function ticket_add( $post_id, $data ) {
$ticket = new Tribe__Tickets__Ticket_Object();
$ticket->ID = isset( $data['ticket_id'] ) ? absint( $data['ticket_id'] ) : null;
$update = ! empty( $ticket->ID );
$ticket->name = isset( $data['ticket_name'] ) ? esc_html( $data['ticket_name'] ) : null;
$ticket->description = isset( $data['ticket_description'] ) ? wp_kses_post( $data['ticket_description'] ) : '';
$ticket->price = ! empty( $data['ticket_price'] ) ? filter_var( trim( $data['ticket_price'] ), FILTER_SANITIZE_NUMBER_FLOAT, FILTER_FLAG_ALLOW_FRACTION | FILTER_FLAG_ALLOW_THOUSAND ) : 0;
$ticket->show_description = isset( $data['ticket_show_description'] ) && tribe_is_truthy( $data['ticket_show_description'] ) ? 'yes' : 'no';
$ticket->provider_class = $this->class_name;
$ticket->start_date = null;
$ticket->end_date = null;
$ticket->menu_order = isset( $data['ticket_menu_order'] ) ? intval( $data['ticket_menu_order'] ) : null;
/** @var Tribe__Tickets__Tickets_Handler $tickets_handler */
$tickets_handler = tribe( 'tickets.handler' );
$tickets_handler->toggle_manual_update_flag( true );
if ( ! empty( $ticket->price ) ) {
// remove non-money characters
$ticket->price = preg_replace( '/[^0-9\.\,]/Uis', '', $ticket->price );
}
if ( ! empty( $data['ticket_start_date'] ) ) {
$start_datetime = Tribe__Date_Utils::maybe_format_from_datepicker( $data['ticket_start_date'] );
if ( ! empty( $data['ticket_start_time'] ) ) {
$start_datetime .= ' ' . $data['ticket_start_time'];
$ticket->start_time = date( Tribe__Date_Utils::DBTIMEFORMAT, strtotime( ( $start_datetime ) ) );
}
$ticket->start_date = date( Tribe__Date_Utils::DBDATEFORMAT, strtotime( $start_datetime ) );
}
if ( ! empty( $data['ticket_end_date'] ) ) {
$end_datetime = Tribe__Date_Utils::maybe_format_from_datepicker( $data['ticket_end_date'] );
if ( ! empty( $data['ticket_end_time'] ) ) {
$end_datetime .= ' ' . $data['ticket_end_time'];
$ticket->end_time = date( Tribe__Date_Utils::DBTIMEFORMAT, strtotime( ( $end_datetime ) ) );
}
$ticket->end_date = date( Tribe__Date_Utils::DBDATEFORMAT, strtotime( $end_datetime ) );
}
/**
* Fired before a ticket is saved.
*
* @since 5.27.0
*
* @param int $post_id The ticket parent post ID.
* @param Tribe__Tickets__Ticket_Object $ticket The ticket that is being saved.
* @param array $data The ticket data that is being saved.
*/
do_action( 'tec_tickets_ticket_pre_save', (int) $post_id, $ticket, $data );
if ( $update ) {
update_post_meta( $ticket->ID, '_type', $data['ticket_type'] ?? 'default' );
}
// Pass the control to the child object.
$save_ticket = $this->save_ticket( $post_id, $ticket, $data );
// Set the ticket type before the module saves the ticket.
$ticket_type = 'default';
if ( $update ) {
$ticket_type = get_post_meta( $ticket->ID, '_type', true ) ?: 'default';
}
$new_ticket_type = ! empty( $data['ticket_type'] ) ? $data['ticket_type'] : $ticket_type;
update_post_meta( $ticket->ID, '_type', $new_ticket_type );
/**
* Fired once a ticket has been created and added to a post.
*
* @since 5.8.0 Add the `$update` parameter.
*
* @param int $post_id The ticket parent post ID.
* @param Tribe__Tickets__Ticket_Object $ticket The ticket that was just added.
* @param array $raw_data The ticket data that was used to save.
* @param string $class The Commerce engine class name.
* @param bool $update Whether the ticket is being updated or created.
*/
do_action( 'tribe_tickets_ticket_add', $post_id, $ticket, $data, __CLASS__, $update );
if ( $update ) {
/**
* Fired once a ticket has been updated.
*
* @since 5.8.0
*
* @param int $post_id The ticket parent post ID.
* @param Tribe__Tickets__Ticket_Object $ticket The ticket that was just added.
* @param array $raw_data The ticket data that was used to save.
* @param string $class The Commerce engine class name.
*/
do_action( 'tec_tickets_ticket_update', $post_id, $ticket, $data, __CLASS__ );
} else {
/**
* Fired once a ticket has been created.
*
* @since 5.8.0
*
* @param int $post_id The ticket parent post ID.
* @param Tribe__Tickets__Ticket_Object $ticket The ticket that was just added.
* @param array $raw_data The ticket data that was used to save.
* @param string $class The Commerce engine class name.
*/
do_action( 'tec_tickets_ticket_add', $post_id, $ticket, $data, __CLASS__ );
}
$tickets_handler->toggle_manual_update_flag( false );
$post = get_post( $post_id );
// If ticket start date is not set, set it to the post date.
if ( empty( $data['ticket_start_date'] ) ) {
$date = strtotime( $post->post_date );
$date = date( 'Y-m-d 00:00:00', $date );
update_post_meta( $ticket->ID, $tickets_handler->key_start_date, $date );
}
/*
* If the ticket end date has not been set and we have an event,
* set the ticket end date to the event start date.
*/
if ( empty( $data['ticket_end_date'] ) && 'tribe_events' === $post->post_type ) {
$event_start = get_post_meta( $post_id, '_EventStartDate', true );
update_post_meta( $ticket->ID, $tickets_handler->key_end_date, $event_start );
}
/** @var Tribe__Tickets__Version $version */
$version = tribe( 'tickets.version' );
$version->update( $ticket->ID );
/**
* Fires once a ticket's data has been saved.
*
* @since 5.20.0
*
* @param int $ticket_id The ticket id that was just added.
* @param int $post_id The ticket parent post ID.
* @param array $raw_data The ticket data that was used to save.
* @param string $class The Commerce engine class name.
*/
do_action( 'tec_tickets_ticket_upserted', $ticket->ID, $post_id, $data, __CLASS__ );
$this->clear_ticket_cache_for_post( $post_id );
return $save_ticket;
}
/**
* Get the saved or default ticket provider, if active.
*
* Will return False if there is a saved provider that is currently not active.
* Example: If provider is WooCommerce Ticket but ETP is inactive, will return False.
*
* @see get_event_ticket_provider_object()
*
* @since 4.7
* @since 4.12.3 Now returning false if the provider is not active.
*
* @param int $event_id The post ID of the event to which the ticket is attached.
*
* @return string|false The ticket object class name, or false if not active.
*/
public static function get_event_ticket_provider( $event_id = null ) {
$provider = static::get_event_ticket_provider_object( $event_id );
if ( empty( $provider ) ) {
return false;
}
return $provider->class_name;
}
/**
* Given a post ID, get the active providers used for RSVP(s)/ticket(s).
*
* @see get_ticket_provider_instance()
*
* @since 5.1.1
*
* @param int $post_id The post ID of the post/event to which RSVP(s)/ticket(s) are attached.
* @param bool $return_instances Whether to return instances, otherwise it will return class name strings.
*
* @return string[]|self[] Instances or names of provider classes for RSVP(s)/ticket(s) attached to the post/event.
*/
public static function get_active_providers_for_post( $post_id, $return_instances = false ) {
$all_active_modules = array_keys( self::modules() );
$active_providers = [];
// Determine which providers have tickets for this event.
foreach ( $all_active_modules as $module ) {
$provider = self::get_ticket_provider_instance( $module );
// Skip this provider if the instance couldn't be set up.
if ( ! $provider ) {
continue;
}
// Get the tickets for this event on this provider, if any.
$tickets_orm = tribe_tickets( $provider->orm_provider );
$tickets_orm->by( 'event', $post_id );
if ( 0 < $tickets_orm->found() ) {
$provider_class = $provider->class_name;
// Check whether to return the provider class names.
if ( ! $return_instances ) {
$provider = $provider_class;
}
$active_providers[ $provider_class ] = $provider;
}
}
return $active_providers;
}
/**
* Given a post ID, get the instance of the saved or default ticket provider class.
*
* Will return False if there is a saved provider that is currently not active.
* Example: If provider is WooCommerce Ticket but ETP is inactive, will return False.
*
* @see get_ticket_provider_instance()
*
* @since 4.12.3
*
* @param int $post_id The post ID of the event to which the ticket is attached.
*
* @return self|false Instance of child class (if confirmed active) or False if provider is not active.
*/
public static function get_event_ticket_provider_object( $post_id = null ) {
/** @var Tribe__Tickets__Tickets_Handler $tickets_handler */
$tickets_handler = tribe( 'tickets.handler' );
// 'Tribe__Tickets__RSVP' unless filtered.
$provider = self::get_default_module();
// If post ID is set and a value has been saved.
if ( ! empty( $post_id ) ) {
$saved = get_post_meta( $post_id, $tickets_handler->key_provider_field, true );
if ( ! empty( $saved ) ) {
$provider = $saved;
}
}
return static::get_ticket_provider_instance( $provider );
}
/**
* Given a provider string (class module name or slug), get its class instance if an active module.
*
* @param self|string $provider Examples: 'Tribe__Tickets_Plus__Commerce__WooCommerce__Main', 'woo', 'rsvp', etc.
*
* @return self|false Instance of child class (if confirmed active) or False if provider is not active.
*/
public static function get_ticket_provider_instance( $provider ) {
$is_provider_active = tribe_tickets_is_provider_active( $provider );
if ( empty( $is_provider_active ) ) {
return false;
}
if ( $provider instanceof self ) {
return $provider;
}
/** @var Tribe__Tickets__Status__Manager $status */
$status = tribe( 'tickets.status' );
$provider = $status->get_provider_class_from_slug( $provider );
$instance = tribe_get_class_instance( $provider );
if ( ! $instance instanceof self ) {
return false;
}
return $instance;
}
/**
* Get currency symbol
*
* @since 4.7.1
*
* @return string
*/
public function get_currency() {
/**
* Default currency value for Tickets.
*
* @since 4.7.1
*
* @return string
*/
return (string) apply_filters( 'tribe_tickets_default_currency', 'USD' );
}
/**
* Returns all the tickets currently in the users cart.
*
* @since 4.9
*
* @param array $tickets
*
* @return array
*/
public function get_tickets_in_cart( $tickets ) {
return $tickets;
}
/**
* Return whether we're currently on the checkout page for this Merchant.
*
* @since 4.9
*
* @return bool
*/
public function is_checkout_page() {
return false;
}
/**
* If tickets exist in the cart for which we don't have meta info,
* redirect to the meta collection screen.
*
* @since 4.9
* @since 5.0.2 Correct provider attendee object.
*
* @param string|null $redirect URL to redirect to.
* @param null|int $post_id Post ID for cart.
*/
public function maybe_redirect_to_attendees_registration_screen( $redirect = null, $post_id = null ) {
// Bail if the meta storage class doesn't exist
if ( ! class_exists( 'Tribe__Tickets_Plus__Meta__Storage' ) ) {
return;
}
if ( ! class_exists( 'Tribe__Tickets_Plus__Main' ) ) {
return;
}
// They're submitting RSVPs, do not include them for now
if ( ! empty( $_POST['tribe_tickets_rsvp_submission'] ) ) {
return;
}
/**
* This Try/Catch is present to deal with a problem on Autoloading from version 5.1.0 ET+ with ET 5.0.3.
*
* @todo Needs to be revised once proper autoloading rules are done for Common, ET and ET+.
*/
try {
/** @var \Tribe__Tickets__Attendee_Registration__Main $attendee_registration */
$attendee_registration = tribe( 'tickets.attendee_registration' );
} catch( RuntimeException $error ) {
return;
}
if (
$attendee_registration->is_on_page()
|| $attendee_registration->is_cart_rest()
|| $attendee_registration->is_using_shortcode()
) {
return;
}
// Return if not trying to access the checkout page
if ( ! $this->is_checkout_page() ) {
return;
}
$q_provider = tribe_get_request_var( 'provider', false );
// Provider to use the attendee object.
if (
static::class === $q_provider
|| empty( $q_provider )
) {
$q_provider = $this->attendee_object;
}
/**
* Filter to add/remove tickets from the global cart
*
* @since 4.9
* @since 4.11.0 Added $q_provider to allow context of current provider.
*
* @param array $tickets_in_cart The array containing the cart elements. Format array( 'ticket_id' => 'quantity' ).
* @param string $q_provider Current ticket provider.
*/
$tickets_in_cart = apply_filters( 'tribe_tickets_tickets_in_cart', [], $q_provider );
// Bail if there are no tickets
if ( empty( $tickets_in_cart ) ) {
return;
}
/** @var Tribe__Tickets_Plus__Meta $meta */
$meta = tribe( 'tickets-plus.meta' );
$cart_has_meta = true;
// If the method exists (latest ET+ version), run it.
if ( method_exists( $meta, 'cart_has_meta' ) ) {
$cart_has_meta = $meta->cart_has_meta( $tickets_in_cart );
}
// There are no meta fields on the cart tickets.
if ( ! $cart_has_meta ) {
return;
}
/** @var \Tribe__Tickets_Plus__Meta__Contents $meta_contents */
$meta_contents = tribe( 'tickets-plus.meta.contents' );
$up_to_date = $meta_contents->is_stored_meta_up_to_date( $tickets_in_cart );
// There are no updates to perform on ticket meta.
if ( $up_to_date ) {
return;
}
$url = $attendee_registration->get_url();
if ( ! empty( $q_provider ) ) {
$provider_slug = tribe_tickets_get_provider_query_slug();
$url = add_query_arg( $provider_slug, $q_provider, $url );
}
if ( ! empty( $redirect ) ) {
$storage = new Tribe__Tickets_Plus__Meta__Storage();
$key = $storage->store_temporary_data( $redirect );
/** @var \Tribe__Tickets__Commerce__PayPal__Main $commerce_paypal */
$commerce_paypal = tribe( 'tickets.commerce.paypal' );
$url = add_query_arg(
[
'event_tickets_redirect_to' => $key,
'provider' => $commerce_paypal->attendee_object,
],
$url
);
}
// Pass post ID to URL if set.
if ( null !== $post_id ) {
$url = add_query_arg( 'tribe_tickets_post_id', $post_id, $url );
}
wp_safe_redirect( $url );
exit;
}
/**
* Get list of tickets in cart for a specific provider.
*
* @since 5.0.3
*
* @param null|string|false $provider The provider slug or false if no provider, leave as null to detect from page.
*
* @return array List of tickets in cart for the provider.
*/
public static function get_tickets_in_cart_for_provider( $provider = null ) {
if ( null === $provider ) {
$provider = tribe_get_request_var( 'provider', false );
}
/**
* Filter to add/remove tickets from the global cart.
*
* @since 4.9
* @since 4.11.0 Added $provider to allow context of current provider.
*
* @param array $tickets_in_cart The array containing the cart elements. Format array( 'ticket_id' => 'quantity' ).
* @param string|false $provider Current ticket provider or false if not set.
*/
return (array) apply_filters( 'tribe_tickets_tickets_in_cart', [], $provider );
}
/**
* Generates the security code that will be used for printed tickets and QR codes.
*
* @since 4.7
*
* @param string $attendee_id The attendee ID or another string to based the security code off of.
*
* @return string The generated security code.
*/
public function generate_security_code( $attendee_id ) {
return substr( md5( wp_rand() . '_' . $attendee_id ), 0, 10 );
}
/**
* Create an attendee for the Commerce provider from a ticket.
*
* @since 5.1.0
*
* @param Tribe__Tickets__Ticket_Object|int $ticket Ticket object or ID to create the attendee for.
* @param array $attendee_data Attendee data to create from.
*
* @return WP_Post|false The new post object or false if unsuccessful.
*/
public function create_attendee( $ticket, $attendee_data ) {
// Get the ticket object from the ID.
if ( is_numeric( $ticket ) ) {
$ticket = $this->get_ticket( 0, (int) $ticket );
}
// If the ticket is not valid, stop creating the attendee.
if ( ! $ticket instanceof Tribe__Tickets__Ticket_Object ) {
return false;
}
/** @var Tribe__Tickets__Attendee_Repository $orm */
$orm = tribe_attendees( $this->orm_provider );
try {
return $orm->create_attendee_for_ticket( $ticket, $attendee_data );
} catch ( Tribe__Repository__Usage_Error $e ) {
do_action( 'tribe_log', 'error', __CLASS__, [ 'message' => $e->getMessage() ] );
return false;
}
}
/**
* Update an attendee for the Commerce provider.
*
* @since 5.1.0
*
* @param array|int $attendee The attendee data or ID for the attendee to update.
* @param array $attendee_data The attendee data to update to.
*
* @return WP_Post|false The updated post object or false if unsuccessful.
*/
public function update_attendee( $attendee, $attendee_data ) {
if ( is_numeric( $attendee ) ) {
$attendee_id = (int) $attendee;
} elseif ( is_array( $attendee ) && isset( $attendee['attendee_id'] ) ) {
$attendee_id = (int) $attendee['attendee_id'];
} else {
return false;
}
// Set the attendee ID to be updated.
$attendee_data['attendee_id'] = $attendee_id;
/** @var Tribe__Tickets__Attendee_Repository $orm */
$orm = tribe_attendees( $this->orm_provider );
try {
$attendee = $orm->update_attendee( $attendee_data );
} catch ( Tribe__Repository__Usage_Error $e ) {
do_action( 'tribe_log', 'error', __CLASS__, [ 'message' => $e->getMessage() ] );
return false;
}
return $attendee;
}
/**
* Maybe lookup or create an attendee user from an email.
*
* @since 5.1.0
*
* @param string $email The email to maybe set up the user from.
* @param array $args The arguments used from this attendee.
*
* @return int|null The user ID or null if not set up.
*/
public function maybe_setup_attendee_user_from_email( $email, $args = [] ) {
if ( empty( $email ) || ! is_email( $email ) ) {
return null;
}
$lookup_user_from_email = Arr::get( $args, 'use_existing_user', true );
$create_user_from_email = Arr::get( $args, 'create_user', false );
$send_new_user_info = Arr::get( $args, 'send_email', false );
/**
* Allow filtering whether to enable user lookups by Attendee Email.
*
* @since 5.1.0
*
* @param bool $lookup_user_from_email Whether to lookup the User using the Attendee Email if User ID is not set.
* @param array $args The arguments being set for this attendee.
*/
$lookup_user_from_email = (bool) apply_filters( 'tribe_tickets_attendee_lookup_user_from_email', $lookup_user_from_email, $args );
if ( $lookup_user_from_email ) {
// Check if user exists.
$user = get_user_by( 'email', $email );
if ( $user ) {
return $user->ID;
}
}
/**
* Allow filtering whether to enable creating users using the Attendee Email.
*
* @since 5.1.0
*
* @param bool $create_user_from_email Whether to create the User using the Attendee Email if User ID is not set.
* @param array $args The arguments being set for this attendee.
*/
$create_user_from_email = (bool) apply_filters( 'tribe_tickets_attendee_create_user_from_email', $create_user_from_email, $args );
// Do not create the user from the email.
if ( ! $create_user_from_email ) {
return null;
}
// Create the user using the attendee email.
$created = wp_create_user( $email, wp_generate_password( 12, false ), $email );
// The user was not created successfully.
if ( ! $created || is_wp_error( $created ) ) {
return null;
}
// Set user details.
$user_details = [
'display_name' => Arr::get( $args, 'display_name', null ),
'first_name' => Arr::get( $args, 'first_name', null ),
'last_name' => Arr::get( $args, 'last_name', null ),
];
$user_details = array_filter( $user_details );
// Save user details if we have any.
if ( ! empty( $user_details ) ){
$user_details['ID'] = $created;
wp_update_user( $user_details );
}
/**
* Allow filtering whether to send the new user information email to the new user.
*
* @since 5.1.0
*
* @param bool $send_new_user_info Whether to send the new user information email to the new user.
* @param array $args The arguments being set for this attendee.
*/
$send_new_user_info = (bool) apply_filters( 'tribe_tickets_attendee_create_user_from_email_send_new_user_info', $send_new_user_info, $args );
if ( $send_new_user_info ) {
wp_send_new_user_notifications( $created, 'user' );
}
return $created;
}
/**
* Localized messages for errors, etc in javascript. Added in assets() above.
* Set up this way to amke it easier to add messages as needed.
*
* @since 4.11.0
*
* @return array
*/
public static function set_messages() {
return [
'api_error_title' => _x( 'API Error', 'Error message title, will be followed by the error code.', 'event-tickets' ),
'connection_error' => __( 'Refresh this page or wait a few minutes before trying again. If this happens repeatedly, please contact the Site Admin.', 'event-tickets' ),
'capacity_error' => __( 'The ticket for this event has sold out and has been removed from your cart.', 'event-tickets' ),
'validation_error_title' => __( 'Whoops!', 'event-tickets' ),
'validation_error' => '<p>' . sprintf( esc_html_x( 'You have %s ticket(s) with a field that requires information.', 'The %s will change based on the error produced.', 'event-tickets' ), '<span class="tribe-tickets__notice--error__count">0</span>' ) . '</p>',
];
}
/**
* Return the string representation of this provider class as the class name for backwards compatibility.
*
* @since 4.12.3
*
* @return string The class name.
*/
public function __toString() {
return $this->class_name;
}
/**
* Removes this module from the list of active modules.
*
* @since 5.8.0
*
* @return void This module is removed from the list of active modules, if it was active.
*/
public function deactivate(): void {
$class_name = get_class( $this );
unset( self::$active_modules[ $class_name ] );
}
/**
* Filter provider information for the admin tickets table.
*
* @since 5.14.0
*
* @param array[] $provider_info The list of provider information.
*
* @return array[] The filtered list of provider information.
*/
public function filter_admin_tickets_table_provider_info( $provider_info ) {
$provider_info[ $this->class_name ] = [
'title' => $this->plugin_name,
'event_meta_key' => $this->get_event_key(),
'attendee_post_type' => $this->attendee_object,
'ticket_post_type' => $this->ticket_object,
];
return $provider_info;
}
/************************
* *
* Deprecated Methods *
* *
************************/
// @codingStandardsIgnoreStart
/**
* 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)
*
* @deprecated 4.6.2
*
* @see tribe( 'tickets.attendees' )->user_can
*
* @param string $generic_cap
* @param int $attendee_id
*
* @return boolean
*/
public function user_can( $generic_cap, $attendee_id ) {
_deprecated_function( __METHOD__, '4.6.2', 'tribe( "tickets.metabox" )->user_can( $generic_cap, $attendee_id )' );
return tribe( 'tickets.metabox' )->user_can( $generic_cap, $attendee_id );
}
/**
* Check and set global capacity options for the "event" post
*
* @deprecated 4.6.2
* @since 4.6
*
* @return object ajax success object
*/
public function edit_global_capacity_level() {
_deprecated_function( __METHOD__, '4.6.2', 'tribe_tickets_update_capacity' );
}
/**
* Sets an AJAX error, returns a JSON array and ends the execution.
*
* @deprecated 4.6.2
*
* @param string $message
*/
final protected function ajax_error( $message = '' ) {
_deprecated_function( __METHOD__, '4.6.2', 'wp_send_json_error()' );
wp_send_json_error( $message );
}
/**
* Sets an AJAX response, returns a JSON array and ends the execution.
*
* @deprecated 4.6.2
*
* @param mixed $data
*/
final protected function ajax_ok( $data ) {
_deprecated_function( __METHOD__, '4.6.2', 'wp_send_json_success()' );
wp_send_json_success( $data );
}
// @codingStandardsIgnoreEnd
}
}
|