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.