Source Code for /public/06-retaining-state/index.php
<?php
require('../inc/functions.php');
add_header('Lesson 6: Sessions & Cookies');
?>
<div class="objectives">
<h2>Objectives</h2>
<ul>
<li>Understand the concept of cookies and how they work</li>
<li>Use a cookie to detect repeat visitors</li>
<li>Use a session to track a user's login status</li>
<li>Experience a game that uses a session to retain a user's progress</li>
</ul>
</div>
<h2 id="what-are-cookies">What are cookies?</h2>
<p>
The Web was invented to be "stateless". Every page you visit would be a clean slate, with no memory
of what came before.
</p>
<figure>
<img src="/img/mermaid/http-exchange-without-cookies.svg" alt="A browser asks a server for index.php twice, and gets the same 'Welcome to our website!' response both times.">
<figcaption>A traditional HTTP exchange: each request and response exists independently.</figcaption>
</figure>
<p>
By the mid-1990s, it became clear that websites might benefit from some way of being able to "track"
a user from one page load to another, to enable things like login sessions and shopping carts.
The solution was <em>cookies</em>.
</p>
<p>
When you visit a website, it can optionally send a string to your browser, called a cookie. If your
browser accepts the cookie, it will send it <em>back</em> for every page it subsequently
requests from that site. It'll do this until either the cookie expires, the user deletes it, or
the website asks the browser to delete it.
</p>
<figure>
<img src="/img/mermaid/http-exchange-with-cookies.svg" alt="A browser asks a server for index.php twice; the first response includes Set-Cookie: visited=1, which the browser parrots back as Cookie: visited=1 in its second request. This prompts the server to respond with a 'welcome back' message on the second visit.">
<figcaption>Where the server sets a cookie, the browser returns it on each subsequent request. This allows the server to identify repeat visitors.</figcaption>
</figure>
<p class="tip">
Before cookies became mainstream, imaginative developers came up with other ways to approximate their
functionality, e.g. appending GET parameters to every internal link to "pass on" information
to the next page, but this was not always reliable (e.g. if the visitor opened multiple
browser windows pointing to the same website, for example).
</p>
<h2 id="detecting-repeat-visitors">Detecting repeat visitors</h2>
<p>
Let's have a go at implementing the thing from the diagram above - a web page that welcomes you with a different
message if you've visited the page before. Here's <code>welcome-back.php</code>:
</p>
<?php code_block_from_file( '06-retaining-state/welcome-back.php', 'php' ); ?>
<p>
<a href="welcome-back.php">Visit the page</a> and you'll see the message saying it's your first visit.
Refresh the page and you'll see it says "welcome back"!
</p>
<p>
Try opening the page in a private browsing window and you'll see that it doesn't recognise you. A private browsing
window keeps a separate cookie store from your regular browsing window, and usually deletes all cookies when
it's closed.
</p>
<p class="tip">
Could you extend this program so that it counts <em>how many</em> times the visitor has returned to the page?
<small>(<a href="/demo-harness.php?file=public/appendix-samples/welcome-back-counter.php">🏁 view solution</a>)</small>
</p>
<p>
There are lots of options for the <code><a href="https://www.php.net/manual/en/function.setcookie.php">setcookie()</a></code> function.
You've seen the first three (name, value, and lifespan). The others, in order, are:
</p>
<ul>
<li>
<code>path</code>: which <em>part</em> of your website the cookie belongs to, by default it'll be the current directory
your code is in, but if you'd like your cookie to be available throughout your entire site, you can set it to <code>'/'</code>.
</li>
<li>
<code>domain</code>: if your site spans multiple subdomains and you want your cookie to span them all, you'll need to se this. E.g.
if your site spans <code>cool.example.com</code> and <code>funny.example.com</code>, a cookie will need to set a <code>domain</code>
of <code>example.com</code> to be accessed from both of them.
</li>
<li>
<code>secure</code>: setting this to <code>true</code> tells browsers only to send the cookie over HTTPS connections.
</li>
<li>
<code>httponly</code>: cookies can also be manipulated using JavaScript, but if you set this to <code>true</code> then
browsers won't let JavaScript access this cookie. That's useful if there's a chance that malicious JavaScript might end up
being run on your site.
</li>
<li>
<code>options</code>: if you want to do advanced things with cookies, like configuring how they behave when people
hotlink to your page, you'll need this.
</li>
</ul>
<p class="warning">
<code>$_COOKIE</code>s are example of <em>user input</em>, which means that
<a href="/appendix-security#xss">users can tamper with them</a>.
Try it for yourself: after
<a href="/demo-harness.php?file=public/appendix-samples/welcome-back-counter.php">visiting the "visit counter" page</a>,
open your browser's Developer Tools (<kbd>F12</kbd> or <kbd>Ctrl+Shift+I</kbd> in most browsers), go to the "Storage" tab,
and change the value of the "visited" cookie to a different number. Then visit the page again to see the change!
<small>
(<a href="/appendix-security#cookies">How can cookies be secured?</a>)
</small>
</p>
<p class="tip">
Using what you learned in <a href="/05-accepting-input/">the previous lesson</a>, could you ask the user for their name
and then use it whenever they come back?
<small>(<a href="/demo-harness.php?file=public/appendix-samples/remember-my-name.php">🏁 view solution</a>)</small>
</p>
<h2 id="sessions">Logging in (and staying logged in)</h2>
<p>
The easiest way to use cookies in PHP is with PHP's <a href="https://www.php.net/manual/en/ref.session.php">session functionality</a>.
Sessions are a tamper-proof implementation of cookies (with, usually, a short life: until the user closes their browser) that provide
easy-to-access variables to developers.
</p>
<p>
Just call <code>session_start()</code> near the top of your script, and you can use <code>$_SESSION</code> to store and retrieve data.
Let's make an admin section of a website that can be logged-into with a password. Here's <code>login.php</code>:
</p>
<?php code_block_from_file( '06-retaining-state/login.php', 'php' ); ?>
<p>
Let's also make a members area page that's only accessible if you've logged in - <code>members.php</code>:
</p>
<?php code_block_from_file( '06-retaining-state/members.php', 'php' ); ?>
<p>
<a href="/06-retaining-state/login.php">Have a try at logging in</a> and accessing the members area.
Try accessing the members area <em>without</em> logging in, too (it shouldn't let you!).
</p>
<p class="warning">
If you use a session for authentication, be sure to check it on every page that needs protection! A
common mistake is to e.g. check it on the main "members area" page, but then not check it on sub-pages of the
members area. This means that anybody who can guess the address, or who has ever been logged-in before, can access
those sub-pages!
</p>
<p class="tip">
Look at the new cookie using your browser's Developer Tools (<kbd>F12</kbd> or <kbd>Ctrl+Shift+I</kbd> in most browsers). It'll be
called <code>PHPSESSID</code> and will contain a long random string. If you tamper with it, your session will be invalidated: you'd
have to successfully guess the random string belonging to somebody already-logged-in! The actual information (e.g. whether you're
logged in or not) is kept securely on the server, in a file whose <em>name</em> is referenced by that cookie.
</p>
<h2 id="potion-finder">Potion Finder game</h2>
<p>
Let's make a game! I've hidden four different-coloured potions on different pages of the site. Your job is to find them all.
We'll use sessions to keep track of which potions you've found. <a href="/?show_potions=true">Here's a page with more details</a>,
and a link to <a href="/source-viewer.php?file=public/inc/potion-finder.php">the source code</a>.
</p>
<?php if(count( $found_potions ) === 0) { ?>
<p>
Here's the first one, to get you started:
</p>
<?php } ?>
<?php draw_potion( 'blue' ); ?>
<p>
Here's how it works:
</p>
<ul>
<li>
You may remember from <a href="/04-includes-and-functions/">Lesson 4</a> that this site includes a file called
<code><a href="/source-viewer.php?file=public/inc/functions.php">inc/functions.php</a></code> on every page. Well <em>that</em>
file includes another, called <code><a href="/source-viewer.php?file=public/inc/potion-finder.php">inc/potion-finder.php</a></code>,
which contains all of the logic for this game.
</li>
<li>
It uses <code>session_start()</code> to start a session, and a variable called <code>$_SESSION['potions']</code> to track which potions
have been found.
</li>
<li>
It also provides a convenience function called <code>draw_potion()</code>, which I've used throughout the site. This function
checks if you already have the specified potion and, if not, draws a picture of it with a link to collect it.
</li>
<li>
To prevent tampering with the links to instantly collect all the potions, each potion has a unique password which is included in its
link and checked when the link is clicked.
</li>
<li>
<code><a href="/source-viewer.php?file=public/inc/potion-finder.php">inc/potion-finder.php</a></code> includes logic for showing a
page with the details of the potions collected so far, if you put <a href="/?show_potions=true"><code>?show_potions=true</code></a> on
the end of a URL.
</li>
</ul>
<?php
add_footer();
?>