سؤال

I'm rewriting a bunch of forms around a newer paradigm, trying to simplify the mechanism used to prevent reprocessing of stale form data if a user plays with the Back button. I've distilled it down to a minimal test case that is working in FF21, ie10(!), Opera12.15, but only so-so with Chrome 27.0 and Safari 5.1.7 -- not the browsers I would have expected problems with, so it's probably my own fault.

Form view.php always posts to post.php, which does all the work and then redirects to view.php. (Some of these are design pages, where some users iterate many times.) My goals have been to avoid the "Browser must resubmit data" message, to keep the browser from using cached views, and to keep view.php from leaving a long trail in the history. (Some sources state that PRG prevents that.) All three goals are met in FF, Opera, and IE, where no history grows. Chrome and Safari show a trail of identical page views in the history. Looking at request/response headers, it appears that Chrome does a full GET of any historical page revisited, but it displays a mixture of historical and current field values on the same page(!). That's the single most confusing aspect to me, I think, the showing of a combination of values that never coexisted on one page. View Source in Chrome always shows an up-to-date copy. And I'm talking about behavior of the exact distilled examples below, not some more complicated version of them.

Field one echoes an internal counter and ignores user input. It acts the same odd way whether it is readonly or not.

Field two echoes user input. I usually copy or append field one to it.

view.php

<?php

session_start();
header('Cache-Control:private,no-store,no-cache,must-revalidate,post-check=0,pre-check=0');
header('Pragma: no-cache');


$cval = !isset($_SESSION['counter']) ?  0 : $_SESSION['counter'];
$word = !isset($_SESSION['word'])    ? '' : htmlentities($_SESSION['word'],ENT_QUOTES,'UTF-8',FALSE);

echo <<<HTM
<pre>
<form method='POST' action='post.php'>
<input type='text' name='count' value='$cval'/>
<input type='text' name='word' value='$word'/>
<input type='submit' name='doit' value='Submit'/>
</form>
</pre>
HTM;

?>

post.php

<?php

session_start();

if(!isset($_SESSION['counter'])):
    $_SESSION['counter'] = 1;
else:
    $_SESSION['counter'] += 1;
endif;

$_SESSION['word'] = !isset($_POST['word']) ? '' : $_POST['word'];

header('Location: view.php');


?>

If I start backing up through the history, Chrome tends to show the up-to-date Field-one value but shows historical Field-two values. Occasionally it shows historical on both. Without that disparity I'd assume it was a failure to prevent the server (on pair networks) or a proxy from caching the page.

It does the same thing if I add an 'Expires: -1' header.

I can handle recognizing a stale form; what I can't seem to do is prevent a user from reaching or submitting one. An incrementing GET argument on view.php would prevent unpredictable cached views but would create an explicit history in all browsers and only invite people to turn the ratchet backwards.

Why is this happening and is it fixable?

هل كانت مفيدة؟

المحلول

In the absence of contrary advice, it appears there was nothing much wrong my implementation of the p-r-g model. Firefox, IE, and Opera work exactly as intended and Safari just blindly puts every GET into the history. But Safari's history is easily handled by inserting hidden or readonly freshness tokens into the form so that you can recognize a stale form.

The stumbling block is a Chrome quirk and I don't see a pure PHP/HTML way around it. (Duly reported, BTW.)

Here is new code, only a smidgen more complex, that makes the behavior very clear. Just do a few submits, typing something different in field Inval every time, then look back through the history.

Chrome always correctly fetches a new copy of the view, even if you go back into the history. But if you revisit visit #x, Chrome will override the pre-population values for any fields you touched on visit #x, putting in stale data instead of what the fresh view specified. (Check the mismatch between the data in field Inval and the prepopulation value shown to its right.) This is fixable by using JavaScript to re-pre-populate the corrupted fields by copying the prepopulation data into them from hidden elements you embed in the page.

The new VIEW.PHP

<?php # view.php

session_start();
header('Cache-Control: private, no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: -1');

# Format timestamp and counter
$tval = date("H:i:s");
$cval = !isset($_SESSION['counter']) ?  0 : $_SESSION['counter'];

# Format last-posted values for display but (mostly) not for prepopulation
$lphiddn = !isset($_SESSION['xhiddn'])    ? '' : htmlentities($_SESSION['xhiddn'],ENT_QUOTES,'UTF-8',FALSE);
$lpcount = !isset($_SESSION['xcount'])    ? '' : htmlentities($_SESSION['xcount'],ENT_QUOTES,'UTF-8',FALSE);
$lpinval = !isset($_SESSION['xinval'])    ? '' : htmlentities($_SESSION['xinval'],ENT_QUOTES,'UTF-8',FALSE);

# Format pre-population data
$phiddn = "H $tval";          //  H 00:00:00
$pcount = "(#$cval) $tval";   //  (#1) 00:00:00
$pinval = $lpinval;           //  whatever was last posted for this field

echo <<<HTM
<pre style='font-family: monospace;'>
<form method='POST' action='post.php'>
Hiddn <input type='hidden' name='hiddn' value='$phiddn'/> prepop=$phiddn   lastpost=$lphiddn
Count <input type='text'   name='count' value='$pcount'/> prepop=$pcount     lastpost=$lpcount
Inval <input type='text'   name='inval' value='$pinval'/> prepop=$pinval = lastpost=$lpinval
Submt <input type='submit' name='doit' value='Submit'/>
</form>
</pre>
HTM;

?>

The new POST.PHP

<?php # post.php

session_start();
header('Cache-Control: private, no-store, no-cache, must-revalidate, max-age=0');
header('Pragma: no-cache');
header('Expires: -1');

if(!isset($_SESSION['counter'])):
    $_SESSION['counter'] = 1;
else:
    $_SESSION['counter'] += 1;
endif;

$_SESSION['xhiddn'] = !isset($_POST['hiddn']) ? '' : $_POST['hiddn'];
$_SESSION['xcount'] = !isset($_POST['count']) ? '' : $_POST['count'];
$_SESSION['xinval'] = !isset($_POST['inval']) ? '' : $_POST['inval'];
header("HTTP/1.1 303 See Other");
header('Location: view.php');


?>
مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top