Submit Button Jamming

Hi there,

I have an online game where users can create virtual creatures. Virtual creatures can’t have the same name; they must be entirely unique. I have checks in all places on the web site to enforce name uniqueness (query the database to see if the name already exists somewhere else, etc). Right now, uniqueness on the creature_name database column is not enforced in MySQL. Rather, it’s been enforced by programmatic system restraints via PHP (I’m using CodeIgniter, btw).

I’ve ran into a couple of instances where users have either accidentally or intentionally pressed the Submit button on forms multiple times, sending multiple requests (to create a new creature). This has caused the same request to go through more than once. Through this “submit button jamming,” multiple creatures with the same exact name have been created. Uniqueness destroyed.

I’m stumped because I have programmatic restraints everywhere to enforce uniqueness. I would think that the second, third, and forth (etc) requests would dish out an error. But maybe that’s not what’s happening with the web requests… is it possible that, if the server is jammed with multiple requests (let’s say 3 at a time), that the server is actually receiving the 3 requests all at once, with each request passing the programmatic “uniqueness” checks, because the server is handling the requests more as a “batch” than a “transaction?”

That’s my theory so far. Anyway, I digress. Here’s the script.

// --------------------------------------
// Check access
// --------------------------------------

logged_in();

// --------------------------------------
// POST: create creature
// --------------------------------------

if ($this->input->post('createcreature'))
{
	$name = $this->input->post('name');
	
	// Load necessary stuff
	$this->load->library('form_validation');
	
	// Our rules
	$this->form_validation->set_rules('name', 'Creature Name', 'required|trim|min_length[3]|alpha_dash|callback_name_available');
	
	// It's data validation time, boys and girls
	if ($this->form_validation->run() === FALSE)
	{
		$head['notice'] = error(validation_errors());
	}
	else
	{	
		// --------------------------------------
		// Further, non-form specific validation
		// --------------------------------------
		
		if ($this->sess->member('points') < 100000)
		{
			$head['notice'] = error('<p>You do not have enough points to purchase a creature.</p>');
		}
		else if ($this->Creatures->get_count() >= 300)
		{
			$head['notice'] = error('<p>You can have only 300 creatures at any given time.</p>');
		}
		else
		{
			// --------------------------------------
			// Success
			// --------------------------------------
			
			$egginsert = array(
							'user_id' => $this->sess->member('id'),
							'species_id' => 2,
							'name' => $name,
							'gender' => rand(0,1),
							'time' => time()
						);
			$this->db->insert('creatures', $egginsert);
			
			$eggid =& $this->db->insert_id();
			
			// --------------------------------------
			// Points
			// --------------------------------------
			
			$this->points->sub(100000);
			
			[... redirect to page etc ...]
		}
	}
}

// --------------------------------------
// Page information
// --------------------------------------

$head['pt'] = 'Create Creature';

$this->load->view('header', @$head);
$this->load->view('create_creature', @$content);
// ------------------------------------------------------------------------------------------------

// CALLBACK FUNCTION
// CALLED BY POST REQUEST ABOVE
// If Creature Name Used ==> FALSE
// If Creature Name Is Not Used ==> TRUE

function name_available($name)
{
	// --------------------------------------
	// Check access
	// --------------------------------------
	
	logged_in();
	
	// --------------------------------------
	// Check and see if the name is available
	// --------------------------------------
	
	$avail = $this->db->query('SELECT name FROM creatures WHERE name = '.$this->db->escape($name));
	$avail2 = $this->db->query('SELECT name FROM adoption WHERE name = '.$this->db->escape($name));
	
	if ($avail->num_rows() > 0 || $avail2->num_rows() > 0)
	{
		$this->form_validation->set_message('name_available', 'There is another creature with that name already.');
		return FALSE;
	}
	else
	{
		return TRUE;
	}
}

So my question is: how do I truly enforce uniqueness in this situation and also prevent Submit button jamming? Enforce uniqueness in MySQL? Sure, I could go the Javascript route, but that’s a UI fix and not a true application fix.

Enforce uniqueness in MySQL?

I’d do that as a matter of course, you can then react to the mysql error code which is returned 1062 - “Duplicate found”.

[google]mysql unique index[/google]

With PHP, each request is it’s own process, so it is quite possible that these requests are being processed in parallel, rather than the expected series, however unlikely. What is probably happening is that subsequent requests are received microseconds after the first, in such a way that the first has not completed yet.

So, in addition to Cups suggestion, you can use a file lock in your script to prevent further action until the current script completes and releases. The first script to obtain the lock will succeed, while the rest will fail. Simple make the call to release the file your last statement in the script. This ensures that everything that needs to be done over the course of the script has completed before releasing.