1

Folks, I know this topic has been hashed to death, but after reading many answered questions here, I'm no closer to a solution.

The problem: after submitting a form, even with Post-Redirect-Get, users can still press the back button to return to the original form, which will be exactly as it was when posted. Users can then just press the submit button to send the same data again.

I realize it's bad practice to try to disable the back button, so I would like either: 1.- to be able to clear the form so that the data the user entered is no longer there when returning to the posted form via back button. 2.- to be able to distinguish in code between original form submission and repeat form submission.

I've been reading that you can accomplish #2 with a random number. In Stop data inserting into a database twice I read

I use nonces ("number used once") for forms where resubmission is a problem. Create a random number/string, store it in the session data and add it to the form as a hidden field. On form submission, check that the POSTed value matches the session value, then immediately clear the session value. Resubmission will fail. (James Socol)

Seem clear enough but I can't seem to get that to work. Here's skeletal code, as brief as I can make it. I've single-stepped through it. When I press the back button, the PHP code starts executing from the start. $_SERVER['REQUEST_METHOD'] returns GET so it's as if the form has not been posted, and the code falls through and generates a new random number. When the form is submitted (the second time), $_SESSION['rnd'] equals $_POST['rnd'] so the resubmitted form is processed as if it were the first time.

<?php
  session_start();
  if ($_SERVER['REQUEST_METHOD'] == 'POST') {
    if ($_SESSION['rnd'] == $_POST['rnd']) {
      $form_data = $_POST; //process form
      $_SESSION['rnd'] = 0;
      $msg = 'data posted';
    }
    else {
      $msg = 'cannot post twice';
    }
  }
  else {
    $msg = 'ready to post';
    $rnd = rand(100,999);
    $_SESSION['rnd'] = $rnd;
  }
?>
<html>
  <head>
    <title>app page</title>
  </head>
  <body>
  <h1>app page</h1>
  <h3><?= $msg ?></h3>
  <form method="post">
    Field 1: <input type="text" name="field1"
      value="<?= (isset($form_data['field1']) ? $form_data['field1'] : '') ?>">
    <input type="submit" name="submit" value="Ok">
    <input type="hidden" name="rnd" value="<?= (isset($rnd) ? $rnd : $_POST['rnd']) ?>">
  </form>

  </body>
</html>

Thanks for any insight. I realize the above code does not implement PRG, but I think it doesn't matter. The issue isn't an F5 refresh but a back button refresh.

Community
  • 1
  • 1
RobertSF
  • 488
  • 11
  • 24
  • Why don't you just redirect after the the submission? – SBD Dec 09 '14 at 22:07
  • Redirecting protects against accidental resubmission when the user presses F5, but it doesn't protect against deliberately using the back button to return to the page and submit a new form without having to fill it out. Perhaps I should explain the application. This is for a survey generator a professor uses to survey his students. He wants to prevent some prankster from filling out the survey, and then going back-button/submit 50 times, thus introducing lots of noise into the survey. But the surveys are anonymous, so we can restrict on a per-user basis. – RobertSF Dec 09 '14 at 22:26
  • A possible solution could be `js` based where you simply reset the form after submitting, thus they cant retrieve the info on going back. But I dont know if you want to walk that road – Dorvalla Dec 09 '14 at 22:31
  • It maybe worthwhile keeping the 'nonce' in a database rather than a session. The session times out after a few minutes. An 'sqlite' database is fine for this sort of thing. – Ryan Vincent Dec 09 '14 at 22:31
  • @RyanVincent Yes, no doubt, but in a database or in a session or just written to a text file, the problem still remains: when the PHP script starts running, how do you tell whether it's running because it was executed from the browser's URL bar or because the user pressed the back button. There appears to be no way to do that. – RobertSF Dec 10 '14 at 01:36
  • You generate the 'nonce' and save it when the request is first made. You update it with, say the time, when the form comes back. If it comes in again it must be a repeat request. Think of it as each new request having a unique id. They only have to be kept for a day or two so the volumes will not be excessive. If you get a form without a matching id it must be old or invalid. Ensure the 'nonce' is random so it cannot be guessed. – Ryan Vincent Dec 10 '14 at 03:41
  • @RyanVincent That's the theory, but the flaw is that the browser treats the return to a previously submitted form as a new request. Your script, then, generates a new nonce and perhaps even some variables to set default values in the form's text inputs. But when you press the submit button, the $_POST array returns the values left over from the previous form submission, even though you assigned default values to those text inputs. Looks like values stored in the HTML document override attempts to set defaults. I think I'll have to figure out a way to clear those value with javascript. – RobertSF Dec 10 '14 at 13:31
  • Sorry, is not as easy as i thought. Main issue is not creating lots of 'useless' nonce records. Working on it. ;-/ – Ryan Vincent Dec 10 '14 at 18:21
  • If the 'form' is sent then it is quite easy to work out whether the 'back button' has been used. A 'nonce' in a hidden field, in $_SESSION and on the database work fine for that. It is what to do if you don't get the form that i am finding 'awkward' in knowing what to return to the user. This is because i have a 'nonce' in the 'session' and i ain't sure of what importance to give it. – Ryan Vincent Dec 10 '14 at 21:13
  • @RyanVincent Thanks for giving it a second look. At least I know I'm not crazy here. :) But please tell me what you mean by "If the 'form' is sent then it is quite easy to work out whether the 'back button' has been used." Are you using 'sent' in any special way? The code I'm using to test this is here: http://viper-7.com/w3vxIM (if you know of a better PHP sandbox, please let me know). After stepping through the code with XDebug, I think there is no solution because, while an F5 refresh returns a POST array, backing into a previously submitted form does not. – RobertSF Dec 10 '14 at 23:44
  • I will start an answer - but it is really just a comment area - it is also getting late here. I will start it up and point out what i think you need to look at. I will continue 'tomorrow' or rather, later today. – Ryan Vincent Dec 11 '14 at 00:22

3 Answers3

1

You need to add some logic to setting the $_SESSION['rnd'] variable rather than just setting it to 0 on form submission. You may want to also set a cookie with a really long time out. I've added the cookie in this as well.

  session_start();
  if ($_SERVER['REQUEST_METHOD'] == 'POST') {
    if ($_SESSION['rnd'] == $_POST['rnd']) {
      $form_data = $_POST; //process form
      $_SESSION['rnd'] = "POSTED";
      setcookie("rnd", "POSTED", time()+3600*24*14); //cookie expires in 2 weeks
      $msg = 'data posted';
    } else {
      $msg = 'cannot post twice';
    }
  } else {
    $msg = 'ready to post';
    $rnd = rand(100,999);
    if($_SESSION['rnd'] == "POSTED" || $_COOKIE['rnd'] == "POSTED"){
        $_SESSION['rnd'] = "POSTED"; //set value of session if you're here because the cookie was detected
        $rnd = -100;
    } else {
        $_SESSION['rnd'] = $rnd;
    }
  }

If you want to reset the form on back the back button you can do that with this pretty simple javascript.

<script>
    var posted = document.getElementById("rnd").value();
    if(posted < 0){
        document.getElementById("form").reset();
    }
</script>

You'll need to add the id attribute "form" to your form for it to work

  <form id="form" method="post">
quid
  • 1,024
  • 9
  • 9
  • Thanks! I see the changes you made to the code. I'll try them here locally. Just to clarify, the cookie is a good idea but am I correct that, strictly speaking, a cookie AND a session variable are not both necessary to achieve the goal? Also, javascript is my weakest link, so to what event would I attach that javascript code? On the form's onsubmit event? Or is that too early? – RobertSF Dec 09 '14 at 23:04
  • You're correct that they're not both necessary to achieve the same goal, but there are situations where a browser may not accept the cookie but accepts the session variable. You're just covering all your bases to recapture the person if they've already submitted the form. Regarding the javascript, when I tested it, placing it as the last item before the

    tag it just fires when the page is loaded.

    – quid Dec 09 '14 at 23:39
  • Thanks, quid. Your proposed code worked a little too well. Once the form is submitted, no further submission can be made at all because $_SESSION['rnd'] doesn't get reset from 'POSTED'. But that's because you can't tell when the form has been backed into. I think the solution will be a link that says "Enter Another Record," and that link will reset $_SESSION['rnd'] and then redisplay the form. But if the user has finished entering records and goes to Yahoo instead, the browser will be left in a state where new records can't be entered. Oi very, why does this have to be so hard? :) – RobertSF Dec 10 '14 at 23:45
1

This is not an answer yet - but is so i can post points and delete and change 'em.

RobertSF: i am working on an answer for this as i have to understand what to do in these circumstances.

I have had some sleep and am having a coffee.... :-)

I think the 'browser back button' use is a 'red-herring'.

If we state the 'requirements' of the program then we should be able to see what the issues are and what we can do about them.

1) Once the data has been validated and accepted from the 'form' then it becomes 'read ony'. This is not that unusual a requirement.

2) There is no 'obvious' key in the data accepted from the form so we have to we have to generate a 'unique' key for each 'form full' of data. Also each 'form full' of data is to be considered as an 'event'.

3) A user can enter the same data as we already have but if entered under a different 'event' than it must be accepted.

However, if we can write some code that enforces those three requirements then the 'browser back button and resubmission' of data we already have accepted once should not be a problem. It will be reported as already being on the database.

So, how do we do this?

We have to generate 'event keys' that are unique and are also the key to the 'form full of data'.

We can use 'nonces' to do that.

Every 'nonce' has a 'state' with it such as 'newRequest', 'hasData'.

I will write a full, but minimal, program that will satisfy the requirements. We may as well 'structure' it so it can be easily understood so i will use the 'mvc' structure.

I haven't written it in the 'mvc' form yet. It just needs restructuring. Will post it as an answer later. What follows is some of my earlier thought about the issues.

Every form you send out MUST have a unique 'nonce' in it where the state of it can be checked when the, maybe, same form comes back later! In effect there are many possible 'active' forms 'out there' each with a 'unique nonce' and the associated state.

That is why i store them in a database.

If you want to use $_SESSION then you must use an array of 'nonces' and the current state for each one.

The state is: set to: 'hasData' when the user enters data in the form and it is accepted.

Ryan Vincent
  • 4,483
  • 7
  • 22
  • 31
  • Thanks for hanging in there! I don't envision multiple active forms being out there, but your #3 is spot on. Two identical form submissions are not invalid if submitted by two different users (the application is a survey), but since the surveys are anonymous, we can't tell if two forms were filled out by the same user. *However*, the survey is long enough that no one will bother filling out two surveys. We're only trying to stop the prank of easily submitting duplicate forms by using the back button (yeah, back to the back button). – RobertSF Dec 12 '14 at 01:54
  • I hear what you're saying, but it doesn't work because the back button behavior tricks you into giving a previously submitted form a new unique id. And when that previously submitted form is submitted the second time, its unique id matches what you have on file, so you process the form as if it were a new form -- but it's not. – RobertSF Dec 12 '14 at 07:51
0

I look forward to Ryan Vincent's answer, but in the interim, after much hair pulling, here's the code I've devised that handles this situation. To recap, we're talking about preventing duplicate form submission. However, the circumstances make this case somewhat out of the ordinary.

The application is a survey of 150 questions that a professor has his students take at the start and end of the semester. The surveys are anonymous, and students may take them anywhere there's internet access, but many will take the survey at the school's computer lab, where several students taking the survey may gather around the same computer.

Anyone with the URL to the survey can take the survey, but we don't worry about non-students taking it. It's hard enough to get the students to take it, but we don't want students, as a prank, to be able to use the back button to return to the form with all its data still there and submit it again and again and again. On the other hand, it would be perfectly valid for two students to take the survey, one after the other, and submit identical responses. Yet because the surveys are anonymous, we can't distinguish between one student and another, though given the length of the survey, we're not worried about the same student filling a survey out twice.

In pseudo-ish code with a PHP flavor, here's what I came up with.

// application.php
start_session()
if form_posted
  validate_form
  if no_errors
    save_form
    $_SESSION['form_status'] = 'closed'
    header ('Location: confirmation.php')
  else
    //errors, so redisplay form
    $caller = 'process'
    include ('form.php')
  endif
else //form not posted
  if (!isset($_SESSION['form_status']))
    //first time displaying form
    $_SESSION['form_status'] = 'open'
    $caller = 'process'
    include ('form.php')
  elsif $_SESSION['form_status'] == 'open'
    //user refreshed form before submitting it
    unset ($_SESSION['form_status'])
    header ('Location: application.php')
  elsif $_SESSION['form_status'] == 'closed'
    //user pressed back button from confirmation_page
    header ('Location: confirmation.php')
  endif
endif

//confirmation.php
$caller = 'confirmation'
include ('form.php')

//form.php
<form>
  fields
  fields
  fields
  //only show submit button when $caller == 'process'
  if $caller == 'process'
    show_submit_button
  elseif $caller == 'confirmation'
    link ('click to fill out another form', return.php)
  endif
</form>

//return.php
session_start()
unset_session_variables
header('Location: application.php');

As with all scripts that post to themselves, the script is running either because the form as posted or for some other reason. If the form was posted, validate it, save the data, mark the form "closed," and redirect to a confirmation page.

If the form was not posted, there could be several reasons why we're here. One reason is that this is the start of a form cycle, first time through. In that case, mark the form 'open' and show the form. Another reason is that the user, while filling out the form, refreshed the form. In that case, reset the form status and let the script think it's the first-time run. But if the form status is 'closed,' that can only mean that the user was staring at the confirmation page and pressed the back button. In that case, we simply return the user to the confirmation page.

The confirmation page has a link to open a fresh form. That link resets the values in the session and then redirects to the main application script, which then acts as if it's the first time through. If a user submits the form and takes no action at the confirmation page but instead goes to Yahoo! or YouTube, the next user who tries to take the survey will get a confirmation page showing the data the earlier user entered. This is not a problem, as this next user can use the link in the confirmation page to generate a clean, fresh form.

RobertSF
  • 488
  • 11
  • 24