| 1 |
<?php |
| 2 |
|
| 3 |
namespace FluentBooking\App\Hooks\Handlers; |
| 4 |
|
| 5 |
use FluentBooking\App\Models\Booking; |
| 6 |
use FluentBooking\App\Models\Calendar; |
| 7 |
use FluentBooking\App\Models\Availability; |
| 8 |
use FluentBooking\App\Services\PermissionManager; |
| 9 |
|
| 10 |
class DataExporter |
| 11 |
{ |
| 12 |
public function exportCalendar() |
| 13 |
{ |
| 14 |
if (!$this->verifyNonce()) { |
| 15 |
wp_die(esc_html__('Security check failed. Please refresh and try again.', 'fluent-booking'), 403); |
| 16 |
} |
| 17 |
|
| 18 |
$calendarId = isset($_REQUEST['calendar_id']) ? (int)$_REQUEST['calendar_id'] : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 19 |
|
| 20 |
if (!$calendarId) { |
| 21 |
die(esc_html__('Please provide Calendar ID', 'fluent-booking')); |
| 22 |
} |
| 23 |
|
| 24 |
$calendar = Calendar::with(['metas', 'events' => function ($query) { |
| 25 |
$query->with('event_metas'); |
| 26 |
}])->find($calendarId); |
| 27 |
|
| 28 |
if (!$calendar) { |
| 29 |
die(esc_html__('Calendar not found', 'fluent-booking')); |
| 30 |
} |
| 31 |
|
| 32 |
if (!PermissionManager::hasCalendarAccess($calendar)) { |
| 33 |
die(esc_html__('You do not have permission to export data', 'fluent-booking')); |
| 34 |
} |
| 35 |
|
| 36 |
$calendarData = $this->prepareCalendarExportData($calendar); |
| 37 |
|
| 38 |
header('Content-Type: application/json'); |
| 39 |
header('Content-Disposition: attachment; filename=CluentBookingHostExport-' . $calendarId . '.json'); |
| 40 |
echo json_encode($calendarData, JSON_PRETTY_PRINT); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped |
| 41 |
exit(); |
| 42 |
} |
| 43 |
|
| 44 |
public function exportBookingHosts() |
| 45 |
{ |
| 46 |
if (!$this->verifyNonce()) { |
| 47 |
wp_die(esc_html__('Security check failed. Please refresh and try again.', 'fluent-booking'), 403); |
| 48 |
} |
| 49 |
|
| 50 |
$groupId = isset($_REQUEST['group_id']) ? (int)$_REQUEST['group_id'] : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended |
| 51 |
|
| 52 |
if (!$groupId) { |
| 53 |
die(esc_html__('Please provide Group ID', 'fluent-booking')); |
| 54 |
} |
| 55 |
|
| 56 |
$attendees = Booking::where('group_id', $groupId)->get(); |
| 57 |
|
| 58 |
if ($attendees->isEmpty()) { |
| 59 |
die(esc_html__('No bookings found for the provided Group ID', 'fluent-booking')); |
| 60 |
} |
| 61 |
|
| 62 |
if (!PermissionManager::userCanSeeAllBookings() && !$attendees->first()->hasBookingAccess()) { |
| 63 |
die(esc_html__('You do not have permission to export this group\'s attendees', 'fluent-booking')); |
| 64 |
} |
| 65 |
|
| 66 |
$csvData[] = [ |
| 67 |
'First Name', |
| 68 |
'Last Name', |
| 69 |
'Email', |
| 70 |
'Message', |
| 71 |
'Location Details', |
| 72 |
'Source', |
| 73 |
'Booking Type', |
| 74 |
'Status', |
| 75 |
'Source URL', |
| 76 |
'Duration', |
| 77 |
'Start Time', |
| 78 |
'End Time', |
| 79 |
'Payment Status', |
| 80 |
'Payment Order Status', |
| 81 |
'Payment Method', |
| 82 |
'Currency', |
| 83 |
'Total Amount', |
| 84 |
'Order Created At', |
| 85 |
'Transaction ID', |
| 86 |
'Vendor Charge ID', |
| 87 |
'Transaction Payment Method', |
| 88 |
'Transaction Status', |
| 89 |
'Transaction Total', |
| 90 |
'Transaction Created At', |
| 91 |
]; |
| 92 |
|
| 93 |
foreach ($attendees as $attendee) { |
| 94 |
$row = [ |
| 95 |
$this->sanitizeCsvCell($attendee->first_name), |
| 96 |
$this->sanitizeCsvCell($attendee->last_name), |
| 97 |
$this->sanitizeCsvCell($attendee->email), |
| 98 |
$this->sanitizeCsvCell($attendee->message), |
| 99 |
$this->sanitizeCsvCell($attendee->getLocationAsText()), |
| 100 |
$this->sanitizeCsvCell($attendee->source), |
| 101 |
$this->sanitizeCsvCell($attendee->booking_type), |
| 102 |
$this->sanitizeCsvCell($attendee->status), |
| 103 |
$this->sanitizeCsvCell($attendee->source_url), |
| 104 |
$this->sanitizeCsvCell($attendee->slot_minutes), |
| 105 |
$this->sanitizeCsvCell($attendee->start_time), |
| 106 |
$this->sanitizeCsvCell($attendee->end_time), |
| 107 |
$this->sanitizeCsvCell($attendee->payment_status), |
| 108 |
]; |
| 109 |
|
| 110 |
$paymentOrder = $attendee->payment_status ? $attendee->payment_order : null; |
| 111 |
$row = array_merge($row, $this->buildPaymentColumns($paymentOrder)); |
| 112 |
|
| 113 |
$csvData[] = $row; |
| 114 |
} |
| 115 |
|
| 116 |
$csvData = apply_filters('fluent_booking/exporting_booking_data_csv', $csvData, $attendees); |
| 117 |
|
| 118 |
$output = fopen('php://output', 'w'); |
| 119 |
header('Content-Type: text/csv'); |
| 120 |
header('Content-Disposition: attachment; filename=Booking-Event-Guests-' . $groupId . '.csv'); |
| 121 |
|
| 122 |
foreach ($csvData as $index => $row) { |
| 123 |
// Sanitize header row cells for consistency (formula-neutralize and strip control chars) |
| 124 |
if ($index === 0) { |
| 125 |
$row = array_map([$this, 'sanitizeCsvCell'], $row); |
| 126 |
} |
| 127 |
fputcsv($output, $row); |
| 128 |
} |
| 129 |
|
| 130 |
fclose($output); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose |
| 131 |
exit(); |
| 132 |
} |
| 133 |
|
| 134 |
/* |
| 135 |
* Prepare calendar data for export |
| 136 |
* @param Calendar|int $calendar Calendar Model or ID |
| 137 |
* @return array |
| 138 |
*/ |
| 139 |
public function prepareCalendarExportData($calendar = null) |
| 140 |
{ |
| 141 |
if (is_numeric($calendar)) { |
| 142 |
$calendar = Calendar::with(['metas', 'events' => function ($query) { |
| 143 |
$query->with('event_metas'); |
| 144 |
}])->find($calendar); |
| 145 |
} else if (is_null($calendar)) { |
| 146 |
$calendar = Calendar::with(['metas', 'events' => function ($query) { |
| 147 |
$query->with('event_metas'); |
| 148 |
}])->first(); |
| 149 |
} |
| 150 |
|
| 151 |
if (!$calendar) { |
| 152 |
return []; |
| 153 |
} |
| 154 |
|
| 155 |
$availabilities = []; |
| 156 |
|
| 157 |
foreach ($calendar->events as $event) { |
| 158 |
if (isset($availabilities[$event->availability_id])) { |
| 159 |
continue; |
| 160 |
} |
| 161 |
$availability = Availability::find($event->availability_id); |
| 162 |
if ($availability) { |
| 163 |
$availabilities[$event->availability_id] = $availability; |
| 164 |
} |
| 165 |
} |
| 166 |
|
| 167 |
$calendarData = $calendar->toArray(); |
| 168 |
|
| 169 |
$calendarData['data_type'] = 'host'; |
| 170 |
$calendarData['availabilities'] = $availabilities; |
| 171 |
|
| 172 |
$calendarData = apply_filters('fluent_booking/exporting_calendar_data_json', $calendarData, $calendar); |
| 173 |
|
| 174 |
return $calendarData; |
| 175 |
} |
| 176 |
|
| 177 |
private function buildPaymentColumns($order) |
| 178 |
{ |
| 179 |
if (!$order) { |
| 180 |
return array_fill(0, 11, ''); |
| 181 |
} |
| 182 |
|
| 183 |
$order->load(['items', 'transaction']); |
| 184 |
$trans = $order->transaction; |
| 185 |
|
| 186 |
return [ |
| 187 |
$this->sanitizeCsvCell($order->status), |
| 188 |
$this->sanitizeCsvCell($order->payment_method), |
| 189 |
$this->sanitizeCsvCell($order->currency), |
| 190 |
$order->total_amount / 100, |
| 191 |
$this->sanitizeCsvCell($order->created_at), |
| 192 |
$this->sanitizeCsvCell($trans ? $trans->id : ''), |
| 193 |
$this->sanitizeCsvCell($trans ? $trans->vendor_charge_id : ''), |
| 194 |
$this->sanitizeCsvCell($trans ? $trans->payment_method : ''), |
| 195 |
$this->sanitizeCsvCell($trans ? $trans->status : ''), |
| 196 |
$trans ? $trans->total / 100 : '', |
| 197 |
$this->sanitizeCsvCell($trans ? $trans->created_at : ''), |
| 198 |
]; |
| 199 |
} |
| 200 |
|
| 201 |
/** |
| 202 |
* Sanitize a value for safe CSV output: neutralize formula injection and strip control chars. |
| 203 |
* Prefix with single quote when value starts with =, +, -, or @ so spreadsheets treat as text. |
| 204 |
* |
| 205 |
* @param mixed $value Cell value (string, number, or null). |
| 206 |
* @return string Safe string for fputcsv. |
| 207 |
*/ |
| 208 |
private function sanitizeCsvCell($value) |
| 209 |
{ |
| 210 |
if (empty($value)) { |
| 211 |
return ''; |
| 212 |
} |
| 213 |
$value = (string) $value; |
| 214 |
// Strip control characters (ASCII 0-31 except tab, LF, CR). |
| 215 |
$value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $value); |
| 216 |
// Neutralize formula injection: prefix with ' so Excel/LibreOffice treat as text. |
| 217 |
$first = isset($value[0]) ? $value[0] : ''; |
| 218 |
if (in_array($first, ['=', '+', '-', '@'], true)) { |
| 219 |
$value = "'" . $value; |
| 220 |
} |
| 221 |
|
| 222 |
return $value; |
| 223 |
} |
| 224 |
|
| 225 |
/** |
| 226 |
* Verify the request nonce for AJAX actions. |
| 227 |
* |
| 228 |
* @return bool |
| 229 |
*/ |
| 230 |
private function verifyNonce() |
| 231 |
{ |
| 232 |
$nonce = isset($_REQUEST['nonce']) ? sanitize_text_field(wp_unslash($_REQUEST['nonce'])) : ''; |
| 233 |
|
| 234 |
return !empty($nonce) && wp_verify_nonce($nonce, 'fluent-booking'); |
| 235 |
} |
| 236 |
} |
| 237 |
|