Our ability to answer the poll repeatedly has been very useful while testing, but makes it trivial for anyone to skew our poll by hitting “reload”. We need to track their visits, so that we know when and whether they’ve voted in the past. This is what cookies are for. You set a cookie on the client’s browser, and every time the browser visits you in the future, the browser returns the cookie. You can use that cookie to look up information about that visitor. PHP manages this via the $_SESSION variable. At the top of fields.phpi, start a session:
session_start();
And in the VoteCounter class, add a method to get and set the visitor’s current vote status:
protected function remember() {
if (isset($_SESSION['lastvisit'])) {
$this->lastVisit = $_SESSION['lastvisit'];
if (isset($_SESSION[$this->fieldName])) {
$this->existingVote = $_SESSION[$this->fieldName];
}
} else {
$this->lastVisit = null;
}
$_SESSION['lastvisit'] = time();
}
In the list of properties, add:
protected $lastVisit;
protected $existingVote;
When accepting their vote, we should make sure they’ve visited us in the past, and aren’t either coming from some other site or turning cookies off to vote multiple times:
protected function clean() {
$this->remember();
parent::clean();
if (isset($this->value)) {
//if they haven’t visited us before, this can’t be a valid vote
if (!$this->lastVisit) {
$this->value = '';
return;
}
//if they already made a vote, they can’t vote again
if (isset($this->existingVote)) {
$this->value = '';
return;
}
//if their vote isn’t in the list of valid votes, cancel
if (!isset($this->choices[$value])) {
$this->value = '';
}
}
}
And, in the save method, after writing and closing the ballot file save their vote in the session.
if ($this->submitted && $this->value !== '') {
$choice = $this->value;
$ballots = fopen($this->filePath, 'a');
fwrite($ballots, "$choice\n");
fclose($ballots);
$_SESSION[$this->fieldName] = $choice;
}
You should be able to verify this by voting one more time, and then not being able to vote again.
Now, remember: no trust. Checking their last visit in the session will ensure that they visited the page before voting. It does not ensure that they submitted from your page, just that they visited it.
When you start a session in PHP, PHP automatically checks to see if there is already an existing session on the client. You can store data in the session using $_SESSION and then look in $_SESSION in later visits. So here, after we start the session, we check to see if the session data already includes a choice having been made. If it is, we save when they last visited again, but clear any potential double-vote.
Display the results when they’ve already voted
At the moment, it’s very confusing. If they’ve already voted, we give them the form and then refuse their answer. We should use the session to display either the form or a thanks for voting message.
Override the answered method:
public function answered() {
if (isset($this->existingVote)) {
return true;
} else {
return parent::answered();
}
}
We now remember if they made a choice and what the choice was, so we can remind them who they voted for.
<p>Thanks for voting for <?php echo $vote->name(); ?>!</p>
And, for the name method, use:
public function name() {
if (isset($this->existingVote)) {
$vote = $this->existingVote;
} elseif ($this->value != '') {
$vote = $this->value;
}
return $this->choices[$vote];
}
If they voted this time, use that vote; otherwise, if they voted sometime before, use that. Then, look that key up in the array of possible votes to get the display name of that vote.
Cookie errors
When you try to load this page after editing it, you might see something like:
Warning : session_start() [ function.session-start ]: Cannot send session cache limiter - headers already sent (output started at /home/johndoe/public_html/php/poll.php:2) in /home/johndoe/public_html/php/poll.php on line 4
This means that you did not put the session_start line at the very top of your web page. Cookies are not part of your web page’s document area. They are part of the “headers” that you rarely see as a user. The headers must be sent before the document is sent. Once you start sending the document, even if it is just a space or a carriage return, you can no longer send any headers. Make sure that the “<?” that starts your PHP code is at the very top of your file.
Sessions and security
It’s still pretty easy to “beat” our poll. All anyone has to do is delete the cookie from their browser. If they delete the cookie, they’ll never send the cookie back to us, and we’ll never know that they were already here. You can test that: find our cookie in your web browser’s preferences, delete it, and you can vote once again.
Sessions are based on cookies. Cookies are fully under the control of the browser. No matter how much you try to secure your application from cookie manipulation, you cannot win. You cannot rely on cookies being kept. Sessions are useful for casual polls, but not for, say, electing a president.