PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.27
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.27
6.35 6.34 6.33.1 6.33 6.32.1 6.32 6.31 6.25 6.25.1 6.26 6.26.1 6.27 6.28 6.29 6.3 6.3.1 6.3.2 6.30 6.4 6.4.1 6.4.2 6.5 6.5.1 6.5.2 6.5.3 All 141 releases
formidable / classes / models / FrmEntry.php

FrmEntry.php in Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More 6.27, at classes/models/FrmEntry.php

1,286 lines 32.7 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 if ( ! defined( 'ABSPATH' ) ) {
3 die( 'You are not allowed to call this page directly.' );
4 }
5
6 class FrmEntry {
7
8 /**
9 * @since 6.16.3
10 *
11 * @var array
12 */
13 private static $unique_id_match_checks = array();
14
15 /**
16 * Create a new entry
17 *
18 * @param array $values
19 *
20 * @return bool|int $entry_id
21 */
22 public static function create( $values ) {
23 return self::create_entry( $values, 'standard' );
24 }
25
26 /**
27 * Create a new entry with some differences depending on type
28 *
29 * @param array $values
30 * @param string $type
31 *
32 * @return bool|int $entry_id
33 */
34 private static function create_entry( $values, $type ) {
35 $new_values = self::before_insert_entry_in_database( $values, $type );
36
37 // Don't check XML entries for duplicates
38 if ( $type !== 'xml' && self::is_duplicate( $new_values, $values ) ) {
39 return false;
40 }
41
42 return self::continue_to_create_entry( $values, $new_values );
43 }
44
45 /**
46 * Flag the memoized unique id check after a new entry is created.
47 * This prevents possibly DB requests and helps avoid issues when creating repeater entries.
48 *
49 * @since 6.16.3
50 *
51 * @param string $unique_id
52 *
53 * @return void
54 */
55 private static function flag_new_unique_key( $unique_id ) {
56 if ( ! isset( self::$unique_id_match_checks[ $unique_id ] ) ) {
57 self::$unique_id_match_checks[ $unique_id ] = false;
58 }
59 }
60
61 /**
62 * Check for duplicate entries created in the last minute
63 *
64 * @param array $new_values New values.
65 * @param array $values Values.
66 *
67 * @return bool
68 */
69 public static function is_duplicate( $new_values, $values ) {
70 $duplicate_entry_time = apply_filters( 'frm_time_to_check_duplicates', 60, $new_values );
71
72 if ( false === self::is_duplicate_check_needed( $values, $duplicate_entry_time ) ) {
73 return false;
74 }
75
76 if ( self::maybe_check_for_unique_id_match( $values, $new_values['created_at'] ) ) {
77 return true;
78 }
79
80 $check_val = $new_values;
81 $check_val['created_at >'] = gmdate( 'Y-m-d H:i:s', strtotime( $new_values['created_at'] ) - absint( $duplicate_entry_time ) );
82
83 unset( $check_val['created_at'], $check_val['updated_at'], $check_val['is_draft'], $check_val['id'], $check_val['item_key'] );
84
85 // phpcs:ignore Universal.Operators.StrictComparisons
86 if ( $new_values['item_key'] == $new_values['name'] ) {
87 unset( $check_val['name'] );
88 }
89
90 $check_val = apply_filters( 'frm_duplicate_check_val', $check_val );
91
92 global $wpdb;
93 $entry_exists = FrmDb::get_col( $wpdb->prefix . 'frm_items', $check_val, 'id', array( 'order_by' => 'created_at DESC' ) );
94
95 if ( ! $entry_exists || ! isset( $values['item_meta'] ) ) {
96 return false;
97 }
98
99 global $frm_vars;
100 $frm_vars['checking_duplicates'] = true;
101 $is_duplicate = false;
102
103 foreach ( $entry_exists as $entry_exist ) {
104 $is_duplicate = true;
105
106 // make sure it's a duplicate
107 $metas = FrmEntryMeta::get_entry_meta_info( $entry_exist );
108 $field_metas = array();
109
110 foreach ( $metas as $meta ) {
111 if ( 0 === (int) $meta->field_id ) {
112 continue;
113 }
114
115 $field_metas[ $meta->field_id ] = $meta->meta_value;
116 }
117
118 $filtered_vals = array_filter( $values['item_meta'] );
119 $filtered_vals = self::convert_values_to_their_saved_value( $filtered_vals, $entry_exist );
120 $field_metas = array_filter( $field_metas );
121
122 // If prev entry is empty and current entry is not, they are not duplicates
123 if ( ! $field_metas && $filtered_vals ) {
124 return false;
125 }
126
127 // compare serialized values and not arrays
128 $new_meta = array_map( 'maybe_serialize', $filtered_vals );
129
130 if ( $field_metas === $new_meta ) {
131 $is_duplicate = true;
132 break;
133 }
134
135 if ( count( $field_metas ) !== count( $new_meta ) ) {
136 // TODO: compare values saved in the post also
137 $is_duplicate = false;
138 continue;
139 }
140
141 $diff = array_diff_assoc( $field_metas, $new_meta );
142
143 foreach ( $diff as $meta_value ) {
144 if ( ! empty( $meta_value ) ) {
145 $is_duplicate = false;
146 }
147 }
148
149 if ( $is_duplicate ) {
150 break;
151 }
152 }//end foreach
153
154 $frm_vars['checking_duplicates'] = false;
155
156 return $is_duplicate;
157 }
158
159 /**
160 * @since 6.16.3
161 *
162 * @param array $values POST request data.
163 * @param string $created_at The timestamp of the entry we are checking for.
164 *
165 * @return bool
166 */
167 private static function maybe_check_for_unique_id_match( $values, $created_at ) {
168 if ( ! self::should_check_for_unique_id_match() ) {
169 return false;
170 }
171
172 if ( empty( $values['unique_id'] ) ) {
173 return false;
174 }
175
176 $unique_id = sanitize_key( $values['unique_id'] );
177
178 if ( ! $unique_id ) {
179 // Only continue if a unique ID was generated on form submit.
180 return false;
181 }
182
183 if ( isset( self::$unique_id_match_checks[ $unique_id ] ) ) {
184 return self::$unique_id_match_checks[ $unique_id ];
185 }
186
187 $timestamp = strtotime( $created_at );
188
189 if ( false === $timestamp ) {
190 $timestamp = time();
191 }
192
193 self::$unique_id_match_checks[ $unique_id ] = (bool) FrmDb::get_var(
194 'frm_item_metas',
195 array(
196 'field_id' => 0,
197 'meta_value' => serialize( compact( 'unique_id' ) ),
198 'created_at >' => gmdate( 'Y-m-d H:i:s', $timestamp - MONTH_IN_SECONDS ),
199 ),
200 'id'
201 );
202
203 return self::$unique_id_match_checks[ $unique_id ];
204 }
205
206 /**
207 * @since 6.16.3
208 *
209 * @return bool
210 */
211 private static function should_check_for_unique_id_match() {
212 /**
213 * Allow users to opt out of the DB query, in case it causes performance issues.
214 *
215 * @since 6.16.3
216 *
217 * @param bool $should_extend
218 */
219 $should_check = apply_filters( 'frm_check_for_unique_id_match', true );
220 return (bool) $should_check;
221 }
222
223 /**
224 * Convert form data to the actual value that would be saved into the database.
225 *
226 * This is important for the duplicate check as something like 'a:2:{s:5:"typed";s:0:"";s:6:"output";s:0:"";}'
227 * (a signature value) is actually an empty string and does not get saved.
228 *
229 * @param array $filter_vals
230 * @param int $entry_id
231 *
232 * @return array
233 */
234 private static function convert_values_to_their_saved_value( $filter_vals, $entry_id ) {
235 $reduced = array();
236
237 foreach ( $filter_vals as $field_id => $value ) {
238 $field = FrmFieldFactory::get_field_object( $field_id );
239 $reduced[ $field_id ] = $field->get_value_to_save( $value, array( 'entry_id' => $entry_id ) );
240 $reduced[ $field_id ] = $field->set_value_before_save( $reduced[ $field_id ] );
241
242 if ( '' === $reduced[ $field_id ] || ( is_array( $reduced[ $field_id ] ) && 0 === count( $reduced[ $field_id ] ) ) ) {
243 unset( $reduced[ $field_id ] );
244 }
245 }
246
247 return $reduced;
248 }
249
250 /**
251 * Determine if an entry needs to be checked as a possible duplicate
252 *
253 * @since 2.0.23
254 *
255 * @param array $values
256 * @param int $duplicate_entry_time
257 *
258 * @return bool
259 */
260 private static function is_duplicate_check_needed( $values, $duplicate_entry_time ) {
261 // If time for checking duplicates is set to an empty value, don't check for duplicates
262 if ( ! $duplicate_entry_time ) {
263 return false;
264 }
265
266 // If CSV is importing, don't check for duplicates
267 if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
268 return false;
269 }
270
271 // If repeating field entries are getting created, don't check for duplicates
272 return empty( $values['parent_form_id'] );
273 }
274
275 /**
276 * @param int|string $id
277 *
278 * @return false|int
279 */
280 public static function duplicate( $id ) {
281 global $wpdb;
282
283 $values = self::getOne( $id );
284 $new_values = array();
285 $new_values['item_key'] = FrmAppHelper::get_unique_key( '', $wpdb->prefix . 'frm_items', 'item_key' );
286 $new_values['name'] = $values->name;
287 $new_values['is_draft'] = $values->is_draft;
288 $new_values['user_id'] = (int) $values->user_id;
289 $new_values['updated_by'] = (int) $values->user_id;
290 $new_values['form_id'] = $values->form_id ? (int) $values->form_id : null;
291 $new_values['created_at'] = current_time( 'mysql', 1 );
292 $new_values['updated_at'] = $new_values['created_at'];
293
294 $query_results = $wpdb->insert( $wpdb->prefix . 'frm_items', $new_values );
295
296 if ( ! $query_results ) {
297 return false;
298 }
299
300 $entry_id = $wpdb->insert_id;
301
302 global $frm_vars;
303
304 if ( ! isset( $frm_vars['saved_entries'] ) ) {
305 $frm_vars['saved_entries'] = array();
306 }
307 $frm_vars['saved_entries'][] = (int) $entry_id;
308
309 FrmEntryMeta::duplicate_entry_metas( $id, $entry_id );
310 self::clear_cache();
311
312 do_action( 'frm_after_duplicate_entry', $entry_id, $new_values['form_id'], array( 'old_id' => $id ) );
313
314 return $entry_id;
315 }
316
317 /**
318 * Update an entry (not via XML)
319 *
320 * @param int $id
321 * @param array $values
322 *
323 * @return bool|int $update_results
324 */
325 public static function update( $id, $values ) {
326 return self::update_entry( $id, $values, 'standard' );
327 }
328
329 /**
330 * Update an entry with some differences depending on the update type
331 *
332 * @since 2.0.16
333 *
334 * @param int $id
335 * @param array $values
336 * @param string $update_type
337 *
338 * @return bool|int $query_results
339 */
340 private static function update_entry( $id, $values, $update_type ) {
341 global $wpdb;
342
343 $update = self::before_update_entry( $id, $values, $update_type );
344
345 if ( ! $update ) {
346 return false;
347 }
348
349 $new_values = self::package_entry_to_update( $id, $values );
350 $query_results = $wpdb->update( $wpdb->prefix . 'frm_items', $new_values, compact( 'id' ) );
351
352 self::after_update_entry( $query_results, $id, $values, $new_values );
353
354 return $query_results;
355 }
356
357 /**
358 * Delete an entry.
359 *
360 * @param int|string $id
361 *
362 * @return bool True on success, false if nothing was deleted.
363 */
364 public static function destroy( $id ) {
365 global $wpdb;
366 $id = (int) $id;
367
368 // Item meta is required for conditional logic in actions with 'delete' events.
369 $entry = self::getOne( $id, true );
370
371 if ( ! $entry ) {
372 return false;
373 }
374
375 /**
376 * Trigger an action to run custom logic before the entry is deleted.
377 *
378 * @param int $id The id of the entry that was destroyed.
379 * @param stdClass $entry The entry object.
380 */
381 do_action( 'frm_before_destroy_entry', $id, $entry );
382
383 $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'frm_item_metas WHERE item_id=%d', $id ) );
384 $result = $wpdb->query( $wpdb->prepare( 'DELETE FROM ' . $wpdb->prefix . 'frm_items WHERE id=%d', $id ) );
385
386 self::clear_cache();
387
388 /**
389 * Trigger an action to run custom logic after the entry is deleted.
390 * Use this hook if you need to update caching after an entry is deleted.
391 *
392 * @since 5.4.1
393 *
394 * @param int $id The id of the entry that was destroyed.
395 * @param stdClass $entry The entry object.
396 */
397 do_action( 'frm_after_destroy_entry', $id, $entry );
398
399 return $result;
400 }
401
402 /**
403 * @param int $id
404 * @param mixed $value
405 * @param int|string $form_id
406 *
407 * @return false|int
408 */
409 public static function update_form( $id, $value, $form_id ) {
410 global $wpdb;
411 $form_id = isset( $value ) ? $form_id : null;
412 $result = $wpdb->update( $wpdb->prefix . 'frm_items', array( 'form_id' => $form_id ), array( 'id' => $id ) );
413
414 if ( $result ) {
415 self::clear_cache();
416 }
417
418 return $result;
419 }
420
421 /**
422 * Clear entry caching
423 * Called when an entry is changed
424 *
425 * @since 2.0.5
426 *
427 * @return void
428 */
429 public static function clear_cache() {
430 FrmDb::cache_delete_group( 'frm_entry' );
431 FrmDb::cache_delete_group( 'frm_item' );
432 FrmDb::cache_delete_group( 'frm_entry_meta' );
433 FrmDb::cache_delete_group( 'frm_item_meta' );
434 }
435
436 /**
437 * After switching to the wp_loaded hook for processing entries,
438 * we can no longer use 'name', but check it as a fallback
439 *
440 * @since 2.0.11
441 *
442 * @param array $values
443 * @param array|string $default
444 *
445 * @return string
446 */
447 public static function get_new_entry_name( $values, $default = '' ) {
448 $name = $values['item_name'] ?? $values['name'] ?? $default;
449
450 if ( is_array( $name ) ) {
451 $name = reset( $name );
452 }
453
454 return $name;
455 }
456
457 /**
458 * If $entry is numeric, get the entry object
459 *
460 * @since 2.0.9
461 *
462 * @param int|object $entry By reference.
463 *
464 * @return void
465 */
466 public static function maybe_get_entry( &$entry ) {
467 if ( $entry && is_numeric( $entry ) ) {
468 $entry = self::getOne( $entry );
469 } elseif ( ! $entry || 'false' === $entry ) {
470 $entry = false;
471 }
472 }
473
474 /**
475 * @param int|string $id
476 * @param bool $meta
477 *
478 * @return object|null
479 */
480 public static function getOne( $id, $meta = false ) {
481 global $wpdb;
482
483 $query = "SELECT it.*, fr.name as form_name, fr.form_key as form_key FROM {$wpdb->prefix}frm_items it
484 LEFT OUTER JOIN {$wpdb->prefix}frm_forms fr ON it.form_id=fr.id WHERE ";
485
486 $query .= is_numeric( $id ) ? 'it.id=%d' : 'it.item_key=%s';
487 $query_args = array( $id );
488 $query = $wpdb->prepare( $query, $query_args ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
489
490 if ( ! $meta ) {
491 $entry = FrmDb::check_cache( $id . '_nometa', 'frm_entry', $query, 'get_row' );
492 self::prepare_entry( $entry );
493 return $entry;
494 }
495
496 $entry = FrmDb::check_cache( $id, 'frm_entry' );
497
498 if ( $entry !== false ) {
499 self::prepare_entry( $entry );
500 return $entry;
501 }
502
503 $entry = $wpdb->get_row( $query ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
504 $entry = self::get_meta( $entry );
505 self::prepare_entry( $entry );
506
507 return $entry;
508 }
509
510 /**
511 * @since 4.02.03
512 *
513 * @param object $entry
514 *
515 * @return void
516 */
517 private static function prepare_entry( &$entry ) {
518 if ( ! $entry ) {
519 return;
520 }
521
522 FrmAppHelper::unserialize_or_decode( $entry->description );
523 // TODO: Remove slashes on input only, not output.
524 $entry = wp_unslash( $entry );
525 }
526
527 /**
528 * @since 4.02.03
529 *
530 * @param array $entries
531 *
532 * @return void
533 */
534 private static function prepare_entries( &$entries ) {
535 foreach ( $entries as $k => $entry ) {
536 self::prepare_entry( $entry );
537 $entries[ $k ] = $entry;
538 }
539 }
540
541 /**
542 * @param object|null $entry
543 *
544 * @return object|null
545 */
546 public static function get_meta( $entry ) {
547 if ( ! $entry ) {
548 return $entry;
549 }
550
551 global $wpdb;
552 $metas = FrmDb::get_results(
553 $wpdb->prefix . 'frm_item_metas m LEFT JOIN ' . $wpdb->prefix . 'frm_fields f ON m.field_id=f.id',
554 array(
555 'item_id' => $entry->id,
556 'field_id !' => 0,
557 ),
558 'field_id, meta_value, field_key, item_id, f.type'
559 );
560
561 $entry->metas = array();
562
563 $include_key = apply_filters( 'frm_include_meta_keys', false, array( 'form_id' => $entry->form_id ) );
564
565 foreach ( $metas as $meta_val ) {
566 FrmFieldsHelper::prepare_field_value( $meta_val->meta_value, $meta_val->type );
567
568 if ( (int) $meta_val->item_id === (int) $entry->id ) {
569 $entry->metas[ $meta_val->field_id ] = $meta_val->meta_value;
570
571 if ( $include_key ) {
572 $entry->metas[ $meta_val->field_key ] = $entry->metas[ $meta_val->field_id ];
573 }
574 continue;
575 }
576
577 // include sub entries in an array
578 if ( ! isset( $entry->metas[ $meta_val->field_id ] ) ) {
579 $entry->metas[ $meta_val->field_id ] = array();
580 }
581
582 $entry->metas[ $meta_val->field_id ][] = $meta_val->meta_value;
583
584 unset( $meta_val );
585 }//end foreach
586 unset( $metas );
587
588 FrmDb::set_cache( $entry->id, $entry, 'frm_entry' );
589
590 return $entry;
591 }
592
593 /**
594 * @param string $id
595 *
596 * @return bool
597 */
598 public static function exists( $id ) {
599 global $wpdb;
600
601 if ( FrmDb::check_cache( $id, 'frm_entry' ) ) {
602 return true;
603 }
604
605 $where = is_numeric( $id ) ? array( 'id' => $id ) : array( 'item_key' => $id );
606 $id = FrmDb::get_var( $wpdb->prefix . 'frm_items', $where );
607
608 return $id && $id > 0;
609 }
610
611 /**
612 * @param array|string $where
613 * @param string $order_by
614 * @param string $limit
615 * @param bool $meta
616 * @param bool $inc_form
617 *
618 * @return array
619 */
620 public static function getAll( $where, $order_by = '', $limit = '', $meta = false, $inc_form = true ) {
621 global $wpdb;
622
623 $limit = FrmDb::esc_limit( $limit );
624 $cache_key = FrmAppHelper::maybe_json_encode( $where ) . $order_by . $limit . $inc_form;
625 $entries = wp_cache_get( $cache_key, 'frm_entry' );
626
627 if ( false === $entries ) {
628 $fields = 'it.id, it.item_key, it.name, it.ip, it.form_id, it.post_id, it.user_id, it.parent_item_id, it.updated_by, it.created_at, it.updated_at, it.is_draft, it.description'; // phpcs:ignore SlevomatCodingStandard.Files.LineLength.LineTooLong
629 $table = $wpdb->prefix . 'frm_items it ';
630
631 if ( $inc_form ) {
632 $fields = 'it.*, fr.name as form_name,fr.form_key as form_key';
633 $table .= 'LEFT OUTER JOIN ' . $wpdb->prefix . 'frm_forms fr ON it.form_id=fr.id ';
634 }
635
636 if ( preg_match( '/ meta_([0-9]+)/', $order_by, $order_matches ) ) {
637 $fields .= self::sort_by_field( $order_matches[1] );
638 unset( $order_matches );
639 }
640
641 // prepare the query
642 $query = 'SELECT ' . $fields . ' FROM ' . $table . FrmDb::prepend_and_or_where( ' WHERE ', $where ) . $order_by . $limit;
643
644 $entries = $wpdb->get_results( $query, OBJECT_K ); // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared
645 unset( $query );
646
647 FrmDb::set_cache( $cache_key, $entries, 'frm_entry' );
648 }//end if
649
650 if ( ! $meta || ! $entries ) {
651 self::prepare_entries( $entries );
652 return $entries;
653 }
654 unset( $meta );
655
656 if ( ! is_array( $where ) && preg_match( '/^it\.form_id=\d+$/', $where ) ) {
657 $where = array( 'it.form_id' => substr( $where, 11 ) );
658 }
659
660 $meta_where = array( 'field_id !' => 0 );
661
662 // phpcs:ignore Universal.Operators.StrictComparisons
663 if ( $limit == '' && is_array( $where ) && count( $where ) === 1 && isset( $where['it.form_id'] ) ) {
664 $meta_where['fi.form_id'] = $where['it.form_id'];
665 } else {
666 $meta_where['item_id'] = array_keys( $entries );
667 }
668
669 $metas = FrmDb::get_results(
670 $wpdb->prefix . 'frm_item_metas it LEFT OUTER JOIN ' . $wpdb->prefix . 'frm_fields fi ON it.field_id = fi.id',
671 $meta_where,
672 'item_id, meta_value, field_id, field_key, form_id, fi.type'
673 );
674
675 unset( $meta_where );
676
677 if ( ! $metas ) {
678 self::prepare_entries( $entries );
679 return $entries;
680 }
681
682 foreach ( $metas as $m_key => $meta_val ) {
683 if ( ! isset( $entries[ $meta_val->item_id ] ) ) {
684 continue;
685 }
686
687 if ( ! isset( $entries[ $meta_val->item_id ]->metas ) ) {
688 $entries[ $meta_val->item_id ]->metas = array();
689 }
690
691 FrmFieldsHelper::prepare_field_value( $meta_val->meta_value, $meta_val->type );
692 $entries[ $meta_val->item_id ]->metas[ $meta_val->field_id ] = $meta_val->meta_value;
693 unset( $m_key, $meta_val );
694 }
695
696 if ( ! FrmAppHelper::prevent_caching() ) {
697 foreach ( $entries as $entry ) {
698 FrmDb::set_cache( $entry->id, $entry, 'frm_entry' );
699 unset( $entry );
700 }
701 }
702
703 self::prepare_entries( $entries );
704 return $entries;
705 }
706
707 /**
708 * @param int|string $field_id
709 *
710 * @return string
711 */
712 private static function sort_by_field( $field_id ) {
713 global $wpdb;
714 $field_id = (int) $field_id;
715 $field_options = FrmDb::get_var( 'frm_fields', array( 'id' => $field_id ), 'field_options' );
716 FrmAppHelper::unserialize_or_decode( $field_options );
717
718 if ( empty( $field_options['post_field'] ) ) {
719 $sort = ', (SELECT meta_value FROM ' . $wpdb->prefix . 'frm_item_metas WHERE field_id = ' . $field_id . ' AND item_id = it.id) as meta_' . $field_id;
720 } else {
721 $sort = '';
722 }
723
724 return apply_filters( 'frm_handle_field_column_sort', $sort, $field_id, $field_options );
725 }
726
727 // Pagination Methods
728 /**
729 * @param array|int|string $where If int, use the form id.
730 *
731 * @return int|string
732 */
733 public static function getRecordCount( $where = '' ) {
734 global $wpdb;
735 $table_join = $wpdb->prefix . 'frm_items it JOIN ' . $wpdb->prefix . 'frm_forms fr ON it.form_id=fr.id';
736
737 if ( is_numeric( $where ) ) {
738 $table_join = 'frm_items';
739 $where = array( 'form_id' => $where );
740 }
741
742 if ( is_array( $where ) ) {
743 return FrmDb::get_count( $table_join, $where );
744 }
745
746 $cache_key = 'count_' . FrmAppHelper::maybe_json_encode( $where );
747 $query = 'SELECT COUNT(*) FROM ' . $table_join . FrmDb::prepend_and_or_where( ' WHERE ', $where );
748
749 return FrmDb::check_cache( $cache_key, 'frm_entry', $query, 'get_var' );
750 }
751
752 /**
753 * @param int|string $p_size
754 * @param array|int|string $where
755 *
756 * @return int
757 */
758 public static function getPageCount( $p_size, $where = '' ) {
759 $p_size = (int) $p_size;
760 $count = 1;
761
762 if ( $p_size ) {
763 if ( ! is_numeric( $where ) ) {
764 $where = self::getRecordCount( $where );
765 }
766
767 $count = ceil( (int) $where / $p_size );
768 }
769
770 return $count;
771 }
772
773 /**
774 * Prepare the data before inserting it into the database
775 *
776 * @since 2.0.16
777 *
778 * @param array $values
779 * @param string $type
780 *
781 * @return array $new_values
782 */
783 private static function before_insert_entry_in_database( &$values, $type ) {
784 self::sanitize_entry_post( $values );
785
786 if ( $type !== 'xml' ) {
787 $values = apply_filters( 'frm_pre_create_entry', $values );
788 }
789
790 return self::package_entry_data( $values );
791 }
792
793 /**
794 * Create an entry and perform after create actions
795 *
796 * @since 2.0.16
797 *
798 * @param array $values
799 * @param array $new_values
800 *
801 * @return bool|int $entry_id
802 */
803 private static function continue_to_create_entry( $values, $new_values ) {
804 $entry_id = self::insert_entry_into_database( $new_values );
805
806 if ( ! $entry_id ) {
807 return false;
808 }
809
810 self::after_insert_entry_in_database( $values, $new_values, $entry_id );
811
812 return $entry_id;
813 }
814
815 /**
816 * Sanitize the POST values before we use them
817 *
818 * @since 2.0
819 *
820 * @param array $values The POST values by reference.
821 *
822 * @return void
823 */
824 public static function sanitize_entry_post( &$values ) {
825 $sanitize_method = array(
826 'form_id' => 'absint',
827 'frm_action' => 'sanitize_title',
828 'form_key' => 'sanitize_title',
829 'item_key' => 'sanitize_title',
830 'item_name' => 'sanitize_text_field',
831 'frm_saving_draft' => 'absint',
832 'is_draft' => 'absint',
833 'post_id' => 'absint',
834 'parent_item_id' => 'absint',
835 'created_at' => 'sanitize_text_field',
836 'updated_at' => 'sanitize_text_field',
837 );
838
839 FrmAppHelper::sanitize_request( $sanitize_method, $values );
840 }
841
842 /**
843 * Prepare the new values for inserting into the database
844 *
845 * @since 2.0.16
846 *
847 * @param array $values
848 *
849 * @return array $new_values
850 */
851 private static function package_entry_data( &$values ) {
852 global $wpdb;
853
854 if ( ! isset( $values['item_key'] ) ) {
855 $values['item_key'] = '';
856 }
857
858 $item_name = self::get_new_entry_name( $values, $values['item_key'] );
859 $new_values = array(
860 'item_key' => FrmAppHelper::get_unique_key( $values['item_key'], $wpdb->prefix . 'frm_items', 'item_key' ),
861 'name' => FrmAppHelper::truncate( $item_name, 255, 1, '' ),
862 'ip' => self::get_ip( $values ),
863 'is_draft' => self::get_is_draft_value( $values ),
864 'form_id' => (int) self::get_entry_value( $values, 'form_id', null ),
865 'post_id' => (int) self::get_entry_value( $values, 'post_id', 0 ),
866 'parent_item_id' => (int) self::get_entry_value( $values, 'parent_item_id', 0 ),
867 'created_at' => self::get_created_at( $values ),
868 'updated_at' => self::get_updated_at( $values ),
869 'description' => self::get_entry_description( $values ),
870 'user_id' => self::get_entry_user_id( $values ),
871 );
872
873 $new_values['updated_by'] = $values['updated_by'] ?? $new_values['user_id'];
874
875 return $new_values;
876 }
877
878 /**
879 * @param array $values
880 * @param string $name
881 * @param mixed $default
882 *
883 * @return mixed
884 */
885 private static function get_entry_value( $values, $name, $default ) {
886 return $values[ $name ] ?? $default;
887 }
888
889 /**
890 * Get the ip for a new entry.
891 * Allow the import to override the value.
892 *
893 * @since 2.03.10
894 *
895 * @param array $values
896 *
897 * @return string
898 */
899 private static function get_ip( $values ) {
900 if ( ! FrmAppHelper::ips_saved() ) {
901 return '';
902 }
903
904 $ip = FrmAppHelper::get_ip_address();
905
906 if ( defined( 'WP_IMPORTING' ) && WP_IMPORTING ) {
907 $ip = self::get_entry_value( $values, 'ip', $ip );
908 }
909
910 return $ip;
911 }
912
913 /**
914 * Get the is_draft value for a new entry
915 *
916 * @since 2.0.16
917 *
918 * @param array $values
919 *
920 * @return int
921 */
922 private static function get_is_draft_value( $values ) {
923 if ( isset( $values['frm_saving_draft'] ) && FrmEntriesHelper::DRAFT_ENTRY_STATUS === (int) $values['frm_saving_draft'] ) {
924 return FrmEntriesHelper::DRAFT_ENTRY_STATUS;
925 }
926
927 return isset( $values['is_draft'] ) ? absint( $values['is_draft'] ) : FrmEntriesHelper::SUBMITTED_ENTRY_STATUS;
928 }
929
930 /**
931 * Get the created_at value for a new entry
932 *
933 * @since 2.0.16
934 *
935 * @param array $values
936 *
937 * @return string
938 */
939 private static function get_created_at( $values ) {
940 return self::get_entry_value( $values, 'created_at', current_time( 'mysql', 1 ) );
941 }
942
943 /**
944 * Get the updated_at value for a new entry
945 *
946 * @since 2.0.16
947 *
948 * @param array $values
949 *
950 * @return string
951 */
952 private static function get_updated_at( $values ) {
953 return $values['updated_at'] ?? self::get_created_at( $values );
954 }
955
956 /**
957 * Get the description value for a new entry
958 *
959 * @since 2.0.16
960 *
961 * @param array $values
962 *
963 * @return string
964 */
965 private static function get_entry_description( $values ) {
966 if ( ! empty( $values['description'] ) ) {
967 return FrmAppHelper::maybe_json_encode( $values['description'] );
968 }
969
970 return json_encode(
971 array(
972 'browser' => FrmAppHelper::get_server_value( 'HTTP_USER_AGENT' ),
973 'referrer' => FrmAppHelper::get_server_value( 'HTTP_REFERER' ),
974 )
975 );
976 }
977
978 /**
979 * Get the user_id value for a new entry
980 *
981 * @since 2.0.16
982 *
983 * @param array $values
984 *
985 * @return int
986 */
987 private static function get_entry_user_id( $values ) {
988 if ( isset( $values['frm_user_id'] ) && ( is_numeric( $values['frm_user_id'] ) || FrmAppHelper::is_admin() ) ) {
989 return $values['frm_user_id'];
990 }
991
992 $current_user_id = get_current_user_id();
993 return $current_user_id ? $current_user_id : 0;
994 }
995
996 /**
997 * Insert new entry into the database
998 *
999 * @since 2.0.16
1000 *
1001 * @param array $new_values
1002 *
1003 * @return bool|int $entry_id
1004 */
1005 private static function insert_entry_into_database( $new_values ) {
1006 global $wpdb;
1007
1008 $query_results = $wpdb->insert( $wpdb->prefix . 'frm_items', $new_values );
1009
1010 return ! $query_results ? false : $wpdb->insert_id;
1011 }
1012
1013 /**
1014 * Add the new entry to global $frm_vars
1015 *
1016 * @since 2.0.16
1017 *
1018 * @param int|string $entry_id
1019 *
1020 * @return void
1021 */
1022 private static function add_new_entry_to_frm_vars( $entry_id ) {
1023 global $frm_vars;
1024
1025 if ( ! isset( $frm_vars['saved_entries'] ) ) {
1026 $frm_vars['saved_entries'] = array();
1027 }
1028
1029 $frm_vars['saved_entries'][] = (int) $entry_id;
1030 }
1031
1032 /**
1033 * Add entry metas, if there are any
1034 *
1035 * @since 2.0.16
1036 *
1037 * @param array $values
1038 * @param int|string $entry_id
1039 *
1040 * @return void
1041 */
1042 private static function maybe_add_entry_metas( $values, $entry_id ) {
1043 if ( isset( $values['item_meta'] ) ) {
1044 FrmEntryMeta::update_entry_metas( $entry_id, $values['item_meta'] );
1045 self::maybe_add_unique_id_meta( $values, $entry_id );
1046 }
1047 self::maybe_add_captcha_meta( (int) $values['form_id'], (int) $entry_id );
1048 }
1049
1050 /**
1051 * @since 6.16.3
1052 *
1053 * @param array $values
1054 * @param int $entry_id
1055 *
1056 * @return void
1057 */
1058 private static function maybe_add_unique_id_meta( $values, $entry_id ) {
1059 if ( ! empty( $values['parent_form_id'] ) || empty( $values['unique_id'] ) || ! self::should_check_for_unique_id_match() ) {
1060 return;
1061 }
1062
1063 // This unique ID is inserted with JS on form submit.
1064 // It is used to check for duplicate entries.
1065 $unique_id = sanitize_key( $values['unique_id'] );
1066
1067 if ( $unique_id ) {
1068 FrmEntryMeta::add_entry_meta( $entry_id, 0, '', compact( 'unique_id' ) );
1069 self::flag_new_unique_key( $unique_id );
1070 }
1071 }
1072
1073 /**
1074 * @since 5.0.15
1075 *
1076 * @param int $form_id
1077 * @param int $entry_id
1078 *
1079 * @return void
1080 */
1081 private static function maybe_add_captcha_meta( $form_id, $entry_id ) {
1082 global $frm_vars;
1083
1084 if ( array_key_exists( 'captcha_scores', $frm_vars ) && array_key_exists( $form_id, $frm_vars['captcha_scores'] ) ) {
1085 $captcha_score_meta = array( 'captcha_score' => $frm_vars['captcha_scores'][ $form_id ] );
1086 FrmEntryMeta::add_entry_meta( $entry_id, 0, '', maybe_serialize( $captcha_score_meta ) );
1087 }
1088 }
1089
1090 /**
1091 * Trigger frm_after_create_entry hooks
1092 *
1093 * @since 2.0.16
1094 *
1095 * @param int $entry_id
1096 * @param array $values
1097 * @param array $new_values
1098 *
1099 * @return void
1100 */
1101 private static function after_entry_created_actions( $entry_id, $values, $new_values ) {
1102 // This is a child entry.
1103 $is_child = isset( $values['parent_nonce'] ) && ! empty( $values['parent_form_id'] ) && wp_verify_nonce( $values['parent_nonce'], 'parent' );
1104
1105 do_action( 'frm_after_create_entry', $entry_id, $new_values['form_id'], compact( 'is_child' ) );
1106 do_action( 'frm_after_create_entry_' . $new_values['form_id'], $entry_id, compact( 'is_child' ) );
1107 }
1108
1109 /**
1110 * Actions to perform immediately after an entry is inserted in the frm_items database
1111 *
1112 * @since 2.0.16
1113 *
1114 * @param array $values
1115 * @param array $new_values
1116 * @param int $entry_id
1117 *
1118 * @return void
1119 */
1120 private static function after_insert_entry_in_database( $values, $new_values, $entry_id ) {
1121 self::add_new_entry_to_frm_vars( $entry_id );
1122
1123 self::maybe_add_entry_metas( $values, $entry_id );
1124
1125 self::clear_cache();
1126
1127 self::after_entry_created_actions( $entry_id, $values, $new_values );
1128 }
1129
1130 /**
1131 * Perform some actions right before updating an entry
1132 *
1133 * @since 2.0.16
1134 *
1135 * @param int|string $id
1136 * @param array $values
1137 * @param string $update_type
1138 *
1139 * @return bool $update
1140 */
1141 private static function before_update_entry( $id, &$values, $update_type ) {
1142 $update = true;
1143
1144 global $frm_vars;
1145
1146 // phpcs:ignore WordPress.PHP.StrictInArray.MissingTrueStrict
1147 if ( isset( $frm_vars['saved_entries'] ) && is_array( $frm_vars['saved_entries'] ) && in_array( (int) $id, $frm_vars['saved_entries'] ) ) {
1148 $update = false;
1149 }
1150
1151 if ( $update && $update_type !== 'xml' ) {
1152 $values = apply_filters( 'frm_pre_update_entry', $values, $id );
1153 }
1154
1155 return $update;
1156 }
1157
1158 /**
1159 * Package the entry data for updating
1160 *
1161 * @since 2.0.16
1162 *
1163 * @param int $id
1164 * @param array $values
1165 *
1166 * @return array $new_values
1167 */
1168 private static function package_entry_to_update( $id, $values ) {
1169 global $wpdb;
1170
1171 $new_values = array(
1172 'name' => self::get_new_entry_name( $values ),
1173 'form_id' => (int) self::get_entry_value( $values, 'form_id', null ),
1174 'is_draft' => self::get_is_draft_value( $values ),
1175 'updated_at' => current_time( 'mysql', 1 ),
1176 'updated_by' => $values['updated_by'] ?? get_current_user_id(),
1177 );
1178
1179 if ( isset( $values['post_id'] ) ) {
1180 $new_values['post_id'] = (int) $values['post_id'];
1181 }
1182
1183 if ( isset( $values['item_key'] ) ) {
1184 $new_values['item_key'] = FrmAppHelper::get_unique_key( $values['item_key'], $wpdb->prefix . 'frm_items', 'item_key', $id );
1185 }
1186
1187 if ( isset( $values['parent_item_id'] ) ) {
1188 $new_values['parent_item_id'] = (int) $values['parent_item_id'];
1189 }
1190
1191 if ( isset( $values['frm_user_id'] ) && is_numeric( $values['frm_user_id'] ) ) {
1192 $new_values['user_id'] = $values['frm_user_id'];
1193 }
1194
1195 return apply_filters( 'frm_update_entry', $new_values, $id );
1196 }
1197
1198 /**
1199 * Perform some actions right after updating an entry
1200 *
1201 * @since 2.0.16
1202 *
1203 * @param bool|int $query_results
1204 * @param int|string $id
1205 * @param array $values
1206 * @param array $new_values
1207 *
1208 * @return void
1209 */
1210 private static function after_update_entry( $query_results, $id, $values, $new_values ) {
1211 if ( $query_results ) {
1212 self::clear_cache();
1213 }
1214
1215 global $frm_vars;
1216
1217 if ( ! isset( $frm_vars['saved_entries'] ) ) {
1218 $frm_vars['saved_entries'] = array();
1219 }
1220
1221 $frm_vars['saved_entries'][] = (int) $id;
1222
1223 if ( isset( $values['item_meta'] ) ) {
1224 FrmEntryMeta::update_entry_metas( $id, $values['item_meta'] );
1225 }
1226
1227 do_action( 'frm_after_update_entry', $id, $new_values['form_id'] );
1228 do_action( 'frm_after_update_entry_' . $new_values['form_id'], $id );
1229 }
1230
1231 /**
1232 * Create entry from an XML import
1233 * Certain actions aren't necessary when importing (like saving sub entries, checking for duplicates, etc.)
1234 *
1235 * @since 2.0.16
1236 *
1237 * @param array $values
1238 *
1239 * @return bool|int $entry_id
1240 */
1241 public static function create_entry_from_xml( $values ) {
1242 return self::create_entry( $values, 'xml' );
1243 }
1244
1245 /**
1246 * Update entry from an XML import
1247 * Certain actions aren't necessary when importing (like saving sub entries and modifying other vals)
1248 *
1249 * @since 2.0.16
1250 *
1251 * @param int $id
1252 * @param array $values
1253 *
1254 * @return bool|int $updated
1255 */
1256 public static function update_entry_from_xml( $id, $values ) {
1257 return self::update_entry( $id, $values, 'xml' );
1258 }
1259
1260 /**
1261 * @param string $key
1262 *
1263 * @return int entry_id
1264 */
1265 public static function get_id_by_key( $key ) {
1266 $entry_id = FrmDb::get_var( 'frm_items', array( 'item_key' => sanitize_title( $key ) ) );
1267 return (int) $entry_id;
1268 }
1269
1270 /**
1271 * Get entries count.
1272 *
1273 * @since 6.8
1274 *
1275 * @return int|string
1276 */
1277 public static function get_entries_count() {
1278 $args = array(
1279 'or' => 1,
1280 'parent_form_id' => null,
1281 'parent_form_id <' => 1,
1282 );
1283 return self::getRecordCount( $args );
1284 }
1285 }
1286