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 +166 -38 1.5.02 → 2.5.0 View file →
@@ -1,21 +1,54 @@
1 1 <?php
2 2
3 3 namespace FluentBooking\App\Hooks\Handlers;
4 4
5 -
6 5 use FluentBooking\App\Models\Booking;
6 +use FluentBooking\App\Models\Calendar;
7 +use FluentBooking\App\Models\Availability;
7 8 use FluentBooking\App\Services\PermissionManager;
8 9
9 10 class DataExporter
10 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 +
11 44 public function exportBookingHosts()
12 45 {
13 - if (!PermissionManager::hasAllCalendarAccess()) {
14 - 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);
15 48 }
16 49
17 - $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
18 51
19 52 if (!$groupId) {
20 53 die(esc_html__('Please provide Group ID', 'fluent-booking'));
21 54 }
@@ -21,8 +54,16 @@
21 54 }
22 55
23 56 $attendees = Booking::where('group_id', $groupId)->get();
24 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 +
25 66 $csvData[] = [
26 67 'First Name',
27 68 'Last Name',
28 69 'Email',
@@ -50,43 +91,25 @@
50 91 ];
51 92
52 93 foreach ($attendees as $attendee) {
53 94 $row = [
54 - $attendee->first_name,
55 - $attendee->last_name,
56 - $attendee->email,
57 - $attendee->message,
58 - $attendee->getLocationAsText(),
59 - $attendee->source,
60 - $attendee->booking_type,
61 - $attendee->status,
62 - $attendee->source_url,
63 - $attendee->slot_minutes,
64 - $attendee->start_time,
65 - $attendee->end_time,
66 - $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),
67 108 ];
68 109
69 - if ($attendee->payment_status) {
70 - $order = $attendee->payment_order;
71 - if ($order) {
72 - $order->load(['items', 'transaction']);
73 - $row[] = $order->status;
74 - $row[] = $order->payment_method;
75 - $row[] = $order->currency;
76 - $row[] = $order->total_amount / 100;
77 - $row[] = $order->created_at;
78 - $row[] = $order->transaction->id;
79 - $row[] = $order->transaction->vendor_charge_id;
80 - $row[] = $order->transaction->payment_method;
81 - $row[] = $order->transaction->status;
82 - $row[] = $order->transaction->total / 100;
83 - $row[] = $order->transaction->created_at;
84 - }
85 - } else {
86 - // Fill empty columns for payment related data if payment_status is false
87 - $row = array_pad($row, 11, '');
88 - }
110 + $paymentOrder = $attendee->payment_status ? $attendee->payment_order : null;
111 + $row = array_merge($row, $this->buildPaymentColumns($paymentOrder));
89 112
90 113 $csvData[] = $row;
91 114 }
92 115
@@ -95,9 +118,13 @@
95 118 $output = fopen('php://output', 'w');
96 119 header('Content-Type: text/csv');
97 120 header('Content-Disposition: attachment; filename=Booking-Event-Guests-' . $groupId . '.csv');
98 121
99 - 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 + }
100 127 fputcsv($output, $row);
101 128 }
102 129
103 130 fclose($output); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
@@ -103,5 +130,106 @@
103 130 fclose($output); // phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_operations_fclose
104 131 exit();
105 132 }
106 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 + * 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');
234 + }
107 235 }