| Current File : /home/digitaw/www/wp-content/plugins/event-tickets/src/Tickets/Commerce/Admin_Tables/Orders.php |
<?php
namespace TEC\Tickets\Commerce\Admin_Tables;
use TEC\Tickets\Commerce\Gateways\Manager;
use TEC\Tickets\Commerce\Status\Status_Handler;
use TEC\Tickets\Commerce\Traits\Is_Ticket;
use Tribe__Tickets__Tickets as Tickets;
use WP_List_Table;
use WP_Post;
if ( ! class_exists( 'WP_List_Table' ) ) {
require_once( ABSPATH . 'wp-admin/includes/screen.php' );
require_once( ABSPATH . 'wp-admin/includes/class-wp-list-table.php' );
}
/**
* Class Admin Tables for Orders.
*
* @since 5.2.0
*
*/
class Orders extends WP_List_Table {
use Is_Ticket;
/**
* The user option that will be used to store the number of orders per page to show.
*
* @var string
*/
public $per_page_option = 20;
/**
* The current post ID.
*
* @var int
*/
public $post_id;
/**
* The name (what gets submitted to the server) of our search box input.
*
* @var string $search_box_input_name
*/
private $search_box_input_name = 'search';
/**
* The name of the search type slug.
*
* @var string $search_box_input_name
*/
private $search_type_slug = 'tec_tc_order_search_type';
/**
* Orders Table constructor.
*
* @since 5.2.0
*/
public function __construct() {
$args = [
'singular' => 'order',
'plural' => 'orders',
'ajax' => true,
];
parent::__construct( $args );
}
/**
* Overrides the list of CSS classes for the WP_List_Table table tag.
* This function is not hookable in core, so it needs to be overridden!
*
* @since 5.2.0
*
* @return array List of CSS classes for the table tag.
*/
protected function get_table_classes() {
$classes = [ 'widefat', 'striped', 'tribe-tickets-commerce-report-orders' ];
if ( is_admin() ) {
$classes[] = 'fixed';
}
/**
* Filters the default classes added to the Tickets Commerce order report `WP_List_Table`.
*
* @since 5.2.0
*
* @param array $classes The array of classes to be applied.
*/
return apply_filters( 'tec_tickets_commerce_reports_orders_table_classes', $classes );
}
/**
* Checks the current user's permissions.
*
* @since 5.2.0
*/
public function ajax_user_can() {
$post_type = get_post_type_object( $this->screen->post_type );
return ! empty( $post_type->cap->edit_posts ) && current_user_can( $post_type->cap->edit_posts );
}
/**
* Returns the list of columns.
*
* @since 5.2.0
*
* @return array An associative array in the format [ <slug> => <title> ]
*/
public function get_columns() {
$columns = [
'order' => __( 'Order', 'event-tickets' ),
'purchaser' => __( 'Purchaser', 'event-tickets' ),
'email' => __( 'Email', 'event-tickets' ),
'purchased' => __( 'Purchased', 'event-tickets' ),
'date' => __( 'Date', 'event-tickets' ),
'gateway' => __( 'Gateway', 'event-tickets' ),
'gateway_order_id' => __( 'Gateway ID', 'event-tickets' ),
'status' => __( 'Status', 'event-tickets' ),
'total' => __( 'Total', 'event-tickets' ),
];
return $columns;
}
/**
* Generates content for a single row of the table
*
* @since 5.2.0
*
* @param WP_Post $item The current item.
*/
public function single_row( $item ) {
echo '<tr class="' . esc_attr( $item->post_status ) . '">';
$this->single_row_columns( $item );
echo '</tr>';
}
/**
* Prepares the list of items for displaying.
*
* @since 5.2.0
*/
public function prepare_items() {
$post_id = tribe_get_request_var( 'post_id', 0 );
$post_id = tribe_get_request_var( 'event_id', $post_id );
$this->post_id = $post_id;
$product_ids = tribe_get_request_var( 'product_ids' );
$product_ids = ! empty( $product_ids ) ? explode( ',', $product_ids ) : null;
$search = tribe_get_request_var( $this->search_box_input_name );
$page = absint( tribe_get_request_var( 'paged', 0 ) );
$orderby = tribe_get_request_var( 'orderby' );
$order = tribe_get_request_var( 'order' );
$arguments = [
'status' => 'any',
'paged' => $page,
'posts_per_page' => $this->per_page_option,
];
if ( ! empty( $search ) ) {
$search_keys = array_keys( $this->get_search_options() );
// Default selection.
$search_key = 'purchaser_full_name';
$search_type = sanitize_text_field( tribe_get_request_var( $this->search_type_slug ) );
if (
$search_type
&& in_array( $search_type, $search_keys, true )
) {
$search_key = $search_type;
}
$search_like_keys = [
'purchaser_full_name',
'purchaser_email',
];
/**
* Filters the item keys that support LIKE matching to filter orders while searching them.
*
* @since 5.5.6
*
* @param array $search_like_keys The keys that support LIKE matching.
* @param array $search_keys The keys that can be used to search orders.
* @param string $search The current search string.
*/
$search_like_keys = apply_filters( 'tec_tc_order_search_like_keys', $search_like_keys, $search_keys, $search );
// Update search key if it supports LIKE matching.
if ( in_array( $search_key, $search_like_keys, true ) ) {
$search_key .= '__like';
}
$arguments[ $search_key ] = $search;
}
if ( ! empty( $post_id ) ) {
$arguments['events'] = $post_id;
}
if ( ! empty( $product_ids ) ) {
$arguments['tickets'] = $product_ids;
}
if ( ! empty( $orderby ) ) {
$arguments['orderby'] = $orderby;
}
if ( ! empty( $order ) ) {
$arguments['order'] = $order;
}
/**
* Filters the arguments used to fetch the orders for the order report.
*
* @since 5.5.6
*
* @param array $arguments The arguments used to fetch the orders.
*/
$arguments = apply_filters( 'tec_tc_order_report_args', $arguments );
$orders_repository = tec_tc_orders()->by_args( $arguments );
$total_items = $orders_repository->found();
$this->items = $orders_repository->all();
$this->set_pagination_args( [
'total_items' => $total_items,
'per_page' => $this->per_page_option,
] );
}
/**
* Message to be displayed when there are no items.
*
* @since 5.2.0
*/
public function no_items() {
_e( 'No matching orders found.', 'event-tickets' );
}
/**
* Handler for the columns that don't have a specific column_{name} handler function.
*
* @since 5.2.0
*
* @param WP_Post $item The current item.
* @param string $column The column name.
*
* @return string
*/
public function column_default( $item, $column ) {
return empty( $item->$column ) ? '??' : $item->$column;
}
/**
* Returns the customer name.
*
* @since 5.2.0
*
* @param WP_Post $item The current item.
*
* @return string
*/
public function column_purchaser( $item ) {
return esc_html( $item->purchaser['full_name'] );
}
/**
* Returns the customer email.
*
* @since 5.2.0
*
* @param WP_Post $item The current item.
*
* @return string
*/
public function column_email( $item ) {
return esc_html( $item->purchaser['email'] );
}
/**
* Returns the order status.
*
* @since 5.2.0
*
* @param WP_Post $item The current item.
*
* @return string
*/
public function column_status( $item ) {
$status = tribe( Status_Handler::class )->get_by_wp_slug( $item->post_status );
return esc_html( $status->get_name() );
}
/**
* Handler for the date column.
*
* @since 5.2.0
*
* @param WP_Post $item The current item.
*
* @return string
*/
public function column_date( $item ) {
return esc_html( \Tribe__Date_Utils::reformat( $item->post_modified, \Tribe__Date_Utils::DATEONLYFORMAT ) );
}
/**
* Handler for the purchased column.
*
* @since 5.2.0
*
* @param WP_Post $item The current item.
*
* @return string
*/
public function column_purchased( $item ) {
$output = '';
if (
! $item instanceof WP_Post
|| ! isset( $item->items )
) {
return $output;
}
foreach ( $item->items as $cart_item ) {
if ( ! $this->is_ticket( $cart_item ) ) {
continue;
}
$ticket = Tickets::load_ticket_object( $cart_item['ticket_id'] );
$name = esc_html( $ticket->name );
$quantity = esc_html( (int) $cart_item['quantity'] );
$output .= "<div class='tribe-line-item'>{$quantity} - {$name}</div>";
}
return $output;
}
/**
* Handler for the order column.
*
* @since 5.2.0
*
* @param WP_Post $item The current item.
*
* @return string
*/
public function column_order( $item ) {
$output = sprintf( esc_html__( '%1$s', 'event-tickets' ), $item->ID );
$status = tribe( Status_Handler::class )->get_by_wp_slug( $item->post_status );
switch ( $status->get_slug() ) {
default:
$output .= ' <div class="order-status order-status-' . esc_attr( $status->get_slug() ) . '">';
$output .= esc_html( ucwords( $status->get_name() ) );
$output .= '</div>';
break;
}
return $output;
}
/**
* Handler for the total column.
*
* @since 5.2.0
*
* @param WP_Post $item The current item.
*
* @return string
*/
public function column_total( $item ) {
return $item->total_value->get_currency();
}
/**
* Handler for gateway order id.
*
* @since 5.2.0
* @since 5.9.1 Handle when the $order_url is empty.
*
* @param WP_Post $item The current item.
*
* @return string
*/
public function column_gateway_order_id( $item ) {
$gateway = tribe( Manager::class )->get_gateway_by_key( $item->gateway );
if ( ! $gateway ) {
return $item->gateway_order_id;
}
$order_url = $gateway->get_order_controller()->get_gateway_dashboard_url_by_order( $item );
if ( empty( $order_url ) ) {
return $item->gateway_order_id;
}
return sprintf(
'<a href="%s" target="_blank" rel="noopener noreferrer">%s</a>',
$order_url,
$item->gateway_order_id
);
}
/**
* Handler for gateway column.
*
* @since 5.2.0
*
* @param WP_Post $item The current item.
*
* @return string
*/
public function column_gateway( $item ) {
$gateway = tribe( Manager::class )->get_gateway_by_key( $item->gateway );
if ( ! $gateway ) {
return $item->gateway;
}
return $gateway::get_label();
}
/**
* List of sortable columns.
*
* @since 5.5.0
*
* @return array
*/
public function get_sortable_columns() {
return [
'order' => 'order_id',
'purchaser' => 'purchaser_full_name',
'email' => 'purchaser_email',
'date' => 'purchase_time',
'gateway' => 'gateway',
'gateway_order_id' => 'gateway_id',
'status' => 'status',
'total' => 'total_value'
];
}
/**
* Get the allowed search types and their labels.
*
* @see \TEC\Tickets\Commerce\Repositories\Order_Repository for a List of valid ORM args.
*
* @since 5.5.6
*
* @return array
*/
public function get_search_options() {
$options = [
'purchaser_full_name' => __( 'Search by Purchaser Name', 'event-tickets' ),
'purchaser_email' => __( 'Search by Purchaser Email', 'event-tickets' ),
'gateway' => __( 'Search by Gateway', 'event-tickets' ),
'gateway_order_id' => __( 'Search by Gateway ID', 'event-tickets' ),
'id' => __( 'Search by Order ID', 'event-tickets' ),
];
/**
* Filters the search types to be shown in the search box for filtering orders.
*
* @since 5.5.6
*
* @param array $options List of ORM search types and their labels.
*/
return apply_filters( 'tec_tc_order_search_types', $options );
}
/**
* @inheritDoc
*/
public function search_box( $text, $input_id ) {
// Workaround to show the search box even when no items are found.
$old_items = $this->items;
$this->items = [
'Temporary',
];
// Get normal search box HTML to override.
ob_start();
parent::search_box( $text, $input_id );
$search_box = ob_get_clean();
// Assign the custom search name.
$search_box = str_replace( 'name="s"', 'name="' . esc_attr( $this->search_box_input_name ) . '"', $search_box );
// And get its value upon reloading the page to display its search results so user knows what they searched for.
$search_box = str_replace( 'value=""', 'value="' . esc_attr( tribe_get_request_var( $this->search_box_input_name ) ) . '"', $search_box );
$this->items = $old_items;
// Default selection.
$selected = 'purchaser_full_name';
$search_type = sanitize_text_field( tribe_get_request_var( $this->search_type_slug ) );
$options = $this->get_search_options();
if (
$search_type
&& array_key_exists( $search_type, $options )
) {
$selected = $search_type;
}
$template_vars = [
'options' => $options,
'selected' => $selected,
];
$custom_search = tribe( \TEC\Tickets\Commerce\Reports\Orders::class )->get_template()->template( 'orders/search-options', $template_vars, false );
// Add our search type dropdown before the search box input.
$search_box = str_replace( '<input type="submit"', $custom_search . '<input type="submit"', $search_box );
echo $search_box;
}
/**
* Displays extra controls.
*
* @since 5.8.1
*
* @param string $which The location of the actions: 'left' or 'right'.
*/
public function extra_tablenav( $which ) {
$allowed_tags = [
'input' => [
'type' => true,
'name' => true,
'class' => true,
'value' => true,
],
'a' => [
'class' => true,
'href' => true,
'rel' => true,
'target' => true,
],
];
$nav = [
'left' => [
'print' => sprintf(
'<input type="button" name="print" class="print button action" value="%s">',
esc_attr__(
'Print',
'event-tickets'
)
),
'export' => sprintf(
'<a target="_blank" href="%s" class="export action button" rel="noopener noreferrer">%s</a>',
esc_url( $this->get_export_url() ),
esc_html__(
'Export',
'event-tickets'
)
),
],
'right' => [],
];
$nav = apply_filters( 'tribe_events_tickets_orders_table_nav', $nav, $which );
?>
<div class="alignleft actions attendees-actions"><?php echo wp_kses( implode( $nav['left'] ), $allowed_tags ); ?></div>
<div class="alignright attendees-filter"><?php echo wp_kses( implode( $nav['right'] ), $allowed_tags ); ?></div>
<?php
}
/**
* Maybe generate a CSV file for orders.
*
* This method checks if the necessary GET parameters are set to trigger the CSV generation.
* If conditions are met, it generates a CSV file with order data and outputs it for download.
*
* @since 5.8.1
*
* @return void
*/
public function maybe_generate_csv(): void {
// Early bail: Check if the necessary GET parameters are not set.
if ( empty( $_GET['orders_csv'] ) || empty( $_GET['orders_csv_nonce'] ) || empty( $_GET['post_id'] ) ) {
return;
}
$event_id = absint( $_GET['post_id'] );
/**
* Filters the event ID before using it to fetch orders.
*
* @since 5.8.1
*
* @param int $event_id The event ID.
*/
$event_id = apply_filters( 'tec_tickets_filter_event_id', $event_id );
// Early bail: Verify the event ID and the nonce.
if ( empty( $event_id ) || ! wp_verify_nonce( $_GET['orders_csv_nonce'], 'orders_csv_nonce' ) ) { // phpcs:ignore WordPress.Security.ValidatedSanitizedInput.InputNotSanitized
return;
}
$post_id = tribe_get_request_var(
'event_id',
tribe_get_request_var(
'post_id',
0
)
);
$product_ids = explode(
',',
tribe_get_request_var(
'product_ids',
''
)
);
// Initialize arguments for fetching orders.
$arguments = [
'status' => 'any',
'events' => $post_id,
'tickets' => ! empty( $product_ids ) ? $product_ids : null,
'orderby' => tribe_get_request_var( 'orderby', '' ),
'order' => tribe_get_request_var( 'order', '' ),
];
/**
* Filters the arguments for the order report export.
*
* @since 5.8.1
*
* @param array $arguments The arguments for order retrieval.
*/
$arguments = apply_filters( 'tec_tc_order_report_export_args', $arguments );
// Fetch orders using the repository.
$orders_repository = tec_tc_orders()->by_args( $arguments );
$items = $orders_repository->all();
// Format the orders data for CSV.
$formatted_data = $this->format_for_csv( $items );
// Get the event post for filename.
$event = get_post( $post_id );
$filename = sanitize_title( $event->post_title ) . '-' . __(
'orders',
'event-tickets'
) . '.csv';
// Generate and output the CSV file.
$this->generate_csv_file( $formatted_data, $filename );
}
/**
* Formats the orders data for CSV export.
*
* @since 5.8.1
*
* @param array $items The orders data.
*
* @return array The formatted data ready for CSV export.
*/
public function format_for_csv( array $items ): array {
$csv_data = [];
// Use the values of get_columns() as CSV headers.
$csv_headers = array_values( $this->get_columns() );
$csv_data[] = $csv_headers;
// Iterate over each item and format the data.
foreach ( $items as $item ) {
$csv_row = [];
foreach ( array_keys( $this->get_columns() ) as $header ) {
$method_name = 'column_' . $header;
if ( method_exists(
$this,
$method_name
) ) {
// Dynamically call the method for the column.
$value = call_user_func(
[
$this,
$method_name,
],
$item
);
} else {
// Accessing the item properties directly using column_default.
$value = $this->column_default(
$item,
$header
);
}
$csv_row[] = empty( $value ) ? $value : $this->sanitize_and_format_csv_value( $value );
}
$csv_data[] = $csv_row;
}
return $csv_data;
}
/**
* Sanitizes and formats a value for CSV output.
*
* @since 5.8.1
*
* @param string $value The value to be formatted.
*
* @return string The sanitized and formatted value.
*/
private function sanitize_and_format_csv_value( string $value ): string {
$value = wp_strip_all_tags( $value );
$value = html_entity_decode(
$value,
ENT_QUOTES | ENT_XML1,
'UTF-8'
);
$value = str_replace(
[
"\r",
"\n",
],
' ',
$value
);
return $value;
}
/**
* Outputs the formatted data as a CSV file for download.
*
* @since 5.8.1
*
* @param array $formatted_data The formatted data for CSV export.
* @param string $filename The name of the file for download.
*
* @return void
*/
public function generate_csv_file(
array $formatted_data,
string $filename = 'export.csv'
) {
$charset = get_option( 'blog_charset' );
// Set headers to force download on the browser.
header( "Content-Type: text/csv; charset=$charset" );
header( 'Content-Disposition: attachment; filename="' . sanitize_file_name( $filename ) . '"' );
// Open the PHP output stream to write the CSV content.
$output = fopen(
'php://output',
'w'
);
// Iterate over each row and write to the PHP output stream.
foreach ( $formatted_data as $row ) {
fputcsv(
$output,
$row
); // phpcs:ignore WordPressVIPMinimum.Functions.RestrictedFunctions.file_ops_fputcsv
}
// Close the output stream.
fclose( $output );
// Exit to prevent any additional output.
exit;
}
/**
* Generate the export URL for exporting orders.
*
* @since 5.8.1
*
* @return string Relative URL for the export.
*/
public function get_export_url(): string {
return add_query_arg(
[
'orders_csv' => true,
'orders_csv_nonce' => wp_create_nonce( 'orders_csv_nonce' ),
]
);
}
}