PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.1.1
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.1.1
2.4.0 2.3.0 2.2.5 2.2.0 2.1.2 2.1.1 trunk 1.10.0 1.10.01 1.10.02 1.5.0 1.5.01 1.5.02 1.5.1 1.5.10 1.5.20 1.5.21 1.5.22 1.5.23 1.5.24 1.5.25 1.6.0 1.7.0 1.7.1 1.7.2 All 33 releases
fluent-booking / app / Hooks / Handlers / DataExporter.php

DataExporter.php in Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution 2.1.1, at app/Hooks/Handlers/DataExporter.php

233 lines 8.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
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 if (!PermissionManager::hasAllCalendarAccess()) {
51 die(esc_html__('You do not have permission to export data', 'fluent-booking'));
52 }
53
54 $groupId = isset($_REQUEST['group_id']) ? (int)$_REQUEST['group_id'] : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
55
56 if (!$groupId) {
57 die(esc_html__('Please provide Group ID', 'fluent-booking'));
58 }
59
60 $attendees = Booking::where('group_id', $groupId)->get();
61
62 $csvData[] = [
63 'First Name',
64 'Last Name',
65 'Email',
66 'Message',
67 'Location Details',
68 'Source',
69 'Booking Type',
70 'Status',
71 'Source URL',
72 'Duration',
73 'Start Time',
74 'End Time',
75 'Payment Status',
76 'Payment Order Status',
77 'Payment Method',
78 'Currency',
79 'Total Amount',
80 'Order Created At',
81 'Transaction ID',
82 'Vendor Charge ID',
83 'Transaction Payment Method',
84 'Transaction Status',
85 'Transaction Total',
86 'Transaction Created At',
87 ];
88
89 foreach ($attendees as $attendee) {
90 $row = [
91 $this->sanitizeCsvCell($attendee->first_name),
92 $this->sanitizeCsvCell($attendee->last_name),
93 $this->sanitizeCsvCell($attendee->email),
94 $this->sanitizeCsvCell($attendee->message),
95 $this->sanitizeCsvCell($attendee->getLocationAsText()),
96 $this->sanitizeCsvCell($attendee->source),
97 $this->sanitizeCsvCell($attendee->booking_type),
98 $this->sanitizeCsvCell($attendee->status),
99 $this->sanitizeCsvCell($attendee->source_url),
100 $this->sanitizeCsvCell($attendee->slot_minutes),
101 $this->sanitizeCsvCell($attendee->start_time),
102 $this->sanitizeCsvCell($attendee->end_time),
103 $this->sanitizeCsvCell($attendee->payment_status),
104 ];
105
106 $paymentOrder = $attendee->payment_status ? $attendee->payment_order : null;
107 $row = array_merge($row, $this->buildPaymentColumns($paymentOrder));
108
109 $csvData[] = $row;
110 }
111
112 $csvData = apply_filters('fluent_booking/exporting_booking_data_csv', $csvData, $attendees);
113
114 $output = fopen('php://output', 'w');
115 header('Content-Type: text/csv');
116 header('Content-Disposition: attachment; filename=Booking-Event-Guests-' . $groupId . '.csv');
117
118 foreach ($csvData as $index => $row) {
119 // Sanitize header row cells for consistency (formula-neutralize and strip control chars)
120 if ($index === 0) {
121 $row = array_map([$this, 'sanitizeCsvCell'], $row);
122 }
123 fputcsv($output, $row);
124 }
125
126 fclose($output); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
127 exit();
128 }
129
130 /*
131 * Prepare calendar data for export
132 * @param Calendar|int $calendar Calendar Model or ID
133 * @return array
134 */
135 public function prepareCalendarExportData($calendar = null)
136 {
137 if (is_numeric($calendar)) {
138 $calendar = Calendar::with(['metas', 'events' => function ($query) {
139 $query->with('event_metas');
140 }])->find($calendar);
141 } else if (is_null($calendar)) {
142 $calendar = Calendar::with(['metas', 'events' => function ($query) {
143 $query->with('event_metas');
144 }])->first();
145 }
146
147 if (!$calendar) {
148 return [];
149 }
150
151 $availabilities = [];
152
153 foreach ($calendar->events as $event) {
154 if (isset($availabilities[$event->availability_id])) {
155 continue;
156 }
157 $availability = Availability::find($event->availability_id);
158 if ($availability) {
159 $availabilities[$event->availability_id] = $availability;
160 }
161 }
162
163 $calendarData = $calendar->toArray();
164
165 $calendarData['data_type'] = 'host';
166 $calendarData['availabilities'] = $availabilities;
167
168 $calendarData = apply_filters('fluent_booking/exporting_calendar_data_json', $calendarData, $calendar);
169
170 return $calendarData;
171 }
172
173 private function buildPaymentColumns($order)
174 {
175 if (!$order) {
176 return array_fill(0, 11, '');
177 }
178
179 $order->load(['items', 'transaction']);
180 $trans = $order->transaction;
181
182 return [
183 $this->sanitizeCsvCell($order->status),
184 $this->sanitizeCsvCell($order->payment_method),
185 $this->sanitizeCsvCell($order->currency),
186 $order->total_amount / 100,
187 $this->sanitizeCsvCell($order->created_at),
188 $this->sanitizeCsvCell($trans ? $trans->id : ''),
189 $this->sanitizeCsvCell($trans ? $trans->vendor_charge_id : ''),
190 $this->sanitizeCsvCell($trans ? $trans->payment_method : ''),
191 $this->sanitizeCsvCell($trans ? $trans->status : ''),
192 $trans ? $trans->total / 100 : '',
193 $this->sanitizeCsvCell($trans ? $trans->created_at : ''),
194 ];
195 }
196
197 /**
198 * Sanitize a value for safe CSV output: neutralize formula injection and strip control chars.
199 * Prefix with single quote when value starts with =, +, -, or @ so spreadsheets treat as text.
200 *
201 * @param mixed $value Cell value (string, number, or null).
202 * @return string Safe string for fputcsv.
203 */
204 private function sanitizeCsvCell($value)
205 {
206 if (empty($value)) {
207 return '';
208 }
209 $value = (string) $value;
210 // Strip control characters (ASCII 0-31 except tab, LF, CR).
211 $value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $value);
212 // Neutralize formula injection: prefix with ' so Excel/LibreOffice treat as text.
213 $first = isset($value[0]) ? $value[0] : '';
214 if (in_array($first, ['=', '+', '-', '@'], true)) {
215 $value = "'" . $value;
216 }
217
218 return $value;
219 }
220
221 /**
222 * Verify the request nonce for AJAX actions.
223 *
224 * @return bool
225 */
226 private function verifyNonce()
227 {
228 $nonce = isset($_REQUEST['nonce']) ? sanitize_text_field(wp_unslash($_REQUEST['nonce'])) : '';
229
230 return !empty($nonce) && wp_verify_nonce($nonce, 'fluent-booking');
231 }
232 }
233