\d+)/ics', array(
'methods' => 'GET', 'callback' => array( __CLASS__, 'ics' ), 'permission_callback' => '__return_true',
) );
}
/* ---------------- calendar ---------------- */
private static function ics_escape( $s ) {
return str_replace( array( '\\', ';', ',', "\r\n", "\n" ), array( '\\\\', '\;', '\,', '\n', '\n' ), (string) $s );
}
/** All-day calendar entry for the event date (no personal data). */
public static function ics_body( $event_id ) {
$date = (string) get_post_meta( $event_id, '_seatt_event_date', true );
if ( ! preg_match( '/^(\d{4})-(\d{2})-(\d{2})/', $date, $m ) ) {
return '';
}
$start = $m[1] . $m[2] . $m[3];
$end = gmdate( 'Ymd', strtotime( $m[1] . '-' . $m[2] . '-' . $m[3] . ' +1 day' ) );
$title = get_the_title( $event_id );
$url = get_permalink( $event_id );
$lines = array(
'BEGIN:VCALENDAR', 'VERSION:2.0', 'PRODID:-//SeatWP//EN', 'CALSCALE:GREGORIAN', 'METHOD:PUBLISH',
'BEGIN:VEVENT',
'UID:esm-event-' . (int) $event_id . '@' . wp_parse_url( home_url(), PHP_URL_HOST ),
'DTSTAMP:' . gmdate( 'Ymd\THis\Z' ),
'DTSTART;VALUE=DATE:' . $start,
'DTEND;VALUE=DATE:' . $end,
'SUMMARY:' . self::ics_escape( $title ),
'URL:' . self::ics_escape( $url ),
'BEGIN:VALARM', 'TRIGGER:-P1D', 'ACTION:DISPLAY', 'DESCRIPTION:' . self::ics_escape( $title ), 'END:VALARM',
'END:VEVENT', 'END:VCALENDAR',
);
return implode( "\r\n", $lines ) . "\r\n";
}
public static function ics( WP_REST_Request $req ) {
$id = intval( $req['id'] );
// Only published, non-password-protected events are public; drafts,
// private and scheduled events stay hidden from anyone who cannot
// edit them.
$public = 'publish' === get_post_status( $id ) && ! post_password_required( $id );
if ( 'seatt_event' !== get_post_type( $id ) || ( ! $public && ! current_user_can( 'edit_post', $id ) ) || ! ( $body = self::ics_body( $id ) ) ) {
return new WP_Error( 'seatt_not_found', __( 'Event not found.', 'seathive' ), array( 'status' => 404 ) );
}
header( 'Content-Type: text/calendar; charset=utf-8' );
header( 'Content-Disposition: attachment; filename="event-' . $id . '.ics"' );
echo $body; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- escaped per RFC 5545 above
exit;
}
public static function google_calendar_url( $event_id ) {
$date = (string) get_post_meta( $event_id, '_seatt_event_date', true );
if ( ! preg_match( '/^(\d{4})-(\d{2})-(\d{2})/', $date, $m ) ) {
return '';
}
$start = $m[1] . $m[2] . $m[3];
$end = gmdate( 'Ymd', strtotime( $m[1] . '-' . $m[2] . '-' . $m[3] . ' +1 day' ) );
return 'https://calendar.google.com/calendar/render?' . http_build_query( array(
'action' => 'TEMPLATE',
'text' => get_the_title( $event_id ),
'dates' => $start . '/' . $end,
'details' => get_permalink( $event_id ),
) );
}
/** Buttons for receipt page + email. */
public static function calendar_links_html( $event_id, $email_style = false ) {
$g = self::google_calendar_url( $event_id );
if ( ! $g ) {
return '';
}
$ics = rest_url( self::NS . '/events/' . intval( $event_id ) . '/ics' );
$st = $email_style ? 'display:inline-block;margin:4px 6px;padding:8px 14px;border:1px solid #c3c4c7;border-radius:6px;color:#1d2327;text-decoration:none;font-size:13px' : '';
return '
';
}
/* ---------------- export + print ---------------- */
/** One row per seat / zone ticket, bookings that are paid or pending. */
public static function attendee_rows( $event_id ) {
global $wpdb;
$rows = $wpdb->get_results( $wpdb->prepare( 'SELECT b.id AS booking_id, b.status, b.buyer_name, b.buyer_email, b.buyer_phone, b.total, b.promo_code, b.payment_method, b.checked_in_at, b.created_at, b.custom_data,
bs.seat_id, s.row_name, s.label, s.category, bs.attendee_name, bs.attendee_email, bs.attendee_birthdate, bs.attendee_note, bs.price
FROM %i b
JOIN %i bs ON bs.booking_id = b.id
JOIN %i s ON s.id = bs.seat_id
WHERE b.event_id = %d AND b.status IN ( \'paid\', \'pending_payment\' )', $wpdb->seatt_bookings, $wpdb->seatt_booking_seats, $wpdb->seatt_seats, $event_id ) );
$zones = $wpdb->get_results( $wpdb->prepare( 'SELECT b.id AS booking_id, b.status, b.buyer_name, b.buyer_email, b.buyer_phone, b.total, b.promo_code, b.payment_method, b.checked_in_at, b.created_at, b.custom_data,
z.id AS zone_id, z.name AS zone_name, z.category, bz.quantity, bz.price_each
FROM %i b
JOIN %i bz ON bz.booking_id = b.id
JOIN %i z ON z.id = bz.zone_id
WHERE b.event_id = %d AND b.status IN ( \'paid\', \'pending_payment\' )', $wpdb->seatt_bookings, $wpdb->seatt_booking_zones, $wpdb->seatt_zones, $event_id ) );
$out = array();
foreach ( $rows as $r ) {
$out[] = array(
'seat' => seatt_seat_label( $r->row_name, $r->label ), 'row' => (string) $r->row_name, 'sort' => $r->label,
'attendee' => $r->attendee_name ?: $r->buyer_name, 'category' => $r->category, 'price' => $r->price, 'note' => (string) $r->attendee_note,
'attendee_email' => (string) ( $r->attendee_email ?: ( $r->attendee_name ? '' : $r->buyer_email ) ), 'birthdate' => (string) $r->attendee_birthdate,
) + (array) $r;
}
foreach ( $zones as $z ) {
// Quantity tickets with named holders: one row per ticket.
$holders = class_exists( 'SEATT_Attendees' ) ? $wpdb->get_results( $wpdb->prepare( 'SELECT name, email, birthdate, line_no FROM %i WHERE booking_id = %d AND zone_id = %d ORDER BY line_no', $wpdb->seatt_booking_attendees, $z->booking_id, $z->zone_id ) ) : array();
if ( $holders ) {
foreach ( $holders as $h ) {
$out[] = array(
'seat' => $z->zone_name . ' #' . (int) $h->line_no, 'row' => '~' . $z->zone_name, 'sort' => $z->zone_name . sprintf( '%04d', $h->line_no ),
'attendee' => (string) $h->name, 'category' => $z->category, 'price' => $z->price_each, 'note' => '',
'attendee_email' => (string) $h->email, 'birthdate' => (string) $h->birthdate,
) + (array) $z;
}
continue;
}
$out[] = array(
'seat' => $z->zone_name . ' ×' . (int) $z->quantity, 'row' => '~' . $z->zone_name, 'sort' => $z->zone_name,
'attendee' => $z->buyer_name, 'category' => $z->category, 'price' => $z->price_each * $z->quantity, 'note' => '',
'attendee_email' => (string) $z->buyer_email, 'birthdate' => '',
) + (array) $z;
}
usort( $out, function ( $a, $b ) {
return strnatcasecmp( $a['row'], $b['row'] ) ?: strnatcasecmp( $a['sort'], $b['sort'] );
} );
return $out;
}
private static function guard_export() {
if ( ! self::can_staff() || ! check_admin_referer( 'seatt_export' ) ) {
wp_die( esc_html__( 'Not allowed.', 'seathive' ) );
}
$event_id = intval( $_GET['event_id'] ?? 0 );
if ( 'seatt_event' !== get_post_type( $event_id ) ) {
wp_die( esc_html__( 'Choose an event first.', 'seathive' ) );
}
return $event_id;
}
/** Neutralises spreadsheet formula injection (=, +, -, @ …). */
public static function csv_cell( $v ) {
$v = (string) $v;
return ( '' !== $v && in_array( $v[0], array( '=', '+', '-', '@', "\t", "\r" ), true ) ) ? "'" . $v : $v;
}
public static function export_csv() {
$event_id = self::guard_export();
$rows = self::attendee_rows( $event_id );
$name = sanitize_file_name( get_the_title( $event_id ) ?: 'event' ) . '-attendees-' . gmdate( 'Ymd' ) . '.csv';
nocache_headers();
header( 'Content-Type: text/csv; charset=utf-8' );
header( 'Content-Disposition: attachment; filename="' . $name . '"' );
$out = fopen( 'php://output', 'w' );
fwrite( $out, "\xEF\xBB\xBF" ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fwrite -- streaming the CSV download to php://output, not writing a file. BOM so Excel reads UTF-8.
// Checkout questions: one column each.
$questions = class_exists( 'SEATT_Checkout_Fields' ) ? SEATT_Checkout_Fields::fields( $event_id ) : array();
fputcsv( $out, array_merge( array( 'Invoice', 'Booking', 'Status', 'Seat / ticket', 'Attendee', 'Attendee email', 'Date of birth', 'Category', 'Price', 'Buyer', 'Email', 'Phone', 'Promo', 'Paid via', 'Checked in', 'Booked', 'Note' ), array_map( array( __CLASS__, 'csv_cell' ), wp_list_pluck( $questions, 'label' ) ) ) );
foreach ( $rows as $r ) {
$answers = array();
if ( $questions ) {
$by_id = array();
foreach ( SEATT_Checkout_Fields::answers( (object) array( 'id' => $r['booking_id'], 'custom_data' => $r['custom_data'] ?? '' ) ) as $a ) {
$by_id[ $a['id'] ] = $a['value'];
}
foreach ( $questions as $q ) {
$answers[] = $by_id[ $q['id'] ] ?? '';
}
}
fputcsv( $out, array_map( array( __CLASS__, 'csv_cell' ), array_merge( array(
SEATT_DB::invoice_number( (object) array( 'id' => $r['booking_id'], 'created_at' => $r['created_at'] ) ),
$r['booking_id'], $r['status'], $r['seat'], $r['attendee'], $r['attendee_email'] ?? '', $r['birthdate'] ?? '', SEATT_Categories::name( $event_id, (string) $r['category'] ), $r['price'], $r['buyer_name'], $r['buyer_email'],
$r['buyer_phone'], $r['promo_code'], $r['payment_method'], $r['checked_in_at'], $r['created_at'], $r['note'],
), $answers ) ) );
}
fclose( $out ); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose -- php://output stream
exit;
}
public static function print_list() {
$event_id = self::guard_export();
$rows = self::attendee_rows( $event_id );
$title = get_the_title( $event_id );
$date = get_post_meta( $event_id, '_seatt_event_date', true );
?>
· ·