\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 ); ?><?php echo esc_html( $title ); ?>

· ·

✓