MySQL - INSERT validation in query - php

Take the following scenario:
Item cost = 30
User money = 25
Form won't submit if user doesn't have enough money.
$error = false;
if($user_money < $item_cost){
//don't submit form
$error = true;
}
But is that enough? Can a user get around it and purchase the item even if there isn't enough money?
Would it be better to do something like this:
Keep the above:
$error = false;
if($user_money < $item_cost){
//don t submit form
$error = true;
}else{
$myclass->purchaseItem($item_id, $user_id);
}
public function purchaseItem($item_id, $user_id) {
//do the validation here again something like. I don t know how to do the query exactly.
$q = $this->db->mysqli->prepare("INSERT INTO buys (bl bla blah) VALUES (?,?,?) IF ... user has enough points in user_points table");
}
Hope that makes sense and I don't get down voted.

In your database you can use a trigger to check the constraint. Depending on you model you might need a transaction to prevent a record from being inserted incorrectly.
Assuming the following:
Two tables:
buys
wallet
If a user buys something (definite BUY, so not a shopping cart placement action), the wallet is updated in the same action.
To do this you can either write a transaction: See How to start and end transaction in mysqli? on how to.
and use 2 statements:
UPDATE wallet SET amount=amount-{buyAmount} WHERE user=?;
INSERT INTO buys (amount,user,orderId) VALUES (?,?,?);
(Of course buyAmount is also a ? in the prepared statement)
Or you can use a trigger. The trigger has to lock the user record when inserting in the buys table:
CREATE TRIGGER updateWallet() BEFORE INSERT ON buys
BEGIN
SET #updatedWalletAmount=0;
SELECT amount-NEW.buyAmount FROM wallet WHERE user=NEW.user FOR UPDATE;
IF(#updatedWalletAmount>0) THEN
UPDATE wallet SET amount=#updatedWalletAmount;
ELSE
SIGNAL SQLSTATE 'ERR0R'
SET
MESSAGE_TEXT = 'Not enough money',
MYSQL_ERRNO = 'USER-1';
END;
END;
The error will have to be caught in php.

Validating data on server shouldn't be made twice. Validating the data on the php side would be easier and as reliable as on your database server.
For more information on validating input data you can check this.

Related

I want to achieve row level locking using php in mysql

I am designing this page where users will see the change orders for the day. I want only one person to edit one change order at a time. While other user can still edit other change order. The code i am pasting is for final page where the user is redirected after clicking on particular change order. Please help me by giving an example as i am very new to php and mysql. Thanks in advance.
`
<?php
$change = $_GET['value'];
mysqli_query('BEGIN');
$sql="SELECT * FROM cat_firewall USE INDEX(id) WHERE chg_id='$change' FOR UPDATE";
$result=mysqli_query($con,$sql);
// Count table rows
$count=mysqli_num_rows($result);
?>
---MORE CODE----
while ($row = mysqli_fetch_array($result)){ ?>
<tr>
---MORE CODE----
<?php
$source_FW = $_POST['source_FW'];
$source_ACL = $_POST['source_ACL'];
$destination_FW = $_POST['destination_FW'];
$destination_ACL = $_POST['destination_ACL'];
if(isset($_POST['Submit'])){
foreach($source_FW as $key=>$value){
$sql1="UPDATE cat_firewall SET source_FW='$value' WHERE entry_id='$key' AND chg_id='$change' ";
$result=mysqli_query($con,$sql1);
}
foreach($source_ACL as $key1=>$value1){
$sql2="UPDATE cat_firewall SET source_ACL='$value1' WHERE entry_id='$key1' AND chg_id='$change' ";
mysqli_query($con,$sql2);
}
foreach($destination_FW as $key2=>$value2){
$sql3="UPDATE cat_firewall SET destination_FW='$value2' WHERE entry_id='$key2' AND chg_id='$change' ";
mysqli_query($con,$sql3);
}
foreach($destination_ACL as $key3=>$value3){
$sql4="UPDATE cat_firewall SET destination_ACL='$value3' WHERE entry_id='$key3' AND chg_id='$change' ";
mysqli_query($con,$sql4);
}
echo "update complete";
}
sleep(120);
mysqli_query($con,'COMMIT');
?>'
`
I want to lock it as soon as user clicks on the change order and gets to this page to edit rows and release the lock on the row once the submit button is clicked.
That's a big red flag. You should not expect InnoDB locking to run at the glacial speed of a human.
Optmistic version:
Have a sequence number for the updates that happen.
Grab that number at the start of the user looking at the values and scratching his head.
Let the user (at glacial speed) make changes.
Now send the updates and the sequence number back to the client API which does
The following:
BEGIN;
SELECT seq_num ... FOR UPDATE;
if the seq_num has changed, ROLLBACK this transaction and tell the user to start over.
else...
do the UPDATEs
update seq_num to a new number
COMMIT;
Pessimistic approach...
BEGIN;
SELECT who_is_editing FROM ... FOR UPDATE;
if someone else is editing, ROLLBACK and tell user to "try again later".
else...
UPDATE ... who_is_editing = $this_user;
COMMIT;
Now proceed to let the user do whatever he wants. But, at the end, clear who_is_editing.
Caveat: If a user simply vanishes, this 'lock' may never be released. So...
Have some job check for that.
Warn the user that he has a limited amount of time to enter the changes.
Verify inside the UPDATE transaction that the user still matches who_is_editing. If not slap him around, telling him "you took too long!".
(Caveat: both of these approaches were hastily typed; please study carefully, looking for holes in the logic.)

How to remove previous sql insert if one after fails?

When I insert a new record into one table (work_log), I update a record in another table (employers) with the last inserted record from work_log.
But if updating employers-table doesn't succeed, after successfully inserted the new record into work_log, I need to remove the newly added record to work_log since that entry would not be valid anymore.
Here's my script so far:
/**
* This first part has no direct affect on the question, but serves as additional information to understand the script better..
* - - -
* First, a new work session is inserted (this session has nothing to do with browser session)
* If this fails, the script does not continue, and the user is redirected back to the form with an error-message.
* otherwise, the script continues, and try to activate the session by adding a new work_log entry.
*/
$ins_session = $con['site']->prepare('INSERT INTO work_sessions (fields) VALUES (?)');
$ins_session->execute(array(values));
if($ins_session){
KD::notice('success','New work session is created.');
$session_id = $con['site']->lastInsertId();
} else {
KD::notice('error','Work session was not created.');
KD::redirect(); // stops the script, and redirects
}
/**
* This part affects my question
* - - -
* Add a new entry to the work log in order to automatically start the work session.
* If this entry is successfully inserted, then add an indicator to the corresponding employer, in the employers table, to indicate that this employer has an active session (and which one it is).
*/
$ins_work_log = $con['site']->prepare('INSERT INTO work_log (fields) VALUES (?)');
$ins_work_log->execute(array(values));
if($ins_work_log){
$upd_employer = $con['site']->prepare('UPDATE employers SET fk_work_sessions_id = ? WHERE id = ?');
$upd_employer->execute(array($session_id,$_POST['employer_id']));
if($upd_employer){
KD::notice('success','New session was created and started.');
KD::redirect();
} else {
// need to remove the entry from work_log.
KD::notice('Work session was created, but not started. Please start the session manually.');
}
}
To my understanding, I have to delete the last inserted record in the work_log-table?
Is there any other way to do this? like, in another order, or to automatically remove the entry from work_log if this (the update query) fails?
The work_log-table is innoDB, and row format is compact if that is important to know...
UPDATE
I've set it up like this:
It seems to work, but I'm a bit unsure if I'm using it correctly regarding the if/else statements.
$con['site']->beginTransaction();
$ins_work_log = $con['site']->prepare('INSERT INTO work_log (fields) VALUES (?)');
$ins_work_log->execute(array(values));
if($ins_work_log){
# update employer
$upd_employer = $con['site']->prepare('UPDATE employers SET fk_work_sessions_id = ? WHERE id = ?');
$upd_employer->execute(array($session_id,$_POST['employer_id']));
if($upd_employer){
$con['site']->commit();
KD::notice('success','New session was created and started.');
} else {
$con['site']->rollBack();
KD::notice('error','Work session was created, but not started. Please start the session manually.');
}
//
} else {
$con['site']->rollBack();
KD::notice('error','');
}
KD::redirect();
Will if($ins_work_log), and if($upd_employer), have any affect when the query hasn't been committed yet?
This is a classic case for using START TRANSACTION, COMMIT, and ROLLBACK.
http://dev.mysql.com/doc/refman/5.0/en/commit.html
Just make sure you are using a database engine that supports it.
Pseudocode:
query("START TRANSACTION;");
query("INSERT INTO table1 ...");
if (query("INSERT INTO table2 ..."))
query("COMMIT;");
else
query("ROLLBACK;");

Php's Mysqli executes statements in random order (very strange)

EDIT: I am very sorry, I found the mistake and it's very stupid. (See answer)
I seem to have a very strange problem. I have a website that uses 3rd party authorization for login. My users use two social networks for this: Facebook and Vkontakte (Russian analogue).
On log in, I search the database for the user with the passed social id (which is, depending on the chosen social network, user's id in FB or VK, for which I have two different columns) and fetch it.
In case a user has accounts in both FB and VK and has logged in through both of them, he or she now has two separate accounts on my site. However, he can join them into one, by logging in through one social network (this will be his master account) and using the 'user_bind' function with another social network.
This function finds the user's another account and relinks all data in the database to the master account. It then deletes the other account and adds its social id to the master account, so that now the user can log in through both social networks. Social id column has a UNIQUE index, naturally.
However, when the script executes, it seems to execute the UPDATE, which adds the social id, before the DELETE statement, which removes the old user. This produces an error, because it attempts to add an existing social id (because the old user is still there).
When I check the database after the script execution, the old user is gone, so I guess that means that the DELETE statement is indeed executed, but with a delay, in which other statements are executed. The MySQL Workbench's log confirms this, though I'm not sure whether it's reliable.
My question is how do I ensure that the DELETE (or any other MySQL statement for that matter) has actually been executed before executing the rest of the script? And why does this happen anyway?
Here's the adequately-commented code (though I will gladly accept an answer which has no code in it and just explains the principle).
The user_bind function:
function user_bind($eSourceType)
{
//$eSourceType can be 'fb' or 'vk', depending on the social network of the secondary account
$usrMe=get_gl_me(); //gets the user's account, through which he is logged in - the master account
if ($eSourceType=='fb') //if the social network that we are binding this account to is Facebook
{
$vSidName='facebook_id'; //name of the column which contains the social id
if (!$usrMe->get_private_property("facebook_id") & $usrMe->get_private_property("vkontakte_id") ) //check if the master account really doesn't have facebook_id set
{
$fb=get_facebook();//gets facebook object (from FB PHP SDK)
$sid=$fb->getUser();//gets user's id in facebook (social id)
}
else
{
error("The account has facebook_id set");
}
}
elseif($eSourceType=='vk')//same as before, but the id is fetched through $_GET, not object
{
$vSidName='vkontakte_id';
if ($usrMe->get_private_property("facebook_id") & !$usrMe->get_private_property("vkontakte_id") ) //check if it's the right account
{
$sid=$_GET['uid'];
}
else
{
error("The account has vkontakte_id set");
}
}
if(!$sid) //if we couldn't retrieve the social id
{
error("Can't bind: \$sid not set.");
}
$idNew=$usrMe->get_id();//get id (database id) of the master account
$usrOld=fetch_user_by_sid($sid, $eSourceType, true); //fetches the 'user' object by the social id we retrieved before
if ($usrOld)//if there is a user with this social id (if there is a secondary account)
{
$idOld=$usrOld->get_id();//get id of the secondary account
$tblsRelink=array("comments", "posts", "users_private", "vote_posts", "vote_comments"); //get tables in which we have to relink users
foreach($tblsRelink as $tbl)
{
//update set users_idusers to userid
$sp=new Statement_Parameter; //this is a class from PHP.com: http://php.net/manual/en/mysqli-stmt.bind-param.php. It allows to bind variables to the prepared statement in MySQLi without much pain
$query="UPDATE $tbl SET users_idusers=" . db_stmt_operands($idNew, $sp, 'idNew') . " WHERE users_idusers=". db_stmt_operands($idOld, $sp, 'idOld'); //db_stmt_operands inserts question marks in the query, while binding the variables through Statement_Parameter
$affected_rows=db_submit($query, $sp);//see below for the db_submit() function explanation
}
//delete old user
$sp=new Statement_Parameter; //clear Statement_Parameter
$query="DELETE FROM users WHERE idusers=" . db_stmt_operands($idOld, $sp, 'idOld');
$affected_rows=db_submit($query, $sp);
echo "<br>affected: $affected_rows<BR>"; //this actually returns 1
//lets see if the user was actually deleted
$usrTest=fetch_user_by_sid($sid, $eSourceType, true); //fetch the user by the social id
if($usrTest) //if a user is fetched
{
debug_array($usrTest); //custom implementation of print_r
error("User still exsists. Oh no.");//it always does
}
}
$usrMe->set_private_property($vSidName, $sid);//sets the property 'facebook_id' or 'vkontakte_id' to the social id that we got in the beginning
$usrMe->update();//UPDATE statement, which brings the object's properties in the database up to date (in our case: adds the social id)
//the UPDATE statement doesn't execute because the old user is still there
}
The db_submit function:
function db_submit($query, $sp=NULL)
{
$mysqli = db_connect(); //fetches PHP MySQLi object
if ($stmt = $mysqli->prepare($query)) //if the statement is successfully prepared
{
if($sp)//if there is a Statement_Parameter passed
{
$sp->Bind_Params($stmt); //bind parameters from SP
}
if($stmt->execute())//try to execute the statement
{
//on success
if ($mysqli->insert_id) //if this was an INSERT
{
return $mysqli->insert_id;
}
else //if this was DELETE or UPDATE
{
return $mysqli->affected_rows;
}
}
else
{
//on failure
error("Could not submit: could not execute statement. Query: $query." . $stmt->error); //this kills the script
}
}
else
{
error("Could not submit. Query: $query." . $mysqli->error);
}
}
The thing is: private_properties (including the social ids) or the object 'user' are stored in a separate table ('users_private'), which was linked to the main table ('users') through a foreign key.
I included the 'users_private' table in the array of tables that require relinking:
$tblsRelink=array("comments", "posts", "users_private", "vote_posts", "vote_comments");
This resulted in the record in 'users_private' for the old user being relinked to the new user (which now had 2 records - how reckless of me not to make this field UNIQUE). So when the old user got deleted, its associated 'users_private' record was not, because it was now linked to the new user. Naturally, attempt to add the social id was producing an error, because that id already was there, relinked from the old user.
This could be prevented by either
Thinking a bit more about what I was doing (why did I consider 'users_private' a table eligible for relinking?)
More careful database structuring (if a field is supposed to be unique - create a UNIQUE key!)
or, even better, both.

How can I make sure no duplicates occur when making a site that preorders tickets?

I am making a website in which people can select which tickets they want and they just get an ID to go pay and pick them up to the desired location.
I have a PHP function that actually checks if the tickets are already booked before allowing people to reserve them, but I just found out that if 2 people happen to click the "booking" button within the same second, my system reserves the places twice.
Is there a way to prevent this? My current code works well except for what I've said, it's here:
$f1="";
$f2="";
$sqlAP="SELECT * FROM apartados";
if ($resultAP = mysql_query($sqlAP)) {
while ($rowAP = mysql_fetch_array($resultApP)) {
$f = $rowAP['funcion'];
$lugar=$rowAP['lugar'];
$count++;
if($f=="F1"){
$f1.=($lugar. " ");
}else if($f=="F2"){
$f2.=($lugar. " ");
}
}
}
$sqlPag2="SELECT * FROM pagados";
if ($resultPag2 = mysql_query($sqlPag2)) {
while ($rowPag2 = mysql_fetch_array($resultPag2)) {
$f = $rowPag2['funcion'];
$lugar=$rowPag2['lugar'];
$count++;
if($f=="F1"){
$f1.=($lugar. " ");
}else if($f=="F2"){
$f2.=($lugar. " ");
}
}
}
$func1= explode(" ",$f1);
$func2= explode(" ",$f2);
$repetidos=0;
for($int=0;$int<$cant;$int++){
$helper=0;
while($func1[$helper]){
if($func1[$helper]==$lugar[$cant]){
$repetidos++;
}
$helper++;
}
}
for($int=0;$int<$cant2;$int++){
$helper=0;
while($func2[$helper]){
if($func2[$helper]==$lugar2[$cant2]){
$repetidos++;
}
$helper++;
}
}
This takes from the database what has been booked (apartados) and paid (pagados) and then checks for repeated seats trying to get booked (repetidos) after this comes an if just looking for repetidos > 0, it works, except on the instance I described earlier. Could anyone let me know how I can avoid this?
Give each ticket a unique id.
Insert
the tickets in to a "reserved" table.
Do this in a transaction, so all
tickets commit at once.
Put a unique
constraint on the ticket id in the
reserved table.
If you get an error during your insert, then someone else has already reserved one of the tickets.
PHP + MySQL transactions examples
Use transactions. They'll make your life a lot easier when you need to do multiple queries that can't be done in parallel.
The proper way to do this would be to design the database with a unique constraint on the seat/ticket column, then insert a new reservation in one atomic query. If a reservation already exists for this seat, the database will complain and you can handle that complaint in your code. It won't be possible to insert two identical seat reservations into the database. Selecting all records and looping through them in your application is, sorry, ludicrous.
When worried if the same seats are going to be reserved at the same time you can use MySQL table locks. When locked, no one else can write to that table. The process would look something like this:
lock table
check if seats are available
reserve seats for this user if available or return error if not
unlock table
This will prevent tickets being reserved twice as no other scripts can access the table until it is unlocked (other requests get 'queued' if they happen at the same time)

Prevent Users from Performing an Action Twice

We have some problems with users performing a specific action twice, we have a mechanism to ensure that users can't do it but somehow it still happens. Here is how our current mechanism works:
Client side: The button will be disabled after 1 click.
Server side: We have a key hash in the URL which will be checked against the key stored in SESSIONS, once it matches, the key is deleted.
Database side: Once the action is performed, there is a field to be flagged indicating the user has completed the action.
However, with all these measures, still there are users able to perform the action twice, are there any more safer methods?
Here is the partial code for the database side:
$db->beginTransaction();
// Get the user's datas
$user = $db->queryRow("SELECT flag FROM users WHERE userid = {$auth->getProperty('auth_user_id)}");
if ($user['flag'] != 0) {
$db->rollback();
// Return with error
return false;
}
// Proceed with performing the action
// --- Action Here ---
// Double checking process, the user data is retrieved again
$user = $db->queryRow("SELECT flag FROM users WHERE userid = {$auth->getProperty('auth_user_id)}");
if ($user['flag'] != 0) {
$db->rollback();
// Return with error
return false;
}
// --- The final inserting query ---
// Update the flag
$db->query("UPDATE users SET flag = 1 WHERE userid = {$auth->getProperty('auth_user_id)}");
$db->commit();
return true;
It is good to see that you have taken all measures to defeat the bad guys. Speaking in terms of bad guys:
Client side: This can easily be bypassed by simply disabling javascript. Good to have anyways but again not against bad guys.
Server side: This is important, however make sure that you generate a different hash/key with each submission. Here is a good tutorial at nettutes on how to submit forms in a secure fashion.
Database side: Not sure but I suspect, there might be SQL injection problem. See more info about the SQL Injection and how to possibly fix that.
Finally:
I would recommend to you to check out the:
OWASP PHP Project
The OWASP PHP Project's goal (OWASP PHP Project Roadmap) is to enable developers, systems administrators and application architects to build and deploy secure applications built using the PHP programming language.
Well the JS method and Hash method may be cheated by some notorious guy, but 3rd method seems to be very good in order to protect the redundancy. There must be some programming flaw to get passed this.
Why don't u just check the flag field on the page where you are inserting the values rather than where user performing the action (if you are doing it now)
Pseudocode follows:
<?
$act_id; // contains id of action to be executed
$h = uniqid('');
// this locks action (if it is unlocked) and marks it as being performed by me.
UPDATE actions SET executor = $h WHERE act_id = $act_id AND executor = '';
SELECT * FROM actions WHERE executor = $h;
//
// If above query resulted in some action execute it here
//
// if you want to allow for executing this exact action in the future mark it as not executed
UPDATE actions SET executor = '' WHERE act_id = $act_id;
Important things:
First query should be update claiming
the action for me if it is yet
unclaimed.
Second should be query
grabbing action to execute but only
if it was claimed by me.

Categories