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.
Related
I have a php code as shown below in which usernames/passwords are verified through db and only one user are allowed to login (At Line X in the php code below).
In the php code, the table trace_users (user_name, open, write are the columns) keeps the track of logged in users. When any user is logged in, the column open is set to true with value 1 on write column. If the user logs out, the column open is set to false with value 0 on write column.
logged in user => open = true, write = 1
when user logs out => open = false, write = 0
php code:
if ($user_from_db && password_verify($password, $user_from_db->user_pass)) {
$sql = "SELECT * FROM trace_users where open='true'";
if($result1 = mysqli_query($connect, $sql)){
if(mysqli_num_rows($result1) > 0){ // Line X
while($row = mysqli_fetch_array($result1)){
if($row['open'] == "true") {
if(!isset($_SESSION['admin'])) {
$message = "user " . $row['user_name'] . " is currently editing the form. Want to take over ?";
echo "<script type='text/javascript'>if(confirm('$message')) { } else { };</script>"; // Line A
}
break;
}
}
} else{
$_SESSION['admin'] = true;
$_SESSION['user_name'] = $username;
$open="true";
$write="1";
$stmt= $connect->prepare("UPDATE trace_users SET open=?, write=? WHERE user_name=?");
$stmt->bind_param('sss', $open, $write, $_SESSION['user_name']);
$stmt->execute();
}
}
} else {
echo "Invalid Username or Password";
}
At line A if userA is logged in and another user lets say userB tries to login then it will say userA is currently editing the form. Want to take over ?.
What I want to achieve is if userB takes over the form on clicking ok in the alert box then userA should come in the readonly mode (revoke the write access) and userB should
be given full access (write).
userA => open = true, write = 0
userB => open = true, write = 1
This is what I have tried:
After Line A, I am thinking to add these lines so that the user who is trying to take over should be able to login.
$_SESSION['admin'] = true;
$_SESSION['user_name'] = $username;
$open="true";
$write="1";
$stmt= $connect->prepare("UPDATE trace_users SET open=?, write=? WHERE user_name=?");
$stmt->bind_param('sss', $open, $write, $_SESSION['user_name']);
$stmt->execute();
Problem Statement:
I am wondering what changes I should make in the php code above so that if userA is logged in and userB also want to login and try to take over the form
then there should be no write access for userA, and userB should be given full access i.e.
userA (open=true , write=0)
userB (open=true, write=1)
Note: For no write access, the save button will not appear for the user.
The issue is more in the flow logic, than in the code :
UserA connects (call 1, php grants access : open => 1, write => 1)
UserB connects (call 2, php shows popup to UserB : open => 1)
UserB has to decide to take over (or not) (call 3, php grants write access to UserB and revokes write access of UserA : UserA write => 0, UserB write=>1)
With your code you are managing step 1 and 2.
When UserB decides to take over, the php code of step 2 is already over (your prompted the question). You have to write a dedicated code to get the UserB answer to the question (it is all about the javascript you prepared).
Whatever, you are not checking any write access, so once UserB took over and then logged out, UserA will still be with open connection but without write access. If UserB connects again it will be prompted again to take over something that doesn't exist (UserA write access is gone).
I see two basic problems with your approach:
The trace_users.write column might inadvertently get set to 1 for multiple users unless you're really careful about locking the table before updating it. That's just not a good way to go about tracking who is the "active" editor.
Your line "A" might display some HTML, but until the user confirms yes/no, it won't get executed. That step has to be done somewhere else.
A better way would be to have a single canonical record (probably in another table) track the current admin, and your code can check (or update) that record any time as necessary. Any time a user elects to make a change, PHP would first check whether they're the active editor and if so, commit the update, otherwise deny the update.
Pseudo-code for some server-side functions you'll need:
// When an update is requested:
if ( user is logged in and is the current admin ) {
commit the change;
} else {
Display "sorry" message to user
}
// -----------
// When user "X" logs in:
if ( another user is currently the active admin ) {
Display message: "User Y is currently the admin. Take over?"
} else {
Log in user "X" and set user "X" to current admin
}
// -----------
// When user "X" asks to take over as admin:
log in user "X"
set user "X" to current admin
The only flaw I can see here is that user "A" is logged in and editing and saves changes, user "B" might have the old data loaded in their browser. But that's an issue you didn't ask to solve, so it may not be a problem for you anyway.
How about something like this?
$username = $_SESSION['user_name'];
$open="true";
$write="1";
$stmt=$connect->prepare("UPDATE trace_users SET write=0 WHERE write=1"); // revoke write access of all users
$stmt=$connect->prepare("UPDATE trace_users SET open=:open, write=:write WHERE user_name=:username");
$stmt->bind_param('sss', $open, $write, $username);
$stmt->execute();
That will revoke write-access of all users, then set write-access only for the most recent user to request write-acces.
And you will need to make sure your client is updated about its revocation of write access, so the save button can be hidden when necessary.
Not recommended on a large data set, but a single line option for you is to replace your update with one that includes reference to the username:
$stmt= $connect->prepare("UPDATE trace_users SET open=IF(open=1 OR user_name=?,1,0), write=IF(user_name=?,1,0)");
$stmt->bind_param('sss', $_SESSION['user_name'], $_SESSION['user_name']);
This will, for every record in the table:
Leave open=1 where it is 1
Set open=1 where username is targetUser
Leave open=0 where is is 0 and username is not targetUser
Set write=1 where username is targetUser
Set write=0 where username is not targetUser
All in one hit.
For large data sets, create a separate table to track. There are other smarts you can do using unique or primary keys to ensure only one record gets updated, and then validate "If record got updated (i.e. rowCount() > 0), then I got the flag, if not, someone else got it". Only consider that when you're more used to MySQL.
Warning: "Open" and "Write" are both keywords / reserved words and your SQL may not be portable. Either wrap in back ticks, or preface with something like userOpen, userWrite. I've added back ticks in the example below. Read more here: https://dev.mysql.com/doc/refman/8.0/en/keywords.html
$stmt= $connect->prepare("UPDATE trace_users SET `open`=IF(`open`=1 OR user_name=?,1,0), `write`=IF(user_name=?,1,0)");
$stmt->bind_param('sss', $_SESSION['user_name'], $_SESSION['user_name']);
I have a website where it displays all of my records. I can click on an individual record and it gets the student_id of that record and updates it to the URL eg. view_student.php?id=12.
It then takes me to a new page where I want it to display all the information about that record, in this case, show all information about student number 12, but none else.
I haven't a clue how to carry out the statement to display all of the information for that record, this is what I have so far:
if (isset($_GET['student_id'])) {
echo $row['student_name'] . $row['student_age'] . $row['student_gender'];
}
This is a standalone page with nothing else on it. view_student.php simply uses a require function to this script. This code does not display anything, nor does it display any errors. I'm using PDO and I have made sure I'm connected to the database.
My guess is that I will need to use a WHERE clause but I'm just not too sure
Thank you
You can use below PDO query to fetch your data
$statement = $db_con->prepare("select * from student where student_id = :student_id");
$statement->execute(array(':student_id' => $_GET['student_id']));
$row = $statement->fetchAll(PDO::FETCH_ASSOC);
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.
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;");
Here's a situation, i have a list of support tickets that when you click the title of the ticket takes you to a page that displays the ticket in more detail. If uses URL GET variables to query the database. I've taken SQL injection into account but what if someone modifies the url to an id that doesn't exist? whats the best way to deal with that?
Thanks,
Jonesy
If the ID does not exist, send a 404 - Not Found header along with a nice error page telling the user that it wasn't found.
You probably have to make a page handling unsuccessful searches anyway; just route it in there. Then you can help the user to find what (s)he searches in a consistent way, provide cues and "most-searched-after" and what not.
This may seem too simple, but you should always validate your GET (or POST) variable before doing anything with them. In your case, just verify that the ID exists in the database. If it doesn't, inform the user.
You should always check if your query returned anything. If it returned 0 rows, the ID doesn't exist.
<?php
$result = mysql_db_query("your query", $link);
$num_rows = mysql_num_rows($result);
if($num_rows < 1) {
// row with that id doesnt exist
// do whatever you want
} elseif($num_rows > 1) {
// you have problem with your ids in db
} else {
// everything went fine
// do your thing here
}
?>
Check if the ticket exists; if not, react accordingly. What "react accordingly" means is determined by your business logic: create a new ticket? raise an error? take the user to a list of available tickets?
An example using the old mysql extension for brevity:
$sanitized_numeric_id = (int) $_GET['ticket_id']; // assuming id is numeric
$query_resource = mysql_query('SELECT `somecolumn`, `column2`, `othercolumn`
FROM `tickets`
WHERE `id`= ' . $sanitized_numeric_id);
if (mysql_num_rows($query_resource) > 0) {
// the ticket exists, do something with it
} else {
// the ticket doesn't exist, react accordingly
}