get_charset_collate(); dbDelta( "CREATE TABLE $table ( request_hash char(64) NOT NULL, payload_hash char(64) NOT NULL, form_id bigint(20) unsigned NOT NULL, entry_id bigint(20) unsigned NOT NULL DEFAULT 0, message text NOT NULL, PRIMARY KEY (request_hash), KEY entry_id (entry_id) ) ENGINE=InnoDB $charset;" ); } private static function canonical( $value ) { if ( ! is_array( $value ) ) { return $value; } if ( array_keys( $value ) !== range( 0, count( $value ) - 1 ) ) { ksort( $value ); } foreach ( $value as &$item ) { $item = self::canonical( $item ); } unset( $item ); return $value; } public static function identity( $data, $form ) { // Legacy clients and the no-JavaScript fallback remain supported without a retry receipt. if ( ! isset( $data['request_id'] ) ) { return null; } if ( ! is_string( $data['request_id'] ) || ! preg_match( '/^[a-f0-9]{64}$/D', $data['request_id'] ) ) { return new WP_Error( 'request_id', __( 'Reload this form before submitting.', 'killerconversions' ), array( 'status' => 400 ) ); } $meaning = array_intersect_key( $data, array_flip( array( 'values', 'resume_token', 'resume_version', 'resume_revision', 'form_revision' ) ) ); return array( 'request_hash' => hash( 'sha256', $form['id'] . ':' . $data['request_id'] ), 'payload_hash' => hash_hmac( 'sha256', wp_json_encode( self::canonical( $meaning ) ), wp_salt( 'auth' ) ), 'form_id' => $form['id'] ); } public static function lookup( $identity, $locked = false ) { if ( null === $identity ) { return null; } global $wpdb; if ( $locked ) { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Idempotency claims and locked receipt/entry reads require fresh InnoDB state. IDs, hashes and expiry are prepared; the lookup has explicit prepared locked/unlocked branches. $row = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE request_hash=%s FOR UPDATE', KCONV_Core::table( 'receipts' ), $identity['request_hash'] ), ARRAY_A ); } else { // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Idempotency claims and locked receipt/entry reads require fresh InnoDB state. IDs, hashes and expiry are prepared; the lookup has explicit prepared locked/unlocked branches. $row = $wpdb->get_row( $wpdb->prepare( 'SELECT * FROM %i WHERE request_hash=%s', KCONV_Core::table( 'receipts' ), $identity['request_hash'] ), ARRAY_A ); } if ( $wpdb->last_error ) { return self::storage_error(); } if ( ! $row ) { return null; } if ( ! hash_equals( $row['payload_hash'], $identity['payload_hash'] ) ) { return new WP_Error( 'request_conflict', __( 'This attempt already used different answers. Check your previous submission before starting again.', 'killerconversions' ), array( 'status' => 409 ) ); } if ( (int) $row['entry_id'] <= 0 ) { return null; } // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Idempotency claims and locked receipt/entry reads require fresh InnoDB state. IDs, hashes and expiry are prepared; the lookup has explicit prepared locked/unlocked branches. $payload = $wpdb->get_var( $wpdb->prepare( 'SELECT payload FROM %i WHERE id=%d', KCONV_Core::table( 'entries' ), $row['entry_id'] ) ); if ( $wpdb->last_error || ! is_string( $payload ) ) { return self::storage_error(); } $values = json_decode( $payload, true ); if ( ! is_array( $values ) ) { return self::storage_error(); } return new WP_REST_Response( apply_filters( 'kconv_submission_response', array( 'success' => true, 'message' => $row['message'], 'replayed' => true ), (int) $row['entry_id'], $values ), 200 ); } public static function claim( $identity, $message ) { if ( null === $identity ) { return null; } global $wpdb; // A concurrent claimant waits for the first transaction, then reads its committed receipt. // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Idempotency claims and locked receipt/entry reads require fresh InnoDB state. IDs, hashes and expiry are prepared; the lookup has explicit prepared locked/unlocked branches. $ok = $wpdb->query( $wpdb->prepare( 'INSERT INTO %i (request_hash,payload_hash,form_id,entry_id,message) VALUES (%s,%s,%d,0,%s) ON DUPLICATE KEY UPDATE request_hash=VALUES(request_hash)', KCONV_Core::table( 'receipts' ), $identity['request_hash'], $identity['payload_hash'], $identity['form_id'], $message ) ); return false === $ok ? self::storage_error() : self::lookup( $identity, true ); } public static function complete( $identity, $entry_id ) { if ( null === $identity ) { return true; } global $wpdb; // phpcs:ignore WordPress.DB.DirectDatabaseQuery.DirectQuery,WordPress.DB.DirectDatabaseQuery.NoCaching -- Idempotency claims and locked receipt/entry reads require fresh InnoDB state. IDs, hashes and expiry are prepared; the lookup has explicit prepared locked/unlocked branches. return 1 === $wpdb->update( KCONV_Core::table( 'receipts' ), array( 'entry_id' => $entry_id ), array( 'request_hash' => $identity['request_hash'], 'entry_id' => 0 ), array( '%d' ), array( '%s', '%d' ) ); } private static function storage_error() { return new WP_Error( 'storage', __( 'We could not verify your submission. Keep this page open and try again.', 'killerconversions' ), array( 'status' => 503 ) ); } }