PluginProbe
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More / 6.34
Formidable Forms – WordPress Form Builder for Contact Forms, Calculators, Quizzes & More v6.34
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 6.5.4 All 140 releases
formidable / classes / models / FrmEntry.php

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

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