PluginProbe
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution / 2.5.0
Fluent Booking – The Ultimate Appointments Scheduling, Events Booking, Events Calendar Solution v2.5.0
2.5.0 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 All 34 releases
← All changes | app/Hooks/Handlers/DataExporter.php +96 -39 1.7.0 → 2.5.0 View file →
@@ -10,10 +10,14 @@
10 10 class DataExporter
11 11 {
12 12 public function exportCalendar()
13 13 {
14 - $calendarId = (int)$_REQUEST['calendar_id']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
14 + if (!$this->verifyNonce()) {
15 + wp_die(esc_html__('Security check failed. Please refresh and try again.', 'fluent-booking'), 403);
16 + }
15 17
18 + $calendarId = isset($_REQUEST['calendar_id']) ? (int)$_REQUEST['calendar_id'] : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
19 +
16 20 if (!$calendarId) {
17 21 die(esc_html__('Please provide Calendar ID', 'fluent-booking'));
18 22 }
19 23
@@ -24,9 +28,9 @@
24 28 if (!$calendar) {
25 29 die(esc_html__('Calendar not found', 'fluent-booking'));
26 30 }
27 31
28 - if (!PermissionManager::hasAllCalendarAccess() && !PermissionManager::hasCalendarAccess($calendar)) {
32 + if (!PermissionManager::hasCalendarAccess($calendar)) {
29 33 die(esc_html__('You do not have permission to export data', 'fluent-booking'));
30 34 }
31 35
32 36 $calendarData = $this->prepareCalendarExportData($calendar);
@@ -38,13 +42,13 @@
38 42 }
39 43
40 44 public function exportBookingHosts()
41 45 {
42 - if (!PermissionManager::hasAllCalendarAccess()) {
43 - die(esc_html__('You do not have permission to export data', 'fluent-booking'));
46 + if (!$this->verifyNonce()) {
47 + wp_die(esc_html__('Security check failed. Please refresh and try again.', 'fluent-booking'), 403);
44 48 }
45 49
46 - $groupId = (int)$_REQUEST['group_id']; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
50 + $groupId = isset($_REQUEST['group_id']) ? (int)$_REQUEST['group_id'] : null; // phpcs:ignore WordPress.Security.NonceVerification.Recommended
47 51
48 52 if (!$groupId) {
49 53 die(esc_html__('Please provide Group ID', 'fluent-booking'));
50 54 }
@@ -50,8 +54,16 @@
50 54 }
51 55
52 56 $attendees = Booking::where('group_id', $groupId)->get();
53 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 +
54 66 $csvData[] = [
55 67 'First Name',
56 68 'Last Name',
57 69 'Email',
@@ -79,43 +91,25 @@
79 91 ];
80 92
81 93 foreach ($attendees as $attendee) {
82 94 $row = [
83 - $attendee->first_name,
84 - $attendee->last_name,
85 - $attendee->email,
86 - $attendee->message,
87 - $attendee->getLocationAsText(),
88 - $attendee->source,
89 - $attendee->booking_type,
90 - $attendee->status,
91 - $attendee->source_url,
92 - $attendee->slot_minutes,
93 - $attendee->start_time,
94 - $attendee->end_time,
95 - $attendee->payment_status,
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),
96 108 ];
97 109
98 - if ($attendee->payment_status) {
99 - $order = $attendee->payment_order;
100 - if ($order) {
101 - $order->load(['items', 'transaction']);
102 - $row[] = $order->status;
103 - $row[] = $order->payment_method;
104 - $row[] = $order->currency;
105 - $row[] = $order->total_amount / 100;
106 - $row[] = $order->created_at;
107 - $row[] = $order->transaction->id;
108 - $row[] = $order->transaction->vendor_charge_id;
109 - $row[] = $order->transaction->payment_method;
110 - $row[] = $order->transaction->status;
111 - $row[] = $order->transaction->total / 100;
112 - $row[] = $order->transaction->created_at;
113 - }
114 - } else {
115 - // Fill empty columns for payment related data if payment_status is false
116 - $row = array_pad($row, 11, '');
117 - }
110 + $paymentOrder = $attendee->payment_status ? $attendee->payment_order : null;
111 + $row = array_merge($row, $this->buildPaymentColumns($paymentOrder));
118 112
119 113 $csvData[] = $row;
120 114 }
121 115
@@ -124,9 +118,13 @@
124 118 $output = fopen('php://output', 'w');
125 119 header('Content-Type: text/csv');
126 120 header('Content-Disposition: attachment; filename=Booking-Event-Guests-' . $groupId . '.csv');
127 121
128 - foreach ($csvData as $row) {
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 + }
129 127 fputcsv($output, $row);
130 128 }
131 129
132 130 fclose($output); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
@@ -173,6 +171,65 @@
173 171
174 172 $calendarData = apply_filters('fluent_booking/exporting_calendar_data_json', $calendarData, $calendar);
175 173
176 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 + * Make a value safe for CSV: strip control chars and neutralize formula injection.
203 + *
204 + * @param mixed $value Cell value (string, number, or null).
205 + * @return string Safe string for fputcsv.
206 + */
207 + private function sanitizeCsvCell($value)
208 + {
209 + if (empty($value)) {
210 + return '';
211 + }
212 + $value = (string) $value;
213 + // Strip control characters (ASCII 0-31 except tab, LF, CR).
214 + $value = preg_replace('/[\x00-\x08\x0B\x0C\x0E-\x1F]/', '', $value);
215 + // Neutralize formula injection: prefix with ' so Excel/LibreOffice treat as text.
216 + $first = isset($value[0]) ? $value[0] : '';
217 + if (in_array($first, ['=', '+', '-', '@'], true)) {
218 + $value = "'" . $value;
219 + }
220 +
221 + return $value;
222 + }
223 +
224 + /**
225 + * Verify the request nonce for AJAX actions.
226 + *
227 + * @return bool
228 + */
229 + private function verifyNonce()
230 + {
231 + $nonce = isset($_REQUEST['nonce']) ? sanitize_text_field(wp_unslash($_REQUEST['nonce'])) : '';
232 +
233 + return !empty($nonce) && wp_verify_nonce($nonce, 'fluent-booking');
177 234 }
178 235 }