0

On my website, I display 5 questions (MCQs) per page and when the user requests new page, I am calling a script score_update() with the score of this page and then presenting him with the next page.

The scoreUpdate() script is something like

<?php
//connect to database
//update the score
?>

The problem is that the user may refresh the page and the score may be updated twice or the number of times he refreshes the page or he may directly call the script by viewing the source code.

How can I implement this system?I need an idea.

EDIT

Here is my database schema

user

------------------------------------------
user_id  | username  |  password  | points
------------------------------------------

PS :The user may attempt the same question again at some point in future. There is no restriction on it. So no need to keep track of questions attempted by him. He must get marks only if he attempted the question and knocked it right. Hope I am clear.

Surreal Dreams
  • 26,055
  • 3
  • 46
  • 61
Naveen
  • 7,944
  • 12
  • 78
  • 165
  • 2
    It's a good practice to use PRG (Post Redirect Get) to (mostly) avoid this problem. – Ja͢ck Jan 28 '14 at 07:35
  • @Jack:PRG is not useful in my case as I am updating the score in background and cannot redirect him to a page.I want to implementing a scoring system similar to stack overflow – Naveen Jan 28 '14 at 07:44
  • @InsaneCoder read my latest comment in my answer, if you dont need a time you can just make a table containing the questionID and the accountID and fetch the result with a WHERE clause matching both accountID and currentQuestionID. If the returned rows are > 0 then you cannot add the score again because the user has already rated. – Steini Jan 28 '14 at 07:46
  • See also http://stackoverflow.com/questions/4142809/simple-post-redirect-get-code-example – Strawberry Feb 03 '14 at 13:48

7 Answers7

2

I would recommend saving the user's state in your database. You should add another table in order to do so.

-----------------------------------
user_id  | question_id  |  answer
-----------------------------------

When a user answers a question you can check if the user had already answered this question. If so, update his answer and if it's the correct answer update him score. This method works assuming you won't present the same question again if the user already answered it correctly.

If you want to use questions multiple times I recommend another method. Use 2 tables:

----------------------------
user_id  | questionnaire_id   
----------------------------

and

------------------------------------------
 questionnaire_id |  question_id | answer
------------------------------------------

Each questionnaire is unique and contains some questions - the answer to each question is empty at the start. Generate new questionnaire each time the user gets new questionnaire and save his answers per questionnaire. This way you can make sure the user can't submit the same questionnaire results twice (or more). If it's the first time the user submit this questionnaire you can update the score, if not, do nothing.

To make sure the user does not change his questionnaire_id manually you can save it in a session on the server so the user won't have access to it.

Itay Gal
  • 10,706
  • 6
  • 36
  • 75
  • For his question the simple 1 Table solution is enough... Just use "INSERT OR UPDATE ANSWER = :answer WHERE user_id = :user AND answer = :answer" - if you want it even more trivial, you can replace the field 'answer' with 'score' and just save a 1 or 0 there, if the question was right or wrong - if you want to display the score to the user, just query sum('score') where user_id = :user – Falco Feb 10 '14 at 13:18
1

I would suggest using form keys, also known as NONCE.

This means that each time a submission is made, a new form key (NONCE) is generated.

Each NONCE can only be used once and the NONCE must be valid for the form submission to work.

Most modern frameworks have something like this built in as standard.

See this article for a more in depth explanation of the idea: http://net.tutsplus.com/tutorials/php/secure-your-forms-with-form-keys/

And this section of the Symfony2 CSRF protection on forms which uses the same technique: http://symfony.com/doc/current/book/forms.html#csrf-protection

edmondscommerce
  • 2,001
  • 12
  • 21
  • :I think this might be the solution .I am looking into it – Naveen Feb 04 '14 at 07:42
  • After banging my head against the wall trying other methods ,I finally ended up using what you suggested and Its a fabulous way,saving the drudgery of my server. +1 – Naveen Feb 12 '14 at 13:39
0

There are different possible solutions for problems like this. It is basically the same with visitor counters or polls.

Atleast you have to store your information somewhere if there user as already triggered that script and redentify him on every page call.

  • The first and best method is a user account to login and save it in the PHP $_SESSION or directly in the database linked to the user_id / account_id. But this if your page doesnt have a login right now this is too much for a smaller problem I guess. But if you have already one login panel this is by far the best solution.
  • Another method is to save a cookie which may be a legal problem in some countries lately if the user doesnt agree to that before hand and cookies can be deleted so there it is easy to manipulate.
  • You can also save the users IP Address: Harder to manipulate (requires restart of internet and such and noone will do that a dozen times to fake your score counter) but if multiple people are sharing the same internet connection only one of them can achive one score.

All of them have different advantages and disadvantages. Depending on how paranoid you are you could also combine multiple of them if you want to make cheating / abusing harder but that decision is up to you.

Steini
  • 2,753
  • 15
  • 24
  • :Yes I have a login system and first method seems best.When the user login in I pick the last score from database and add the score value to it and display the total to the user so he sees latest score.But when would I update the score to database? because its possible that the user didn't logout. – Naveen Jan 28 '14 at 07:36
  • You should save it immediately. It is all a matter of the database structure. For example: table Score(account_id, timestamp, question_id, score); could look like this. Then you fetch simply SELECT * FROM `score` WHERE `question_id` = $currentQuestionID, `account_id` = $user_id AND `timestamp` + 86400 < time(); This for example would return all results that are not expired in the last 86400 seconds (1 day). If rows are > 0 then the user cannot be saved again. This is just a scribble with one possibility of saying ok the user may do it once a day. You need to make it fit your scenario ofcourse. – Steini Jan 28 '14 at 07:41
  • :My database structure is like **user** (id,name,points) and the questions are fetched from table **questions** (id,title,option_a,option_b,option_c,option_d,answer) – Naveen Jan 28 '14 at 07:50
  • Then create a new table user_questions(user_id, question_id, option); This is helps to fetch the results in general and also to determine if the user already had an attempt on this question. – Steini Jan 28 '14 at 08:02
0

Check the $_SERVER['HTTP_REFERER'] value.

  • If it's the same the page: is reloaded. (do nothing)
  • If it is the previous: update database
  • If it is another domain: illegal access (redirect to first question)
ʰᵈˑ
  • 11,279
  • 3
  • 26
  • 49
Nijboer IT
  • 1,178
  • 1
  • 13
  • 18
  • :I think to handle the last case I can also use another approach like setting the `temp_score` per page to 0 after the update so that even if it from another domain,0 is added.But your approach saves that query.Nice and smart answer. Hope it doesn't have any pitfall .Waiting for others before I award you the bounty – Naveen Feb 04 '14 at 07:58
  • :And one more thing ,since the score is calculated at the client side using javascript for a page.There are chances that the user may alter the code or pass score manually.I can obfuscate the code using compressor but is there any better solution or advice from your side. – Naveen Feb 04 '14 at 08:02
  • using a combination of AJAX and $_SESSION i suppose would do the trick. send the score through AJAX to the server and then calculate the total score and store it into a session. – Nijboer IT Feb 05 '14 at 17:49
0

Consider the following setup;

users
+------------+-------------+------+-----+---------+----------------+
| Field      | Type        | Null | Key | Default | Extra          |
+------------+-------------+------+-----+---------+----------------+
| user_id    | smallint(5) | NO   | PRI | NULL    | auto_increment |
| username   | varchar(10) | NO   |     | NULL    |                |
+------------+-------------+------+-----+---------+----------------+
 ... You'll have more columns, but you get the idea

-

questions
+----------+--------------+------+-----+---------+----------------+
| Field    | Type         | Null | Key | Default | Extra          |
+----------+--------------+------+-----+---------+----------------+
| qid      | smallint(5)  | NO   | PRI | NULL    | auto_increment |
| question | varchar(10)  | NO   |     | NULL    |                |
| votes    | smallint(5)  | NO   |     | 0       |                |
+----------+--------------+------+-----+---------+----------------+

-

votes
+--------+-------------+------+-----+---------+-------+
| Field  | Type        | Null | Key | Default | Extra |
+--------+-------------+------+-----+---------+-------+
| qid    | smallint(5) | NO   |     | NULL    |       |
| user_id| smallint(5) | NO   |     | NULL    |       |
+--------+-------------+------+-----+---------+-------+

In this setup, I'm userid 1 and voting for question id 1

When a user votes, their vote is placed within votes

INSERT INTO `votes` (`qid`,`user_id`) VALUES (1, 1);

To check they've already voted, simply do;

SELECT `user_id` FROM `votes` WHERE (`user_id`=1) AND (`qid`=1);

If that query returns any rows, we know the user has already voted, and we shouldn't process the duplicate vote.

Of course this only restricts us to one type of voting - positive, or negative - whichever you decide to track. We can adapt votes to store the type of vote it is;

ALTER TABLE votes ADD type ENUM('up', 'down') NOT NULL DEFAULT 'up';

Which will make our table structure to the following;

+---------+-------------------+------+-----+---------+-------+
| Field   | Type              | Null | Key | Default | Extra |
+---------+-------------------+------+-----+---------+-------+
| qid     | smallint(5)       | NO   |     | NULL    |       |
| user_id | smallint(5)       | NO   |     | NULL    |       |
| type    | enum('up','down') | NO   |     | up      |       |
+---------+-------------------+------+-----+---------+-------+

And, again, adapt the lookup query;

SELECT `user_id` FROM `votes` WHERE (`user_id`=1) AND (`qid`=1) AND (`type`='up');
ʰᵈˑ
  • 11,279
  • 3
  • 26
  • 49
  • @h: I think you read the question wrong.Even if I look the vote system you have suggested , in my case it restricts the user to attempt the question only once in his lifetime.But as I quoted ,he may attempt the question any times but the only thing I want is to stop updations on page refresh. – Naveen Feb 04 '14 at 07:39
  • Ah my bad, apologies! – ʰᵈˑ Feb 04 '14 at 08:38
0

The most foolproof system I see is based on tracking the whole lifetime of a given quizz.

If you store a "current question number" associated with the user and this particular quizz, you can easily filter out duplicate responses:

update_score ($question_number, $choice)
  if current question for this quizz and user is not set to $question_number
    ignore request
  else
    set choice for this specific question and update score
    increment current question (possibly reaching the end of the quizz)

When the last question is answered, the final score is displayed/recorded and the "current question" reset to 0.

If the user wants to retry the test, current question is set to 1 and the whole process restarts.

If the user wants to cancel the current test and restart, he/she can do so by going back to quizz start page.

So any attempt to submit a second answer to the same question would fail (be it from accidental refresh or malicious attempts), until the quizz is finished and you can start back with question 1.

kuroi neko
  • 8,479
  • 1
  • 19
  • 43
0

You can use a toggle session variable approach (name it as flag),which is the simplest and has a good level of security against duplicate requests.

Make a script called updateScore.php .When the user login set the flag=1 ,which means when the next request comes for updation,process it in updateScore.php and at the end of ths script make flag=0. When the next page appears again make flag=1.This way you alternate the values and also set a maximum update limit in your script,say, in your case you have 5 questions so you can set it to 50 (+10 per question). You can take more complicate values of flag to reduce guess chances.

EduardoSaverin
  • 545
  • 4
  • 19
  • Your solution only works for innocent users who tries to submit the form twice accidentally. The user can submit the same form once, then, wait for the session on the server to expire and then submit the same form once again. In this solution there are few more scenarios that enables the user to misuse the site and cheat the scoring system. I would advice to build a robust system and not to use this solution. – Itay Gal Feb 10 '14 at 14:22