diff --git a/classes/models/FrmEntry.php b/classes/models/FrmEntry.php index 3c62c10456..b3dffb90b1 100644 --- a/classes/models/FrmEntry.php +++ b/classes/models/FrmEntry.php @@ -51,6 +51,10 @@ public static function is_duplicate( $new_values, $values ) { return false; } + if ( self::maybe_check_for_unique_id_match( $new_values['created_at'] ) ) { + return true; + } + $check_val = $new_values; $check_val['created_at >'] = gmdate( 'Y-m-d H:i:s', strtotime( $new_values['created_at'] ) - absint( $duplicate_entry_time ) ); @@ -80,6 +84,9 @@ public static function is_duplicate( $new_values, $values ) { $metas = FrmEntryMeta::get_entry_meta_info( $entry_exist ); $field_metas = array(); foreach ( $metas as $meta ) { + if ( 0 === (int) $meta->field_id ) { + continue; + } $field_metas[ $meta->field_id ] = $meta->meta_value; } @@ -123,6 +130,56 @@ public static function is_duplicate( $new_values, $values ) { return $is_duplicate; } + /** + * @since x.x + * + * @param string $created_at + * @return bool + */ + private static function maybe_check_for_unique_id_match( $created_at ) { + if ( ! self::should_check_for_unique_id_match() ) { + return false; + } + + $unique_id = FrmAppHelper::get_post_param( 'unique_id', '', 'sanitize_key' ); + if ( ! $unique_id ) { + // Only continue if a unique ID was generated on form submit. + return false; + } + + $timestamp = strtotime( $created_at ); + if ( false === $timestamp ) { + $timestamp = time(); + } + + $unique_id_match = FrmDb::get_var( + 'frm_item_metas', + array( + 'field_id' => 0, + 'meta_value' => serialize( compact( 'unique_id' ) ), + 'created_at >' => gmdate( 'Y-m-d H:i:s', $timestamp - MONTH_IN_SECONDS ), + ), + 'id' + ); + + return (bool) $unique_id_match; + } + + /** + * @since x.x + */ + public static function should_check_for_unique_id_match() { + /** + * Allow users to opt out of the DB query, in case it causes performance issues. + * + * @since x.x + * + * @param bool $should_extend + */ + $should_check = apply_filters( 'frm_check_for_unique_id_match', true ); + return (bool) $should_check; + } + /** * Convert form data to the actual value that would be saved into the database. * This is important for the duplicate check as something like 'a:2:{s:5:"typed";s:0:"";s:6:"output";s:0:"";}' (a signature value) is actually an empty string and does not get saved. diff --git a/classes/models/FrmEntryMeta.php b/classes/models/FrmEntryMeta.php index 1668ca152b..acba6c5bf3 100644 --- a/classes/models/FrmEntryMeta.php +++ b/classes/models/FrmEntryMeta.php @@ -125,6 +125,13 @@ public static function update_entry_metas( $entry_id, $values ) { 'field_id' ); + // This unique ID is inserted with JS on form submit. + // It is used to check for duplicate entries. + $unique_id = FrmAppHelper::get_post_param( 'unique_id', '', 'sanitize_key' ); + if ( $unique_id && FrmEntry::should_check_for_unique_id_match() ) { + self::add_entry_meta( $entry_id, 0, '', compact( 'unique_id' ) ); + } + $values_indexed_by_field_id = array(); foreach ( $values as $field_id_or_key => $meta_value ) { $field_id = $field_id_or_key; diff --git a/js/formidable.js b/js/formidable.js index 45e408580d..887fd88e02 100644 --- a/js/formidable.js +++ b/js/formidable.js @@ -1621,6 +1621,17 @@ function frmFrontFormJS() { window.hcaptcha = null; } + /** + * @since x.x + * + * @return {string} Unique key, used for duplicate checks. + */ + function getUniqueKey() { + return Array.from( window.crypto.getRandomValues( new Uint8Array( 8 ) ) ) + .map( b => b.toString( 16 ).padStart( 2, '0' ) ) + .join( '' ); + } + return { init: function() { jQuery( document ).off( 'submit.formidable', '.frm-show-form' ); @@ -1767,6 +1778,13 @@ function frmFrontFormJS() { object.appendChild( antispamInput ); } + // Add a unique ID, used for duplicate checks. + const uniqueIDInput = document.createElement( 'input' ); + uniqueIDInput.type = 'hidden'; + uniqueIDInput.name = 'unique_id'; + uniqueIDInput.value = getUniqueKey(); + object.appendChild( uniqueIDInput ); + if ( classList.indexOf( 'frm_ajax_submit' ) > -1 ) { hasFileFields = jQuery( object ).find( 'input[type="file"]' ).filter( function() { return !! this.value;