Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions classes/models/FrmEntry.php
Original file line number Diff line number Diff line change
Expand Up @@ -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 ) );

Expand Down Expand Up @@ -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;
}

Expand Down Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions classes/models/FrmEntryMeta.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
18 changes: 18 additions & 0 deletions js/formidable.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' );
Expand Down Expand Up @@ -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;
Expand Down