PluginProbe
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder / 6.2.14
Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder v6.2.14
6.2.14 6.2.13 6.2.12 6.2.10 6.2.11 6.2.9 6.2.8 6.2.7 6.2.6 6.2.5 6.2.4 6.2.3 6.2.2 3.6.22 3.6.31 3.6.40 3.6.41 3.6.42 3.6.50 3.6.51 3.6.60 3.6.61 3.6.62 3.6.64 3.6.65 All 196 releases
fluentform / app / Models / Submission.php

Submission.php in Fluent Forms – Customizable Contact Forms, Survey, Quiz, & Conversational Form Builder 6.2.14, at app/Models/Submission.php

509 lines 17.4 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace FluentForm\App\Models;
4
5 use Exception;
6 use FluentForm\App\Modules\Payments\PaymentHelper;
7 use FluentForm\App\Services\Manager\FormManagerService;
8 use FluentForm\Framework\Support\Arr;
9
10 /**
11 * Column properties resolved at runtime via the ORM's magic __get; declared
12 * here so static analysis can verify attribute access against the real schema
13 * (database/Migrations/Submissions.php).
14 *
15 * @property int $id
16 * @property int|null $form_id
17 * @property int|null $serial_number
18 * @property string|null $response
19 * @property string|null $source_url
20 * @property int|null $user_id
21 * @property string|null $status
22 * @property int $is_favourite
23 * @property string|null $browser
24 * @property string|null $device
25 * @property string|null $ip
26 * @property string|null $city
27 * @property string|null $country
28 * @property string|null $payment_status
29 * @property string|null $payment_method
30 * @property string|null $payment_type
31 * @property string|null $currency
32 * @property float|null $payment_total
33 * @property float|null $total_paid
34 * @property string|null $created_at
35 * @property string|null $updated_at
36 */
37 class Submission extends Model
38 {
39 /**
40 * The table associated with the model.
41 *
42 * @var string
43 */
44 protected $table = 'fluentform_submissions';
45
46 /**
47 * A submission is owned by a User.
48 *
49 * @return \FluentForm\Framework\Database\Orm\Relations\BelongsTo
50 */
51 public function user()
52 {
53 return $this->belongsTo(User::class, 'user_id', 'ID');
54 }
55
56 /**
57 * A submission is owned by a form.
58 *
59 * @return \FluentForm\Framework\Database\Orm\Relations\BelongsTo
60 */
61 public function form()
62 {
63 return $this->belongsTo(Form::class, 'form_id', 'id');
64 }
65
66 /**
67 * A submission has many meta.
68 *
69 * @return \FluentForm\Framework\Database\Orm\Relations\HasMany
70 */
71 public function submissionMeta()
72 {
73 return $this->hasMany(SubmissionMeta::class, 'response_id', 'id');
74 }
75
76 /**
77 * A submission has many logs.
78 *
79 * @return \FluentForm\Framework\Database\Orm\Relations\HasMany
80 */
81 public function logs()
82 {
83 return $this->hasMany(Log::class, 'source_id', 'id');
84 }
85
86 /**
87 * A submission has many entry details.
88 *
89 * @return \FluentForm\Framework\Database\Orm\Relations\HasMany
90 */
91 public function entryDetails()
92 {
93 return $this->hasMany(EntryDetails::class, 'submission_id', 'id');
94 }
95
96 /**
97 * A submission has many transactions.
98 *
99 * @return \FluentForm\Framework\Database\Orm\Relations\HasMany
100 */
101 public function transactions()
102 {
103 return $this->hasMany(Transaction::class, 'submission_id', 'id');
104 }
105
106 /**
107 * A submission has many subscriptions.
108 *
109 * @return \FluentForm\Framework\Database\Orm\Relations\HasMany
110 */
111 public function subscriptions()
112 {
113 return $this->hasMany(Subscription::class, 'submission_id', 'id');
114 }
115
116 /**
117 * A submission has many order items.
118 *
119 * @return \FluentForm\Framework\Database\Orm\Relations\HasMany
120 */
121 public function orderItems()
122 {
123 return $this->hasMany(OrderItem::class, 'submission_id', 'id');
124 }
125
126 /**
127 * Returns the column the entries listing should be sorted by.
128 * Defaults to created_at so imported entries respect their original
129 * submission date. Site owners can switch back to legacy id-based
130 * ordering via the fluentform/entries_default_sort_column filter.
131 */
132 public static function getSortColumn()
133 {
134 $column = apply_filters('fluentform/entries_default_sort_column', 'created_at');
135 $allowed = ['id', 'created_at'];
136
137 return in_array($column, $allowed, true) ? $column : 'created_at';
138 }
139
140 public function customQuery($attributes = [], $searchExtender = null)
141 {
142 $entryType = Arr::get($attributes, 'entry_type');
143 $dateRange = Arr::get($attributes, 'date_range');
144 $isFavourite = false;
145 $status = $entryType;
146
147 // We have to handle favorites separately because status and favorites are different
148 if ('favorites' === $entryType) {
149 $isFavourite = true;
150 $status = false;
151 }
152
153 $formId = Arr::get($attributes, 'form_id');
154 $startDate = Arr::get($dateRange, 0);
155 $endDate = Arr::get($dateRange, 1);
156 $search = Arr::get($attributes, 'search');
157 $sortBy = \FluentForm\App\Helpers\Helper::sanitizeOrderValue(Arr::get($attributes, 'sort_by', 'DESC'));
158
159 $wheres = [];
160 $paymentStatuses = Arr::get($attributes, 'payment_statuses');
161
162 if ($paymentStatuses && is_array($paymentStatuses)) {
163 $wheres[] = ['payment_status', $paymentStatuses];
164 }
165
166 // Sort by submission date so imported entries (which get new auto-increment ids
167 // but carry their original created_at) interleave correctly with native ones.
168 // Tie-break on id to keep pagination stable for rows sharing a timestamp.
169 // Filter lets site owners revert to legacy id-based ordering if needed.
170 $sortColumn = self::getSortColumn();
171 $query = $this->orderBy('fluentform_submissions.' . $sortColumn, $sortBy);
172 if ('id' !== $sortColumn) {
173 $query = $query->orderBy('fluentform_submissions.id', $sortBy);
174 }
175 $query = $query
176 ->when($formId, function ($q) use ($formId) {
177 return $q->where('fluentform_submissions.form_id', $formId);
178 })
179 ->when($isFavourite, function ($q) {
180 return $q->where('is_favourite', true);
181 })
182 ->where(function ($q) use ($status) {
183 $operator = '=';
184
185 if (!$status) {
186 $operator = '!=';
187 $status = 'trashed';
188 }
189
190 return $q->where('fluentform_submissions.status', $operator, $status);
191 })
192 ->when($startDate && $endDate, function ($q) use ($startDate, $endDate) {
193 $endDate .= ' 23:59:59';
194
195 return $q->where('fluentform_submissions.created_at', '>=', $startDate)
196 ->where('fluentform_submissions.created_at', '<=', $endDate);
197 })
198 ->when($search, function ($q) use ($search, $searchExtender) {
199 global $wpdb;
200 $escaped = $wpdb->esc_like($search);
201 return $q->where(function ($q) use ($escaped, $searchExtender) {
202 $q->where('fluentform_submissions.id', 'LIKE', "%{$escaped}%")
203 ->orWhere('response', 'LIKE', "%{$escaped}%")
204 ->orWhere('fluentform_submissions.status', 'LIKE', "%{$escaped}%")
205 ->orWhere('fluentform_submissions.created_at', 'LIKE', "%{$escaped}%");
206 if ($searchExtender) {
207 $searchExtender($q, $escaped);
208 }
209 });
210 })
211 ->when($wheres, function ($q) use ($wheres) {
212 foreach ($wheres as $where) {
213 if (is_array($where) && count($where) > 1) {
214 if (count($where) > 2) {
215 $column = $where[0];
216 $operator = $where[1];
217 $value = $where[2];
218 } else {
219 $column = $where[0];
220 $operator = '=';
221 $value = $where[1];
222 }
223
224 if (is_array($value)) {
225 return $q->whereIn($column, $value);
226 } else {
227 return $q->where($column, $operator, $value);
228 }
229 }
230 }
231 });
232
233 return $query;
234 }
235
236 public function paginateEntries($attributes = [])
237 {
238 $formId = Arr::get($attributes, 'form_id');
239 $allowFormIds = FormManagerService::getUserAllowedFormsScope();
240 $query = $this->customQuery($attributes);
241 $query = $query->when(false !== $allowFormIds, function ($q) use ($allowFormIds) {
242 return $q->whereIn('fluentform_submissions.form_id', $allowFormIds ?: [0]);
243 });
244 if (Arr::get($attributes, 'advanced_filter')) {
245 $query = apply_filters('fluentform/apply_entries_advance_filter', $query, $attributes);
246 }
247 $response = $query->paginate();
248 $response = apply_filters_deprecated(
249 'fluentform_get_raw_responses',
250 [
251 $response,
252 $formId
253 ],
254 FLUENTFORM_FRAMEWORK_UPGRADE,
255 'fluentform/get_raw_responses',
256 'Use fluentform/get_raw_responses instead of fluentform_get_raw_responses.'
257 );
258
259 return apply_filters('fluentform/get_raw_responses', $response, $formId);
260 }
261
262 public function findAdjacentSubmission($attributes = [])
263 {
264 $sortBy = Arr::get($attributes, 'sort_by', 'DESC');
265
266 $direction = Arr::get($attributes, 'direction', 'next');
267
268 $operator = 'ASC' === $sortBy && 'previous' === $direction ? '>' : '<';
269
270 if ('previous' === $direction) {
271 $operator = 'ASC' === $sortBy ? '>' : '<';
272 } else {
273 $operator = 'ASC' === $sortBy ? '<' : '>';
274 $attributes['sort_by'] = 'ASC' === $sortBy ? 'DESC' : 'ASC';
275 }
276
277 $entryId = Arr::get($attributes, 'entry_id');
278
279 $columns = Arr::get($attributes, 'columns', 'id');
280
281 $query = $this->customQuery($attributes);
282
283 // Adjacency must match customQuery's sort order. Compare on the same
284 // (sortColumn, id) row-tuple the listing orders by, so Next/Prev walk
285 // in display order regardless of which sort column the filter selects.
286 // When sortColumn is id the tuple degenerates to a simple id compare.
287 $sortColumn = self::getSortColumn();
288 // Re-assert whitelist; $sortColumn is interpolated into whereRaw below.
289 if (!in_array($sortColumn, ['id', 'created_at'], true)) {
290 $sortColumn = 'created_at';
291 }
292 $current = static::select(['id', $sortColumn])->find($entryId);
293 if (!$current) {
294 return apply_filters('fluentform/next_submission', null, $entryId, $attributes);
295 }
296
297 global $wpdb;
298 $table = $wpdb->prefix . 'fluentform_submissions';
299 $submission = $query->select($columns)
300 ->whereRaw(
301 "({$table}.{$sortColumn}, {$table}.id) {$operator} (?, ?)",
302 [$current->{$sortColumn}, $entryId]
303 )
304 ->first();
305
306 return apply_filters('fluentform/next_submission', $submission, $entryId, $attributes);
307 }
308
309 public function countByGroup($formId)
310 {
311 $statuses = $this->selectRaw('status, COUNT(*) as count')
312 ->where('form_id', $formId)
313 ->groupBy('status')
314 ->get();
315
316 $counts = [];
317
318 foreach ($statuses as $status) {
319 $counts[$status->status] = (int) $status->count;
320 }
321
322 $counts['all'] = array_sum($counts);
323
324 if (isset($counts['trashed'])) {
325 $counts['all'] -= $counts['trashed'];
326 }
327
328 $favorites = $this->where('form_id', $formId)
329 ->where('is_favourite', 1)
330 ->where('status', '!=', 'trashed')
331 ->count();
332
333 $counts['favorites'] = $favorites;
334
335 return array_merge([
336 'unread' => 0,
337 'read' => 0,
338 'spam' => 0,
339 'trashed' => 0,
340 ], $counts);
341 }
342
343 public function amend($id, $data = [])
344 {
345 $this->where('id', $id)->update($data);
346 }
347
348 public static function remove($submissionIds, $formId = null)
349 {
350 // Fail-closed scope guard: $formId scopes every delete to its owning form;
351 // a missing scope throws rather than ever deleting unscoped.
352 if (empty($formId)) {
353 throw new \InvalidArgumentException('Submission::remove() requires a form id to scope the deletion.');
354 }
355
356 $submissionIds = static::where('form_id', $formId)
357 ->whereIn('id', (array) $submissionIds)
358 ->pluck('id')
359 ->all();
360
361 if (!$submissionIds) {
362 return;
363 }
364
365 static::whereIn('id', $submissionIds)->delete();
366
367 SubmissionMeta::whereIn('response_id', $submissionIds)->delete();
368
369 Log::whereIn('source_id', $submissionIds)
370 ->where('source_type', 'submission_item')
371 ->delete();
372
373 EntryDetails::whereIn('submission_id', $submissionIds)->delete();
374
375 try {
376 if (PaymentHelper::hasPaymentSettings()) {
377 OrderItem::whereIn('submission_id', $submissionIds)->delete();
378 Transaction::whereIn('submission_id', $submissionIds)->delete();
379 Subscription::whereIn('submission_id', $submissionIds)->delete();
380 }
381
382 wpFluent()->table('ff_scheduled_actions')
383 ->whereIn('origin_id', $submissionIds)
384 ->where('type', 'submission_action')
385 ->delete();
386
387 } catch (Exception $exception) {
388 // ...
389 }
390 }
391
392 public function allSubmissions($attributes = []) {
393 $searchExtender = function ($q, $escaped) {
394 $q->orWhereHas('form', function ($q) use ($escaped) {
395 $q->where('title', 'LIKE', "%{$escaped}%");
396 });
397 };
398 $customQuery = $this->customQuery($attributes, $searchExtender);
399 $allowFormIds = FormManagerService::getUserAllowedFormsScope();
400
401 $result = $customQuery
402 ->with([
403 'form' => function ($q) {
404 $q->select(['id', 'title']);
405 }
406 ])
407 ->select(['id', 'form_id', 'status', 'created_at', 'browser', 'currency', 'total_paid'])
408 ->when(false !== $allowFormIds, function ($q) use ($allowFormIds){
409 return $q->whereIn('form_id', $allowFormIds ?: [0]);
410 })
411 ->paginate()
412 ->toArray();
413
414 $useHumanDate = apply_filters('fluentform/entries_human_date', false);
415
416 foreach ($result['data'] as &$entry) {
417 $entry['entry_url'] = admin_url('admin.php?page=fluent_forms&route=entries&form_id=' . $entry['form_id'] . '#/entries/' . $entry['id']);
418
419 if ($useHumanDate) {
420 $entry['human_date'] = human_time_diff(strtotime($entry['created_at']), strtotime(current_time('mysql')));
421 }
422 }
423
424 $result['available_forms'] = $this->availableForms();
425
426 return $result;
427 }
428
429 public function availableForms()
430 {
431 $form = new Form();
432 if (false !== ($allowForms = FormManagerService::getUserAllowedFormsScope())) {
433 return $form->select('id', 'title')->whereIn('id', $allowForms ?: [0])->get();
434 }
435 return $form->select('id', 'title')->get();
436 }
437
438 public static function report($attributes)
439 {
440 $from = date('Y-m-d H:i:s', strtotime('-30 days'));
441 $to = date('Y-m-d H:i:s', strtotime('+1 days'));
442 $formId = Arr::get($attributes, 'form_id');
443 $allowFormIds = FormManagerService::getUserAllowedFormsScope();
444 $status = Arr::get($attributes, 'entry_status');
445 $start = Arr::get($attributes, 'date_range.0', '');
446 $end = Arr::get($attributes, 'date_range.1', '');
447 $dateRange = Arr::get($attributes, 'date_range');
448
449 if ('all' === $dateRange) {
450 $firstItem = self::orderBy('created_at', 'ASC')
451 ->when(false !== $allowFormIds, function ($q) use ($allowFormIds) {
452 return $q->whereIn('form_id', $allowFormIds ?: [0]);
453 })
454 ->when($formId, function ($q) use ($formId) {
455 return $q->where('form_id', $formId);
456 })
457 ->when($status, function ($q2) use ($status) {
458 return $q2->where('status', $status);
459 })
460 ->first();
461
462 if ($firstItem && $firstItem->created_at) {
463 $from = date('Y-m-d H:i:s', strtotime($firstItem->created_at));
464 $to = date('Y-m-d H:i:s');
465 }
466 }
467
468
469 if ($start && $startTime = strtotime($start)) {
470 $from = date('Y-m-d 00:00:00', $startTime);
471 }
472
473 if ($end && $endTime = strtotime($end)) {
474 $to = date('Y-m-d 23:59:59', $endTime);
475 }
476
477 $period = new \DatePeriod(new \DateTime($from), new \DateInterval('P1D'), new \DateTime($to));
478
479 $range = [];
480
481 foreach ($period as $date) {
482 $range[$date->format('Y-m-d')] = 0;
483 }
484
485 $items = self::selectRaw('DATE(created_at) AS date')
486 ->selectRaw('COUNT(id) AS count')
487 ->whereBetween('created_at', [$from, $to])
488 ->groupBy('date')
489 ->orderBy('date', 'ASC')
490 ->when(false !== $allowFormIds, function ($q) use ($allowFormIds) {
491 return $q->whereIn('form_id', $allowFormIds ?: [0]);
492 })
493 ->when($formId, function ($q) use ($formId) {
494 return $q->where('form_id', $formId);
495 })
496 ->when($status, function ($q2) use ($status) {
497 return $q2->where('status', $status);
498 })
499 ->get();
500
501
502 foreach ($items as $item) {
503 $range[$item->date] = $item->count;
504 }
505
506 return $range;
507 }
508 }
509