PHP and MySQL: The Missing Manual (2011)

Part 4. Security and the Real World

Chapter 12. Cookies, Sign-ins, and Ditching Crummy Pop-ups

Yes, it’s time to begin to wind down. You’ve gone from seeing PHP as some strange, cryptic set of angle brackets and dollar signs to building your own application, including integration with a MySQL database, authentication, redirection, and a pretty decent set of utility functions. And no, you’re probably not going to sell your mini-application with its Twitter and Facebook sign-up to Google for a billion dollars or anything. But you should have a pretty good idea of how to think in PHP, and how scripts are structured to solve problems.

But before you can go twist and bend this app and your new skills to your own purposes, there are still some lingering issues that need to be handled. A few of these are nice-to-haves, and some are downright necessities if you’re going to spend your career writing web applications.

Here are just a few things that your application needs to round out both the app and your skills:

§  A better login screen. Nobody likes a bland gray pop-up; they want a branded, styled login form.

§  Better messaging indicating whether a user is logged in or not.

§  A way to log out.

§  Two levels of authentication: one to get to the main application, and then admin-level authentication to get to a page like show_users.php and delete_user.php.

§  Some basic navigation—and that navigation should change based on a user’s login and the groups to which they belong.

These are almost all related to the idea of logging in, and that’s no accident. Whether it’s a good-looking login screen or the ability to group users, you’ll probably spend as much time on the authentication and authorization of your web applications as you do anything else. Even if you have boilerplate code to get a username and password, most web pages are built using components that are only selectively accessible. In other words, a web application shows users different things and gives users different functionality based upon their login.

Get a handle on how to store user credentials, move users around your site, and the issues that underlie keeping up with a user’s information. Yeah, you’re ready to take your programming into the real world.

Going Beyond Basic Authentication

Right now, your authentication uses the browser’s built in HTTP capabilities. Unfortunately, as useful as HTTP authentication is, it leaves you with a pretty lame visual; check out Figure 12-1 for the sad reminder.

The biggest issue with this login page really isn’t its awful look and feel. It’s that you don’t have as much control as you’ll ultimately want. You can’t provide a customized message if the user fails login. You have to cause the user to request a page to fire the login headers off. And ultimately, that’s way too little control for someone who is comfortable with PHP…and that’s definitely you by now.

Figure 12-1. The biggest issue with this login page really isn’t its awful look and feel. It’s that you don’t have as much control as you’ll ultimately want. You can’t provide a customized message if the user fails login. You have to cause the user to request a page to fire the login headers off. And ultimately, that’s way too little control for someone who is comfortable with PHP…and that’s definitely you by now.

Now, keep in mind: other than signing up initially or seeing a generic home page, this is the doorway to much of your application. So any work you do with a top-tier designer, any nice CSS and color scheming, or any clever HTML5 and SVG is all lost because it’s hidden behind that annoying gray pop-up. Even worse, when the user doesn’t get in, they keep getting that annoying pop-up.

But changing that takes more than changing one thing. It’s going to take a complete reworking of how users access your site.

Starting with a Landing Page

Any site that requires a login has to give you somewhere to land before you hit the login page. So to build out your site, you need something simple and effective as a central location for your users to begin. From this starting point they should be able to login, or create a new login.

Here’s a simple version of just that. Call it index.html so it can eventually be your site’s default landing page:

<html>

 <head>

  <link href="../css/phpMM.css" rel="stylesheet" type="text/css" />

 </head>

 <body>

  <div id="header"><h1>PHP & MySQL: The Missing Manual</h1></div>

  <div id="content">

    <div id="home_banner"></div>

    <div id="signup">

      <a href="create_user.html"><img src="../images/sign_me_up.png" /></a>

      <a href="signin.html"><img src="../images/sign_me_in.png" /></a>

    </div>

  </div>

  <div id="footer"></div>

 </body>

</html>

You can see what the page looks like in Figure 12-2.

Signing users up is pretty easy: just point them over to create_user.html and let the work you’ve already done take effect. But that link to signin.html creates a new set of questions to answer. First and foremost: what exactly needs to happen there to sign a user in?

Taking Control of User Sign-ins

Obviously, there needs to be a form into which a user can enter information. And the way things have been going, that form should submit to a script, which checks the username and password. Already, that’s different from what you’ve got: currently, authentication happens as a sort of side effect of requesting a page that requires authorize.php. So there’s not an explicit login form—but now there needs to be.

No, you probably shouldn’t submit this site for any Web 2.0 design awards. Still, it gets the basic point across: you want users to either sign up or log in. A sign-up page, you’ve got…but logging in? That’s going to require a new page or two, some PHP, and the death of that HTTP authentication login box.

Figure 12-2. No, you probably shouldn’t submit this site for any Web 2.0 design awards. Still, it gets the basic point across: you want users to either sign up or log in. A sign-up page, you’ve got…but logging in? That’s going to require a new page or two, some PHP, and the death of that HTTP authentication login box.

Then, this script that receives information from the form login has to check the user’s credentials. That’s easy; authorize.php already does that, and even though it currently uses $_SERVER, that’s easy to change to accept input from a sign-in form. But now here’s another wrinkle: if the credentials aren’t good, then the sign-in form needs to be shown again…preferably with the user’s original input for username, or at a minimum, a message stating that there was an error logging in.

NOTE

There is nothing as frustrating as a login form that sits staring blank-faced at you, never telling you that it’s received your credentials and they were rejected. User feedback is critical in any good login system.

So here’s the basic flow:

1.    Sign-in form (HTML): Takes in the user’s username and password. Submits them to…

2.    Authentication script (PHP): Verify the user’s username and password against the database. If there’s a match take them to a secure page, like the user’s profile (show_user.php), and let him know he’s logged in. If his credentials are not valid, take him back to…

3.    Sign-in form (HTML)

And here’s a problem: how can an HTML form display an error message on a particular condition? Or pre-fill out a username field?

Having that sign-in form as HTML really limits you, not on its initial display, but in the situation where there’s a login failure. It’s then that you want PHP on your side.

The obvious solution is to convert the sign-in page to PHP, and you’d end up with a flow like this:

1.    Sign-in form (PHP this time): Takes in the user’s username and password. Submits to…

2.    Authentication script (PHP): Verify the user’s username and password against the database. If there’s a match take them to a secure page, like the user’s profile (show_user.php), and let them know they’re logged in. If their credentials are not valid, take them back to…

3.    Sign-in form (PHP): Now this form can display a customized error and reload the user’s username.

But why not take it even further? There are all just steps in a login process. So what if instead of two scripts, you had a single script that submitted to itself, and either redirects the user on successful login, or re-shows itself on an unsuccessful login? (If the idea of a script submitting to itself sounds like something you’d see in Inception, check out the box on PHP Loves to Self-Reference.)

By the way, you’ll need to make a quick change to your site’s new home page before you forget. Since you’re using a script not just for processing logins, but creating the login page itself, do that now before you’re neck deep into PHP:

<html>

 <head>

  <link href="../css/phpMM.css" rel="stylesheet" type="text/css" />

 </head>

 <body>

  <div id="header"><h1>PHP & MySQL: The Missing Manual</h1></div>

  <div id="content">

    <div id="home_banner"></div>

    <div id="signup">

      <a href="create_user.html"><img src="../images/sign_me_up.png" /></a>

      <a href="signin.php"><img src="../images/sign_me_in.png" /></a>

    </div>

  </div>

  <div id="footer"></div>

 </body>

</html>

POWER USERS’ CLINIC: PHP LOVES TO SELF-REFERENCE

Up until now, you’ve had a pretty strong distinction between forms—created in HTML files—and the scripts to which they submit—PHP. But you’ve pretty much torn down that distinction in view pages. You’ve got lots of scripts that do some programmatic “stuff”—logging a user in, or getting all the current users, or looking up a particular user—and then outputting a bunch of HTML.

So then why not blow that distinction away in forms, too?

A script could output a form, and set the action of that form as…itself. Then, when the form is recalled, it would see if there’s any submitted data. If so, it’s receiving a form submission and can do what it needs to programmatically. There’s no real magic here; just an if that directs traffic. Inside that if, you could even output a completely different page, perhaps letting the user know their data has been accepted.

And if there’s no submission data, then it’s just a normal initial request for the form, and so the form should be shown. But you get some nice benefits here, too. You can see if there might be error messages or existing data from a previous submission, and drop those values right into your form.

Using submitted data is extremely common in PHP, and something with which you want to get comfortable. Even though it’s a bit of a mind trip the first few times, you’ll soon find that in a PHP-driven application, there are very few times where you’re not going to use a PHP script. Forms, error pages, login pages, even welcome pages…you’ll get hooked on having the ability to use PHP, and be hard-pressed to go back.

Now, there are a few of you—those of you who are partial to the MVC (Model-View-Controller) pattern—who are dying inside. HTML inside a script that submits to itself means you’ve completely eradicated a wall (or even a large overgrown hedge) between the model, the view, and the controller. But as you’ve already seen, you’re not going to get a true MVC pattern working well in PHP. You can get an approximation, and don’t shy away from that approximation. You’re just not going to get the clean separation that’s possible in languages like Ruby or Java (and you can still make just as big a mess in those languages, in case you were wondering).

Given that, you may want to simply accept that PHP is often going to cause you to sacrifice clean MVC on the altar of getting things done. Always, always, always…get things done.

From HTTP Authentication to Cookies

Before you can dive into writing this sign-in script—call it signin.php—there’s another glaring issue to work out. How do you let the user log in? By abandoning that popup login form, you’re taking logging in into your own hands.

Getting the username and password and checking them against the database isn’t a big deal. You can and will do that in signin.php. But the big problem is keeping that information around. With HTTP authentication, the browser kept up with all your pages being in one realm, and whether or not the user was logged into that realm. So logging in and accessing show_users.php meant that a user did not have to log in to get to delete_user.php. They’d already done that for another page in the same realm.

And that’s where cookies come into play.

NOTE

At this point, about a thousand obligatory jokes related to baking and sweets and a million other things come into focus. It’s a strange term, one that refers back to something called magic cookies. That was a term old-school Unix hackers used for little bits of data passed back and forth between programs.

In any case, it stuck, so if you’re new to cookies in the programming world, feel free to snicker as you code the rest of this chapter.

What is a Cookie?

A cookie is nothing magical. It’s simply a means by which you can store a single piece of information. So a cookie has a name, a value—that single piece of information—and a date at which the cookie expires. When the cookie expires, it’s gone; you can’t get the value anymore.

So you could have a cookie with a name “username” and a value “my_username”, and perhaps another cookie named “user_id” with a value of “52”. Then, your scripts can check to see if there’s a “username” cookie, and if so, assume the user’s logged in. In the same manner, your login script can set a “username” cookie.

In other words, other than setting the cookie in the first place, you get the same sort of effect as you were getting with basic authentication. Of course, the creation of cookies is within your control, so you can create them with your own form, delete them with your scripts (say, on a user logout), and issue messages based on the status of cookies.

WARNING

Although you can control the creation of cookies, your users can easily modify them, delete them, and even create cookies of their own. Because of that, they’re not ideal for the sort of information you’re storing in them here: secure usernames and the like.

That’s why there’s a Chapter 13—and have no fear, even though you’ll change the manner in which you use cookies, everything you’re learning here will be important in your final authentication solution. Besides, there are plenty of times when cookies are helpful, and they’ll be a staple of your programming toolkit.

Create and Retrieve Cookies

You’re almost ready to jump into scripting again—and that’s where all the fun is. (It’s certainly not as fun reading about code as it is writing code.) All that’s left is to figure out to write cookies and then look them up and get their values. Thankfully, PHP makes working with cookies as simple as working with the superglobals you’ve already gotten used to: $_SERVER and $_REQUEST.

To set a cookie, you simply call setcookie with the cookie’s name and value:

setcookie("username", "my_username");

Once a cookie’s set, you get the value you just set with the $_COOKIE superglobal:

echo "You are signed in as " . $_COOKIE['username'] . ".";

That’s it. It’s that simple. Sure, there are some wrinkles here and there, and you’ll add a bit of nuance to your cookie creation, but if you’ve got setcookie and $_COOKIE down, you’re more or less ready to roll.

NOTE

One of those nuances you may already be thinking about is the expiration value of a cookie. You can pass that as a third value to setcookie, but don’t worry, there’s more to come on expiration soon.

Logging In with Cookies

You know what cookies are. You know the flow of the sign-in form. Now it’s time to code. Create signin.php and start with the basic outline:

<?php

require_once '../scripts/database_connection.php';

require_once '../scripts/view.php';

// If the user is logged in, the user_id cookie will be set

if (!isset($_COOKIE['user_id'])) {

  // See if a login form was submitted with a username for login

  if (isset($_REQUEST['username'])) {

    // Try and log the user in

    $username = mysql_real_escape_string(trim($_REQUEST['username']));

    $password = mysql_real_escape_string(trim($_REQUEST['password']));

    // Look up the user

    // If user not found, issue an error

  }

  // Still in the "not signed in" part of the if

  // Start the page, and we know there's no success or error message

  //   since they're just logging in

  page_start("Sign In");

?>

<html>

  <div id="content">

    <h1>Sign In to the Club</h1>

    <form id="signin_form" action="signin.php" method="POST">

      <fieldset>

        <label for="username">Username:</label>

        <input type="text"  name="username" id="username" size="20" />

        <br />

        <label for="password">Password:</label>

        <input type="password"  name="password" id="password" size="20" />

      </fieldset>

      <br />

      <fieldset class="center">

        <input type="submit" value="Sign In" />

      </fieldset>

    </form>

  </div>

  <div id="footer"></div>

 </body>

</html>

<?php

} else {

  // Now handle the case where they're logged in

  // redirect to another page, most likely show_user.php

}

?>

NOTE

Did you notice that database_connection.php is required—for logging the user in—but app_config.php isn’t? You can include app_config.php, as there’s a good change you’ll need it at some point, but you may also remember that database_connection.php actually requires app_config.php itself. So if you require data-base_connection.php, you get a require_once for app_config.php for free.

This script is far from complete, has several problems, but is still a lot of code. Take it piece by piece.

Is the User is Already Signed In?

Even if a user comes to your sign-in page explicitly, you shouldn’t make them sign in. So the first thing to do (other than a few require_once lines) is see if the “user_id” cookie is set. If it’s not, the user’s not logged in, and everything flows from that.

<?php

require_once '../scripts/database_connection.php';

require_once '../scripts/view.php';

// If the user is logged in, the user_id cookie will be set

if (!isset($_COOKIE['user_id'])) {

Here’s your first clue that cookies aren’t much different than what you’ve already been using: you can use isset to see if it’s already created, and you just pass in the cookie name. Piece of cake.

Is the User Trying to Sign In?

If the “user_id” cookie isn’t set, the user’s not logged in. So now you need to see if she’s trying to log in. That would mean that you’ve got some request information. The user might have actually already filled out the HTML form (later in this script) and submitted that form back to this script.

But that’s not the same as trying to access this script without any information. In that case, the user should just get the regular HTML sign-in form. So you can see if there’s a submission by checking to see if there’s anything in the $_REQUEST superglobal for “username”, a field from the sign-in form:

// See if a login form was submitted with a username for login

if (isset($_REQUEST['username'])) {

  // Try and log the user in

  $username = mysql_real_escape_string(trim($_REQUEST['username']));

  $password = mysql_real_escape_string(trim($_REQUEST['password']));

  // Look up the user

  // If user not found, issue an error

}

If there’s request data, then you can get the username and password that have been submitted, and (in a moment) look up the user and deal with any problems.

Before you do that, though, here’s a nice change you can make. So far, you’ve been using $_REQUEST for everything. That takes in GET requests—which are requests where information is passed through the URL—and POST requests, like the ones that most of your forms have issued. But you already know that the only way information should get to this stage is by a submission from your own form, which will use a POST request.

You can replace $_REQUEST with a more specific superglobal, then: $_POST. $_POST only has request data from a POST request.

NOTE

As you’ve probably already guessed, $_POST has a counterpart for GET requests: $_GET.

It’s a good idea to begin moving toward the more specific $_POST when possible. POST data disallows parameters on the request URL, and is generally a bit more secure.

WARNING

The emphasis here is on “bit.” POST data is a little harder to get at than GET data, but not by much. Just don’t think that a form that POSTs data is secure in and of itself. That’s by no means the case.

So make that small change to your script:

// See if a login form was submitted with a username for login

if (isset($_POST['username'])) {

  // Try and log the user in

  $username = mysql_real_escape_string(trim($_REQUEST['username']));

  $password = mysql_real_escape_string(trim($_REQUEST['password']));

  // Look up the user

  // If user not found, issue an error

}

FREQUENTLY ASKED QUESTION: $_REQUEST OR $_POST: WHAT’S THE BIG DEAL?

Ahh, yes, another quibble over which programmers can argue, demonize, and distort. No matter what you hear, there’s just no functional difference between $_REQUEST, $_GET, and $_POST, in terms of getting request information. $_REQUEST will always have what’s in both $_GET and $_POST, but if you know you’ve got a POST request, you don’t gain or lose anything by using$_REQUEST over $_POST.

In fact, not only does $_REQUEST have the combined values from $_GET and $_POST, it has the contents of $_COOKIE in it too (at least by default). So technically, you could do this in signin.php:

// If the user is logged in, the user_id

cookie will be set

if (!isset($_REQUEST['user_id'])) {

So you could use $_REQUEST and totally ditch $_GET, $_REQUEST, and $_COOKIE. But think back to all the programming principles you’ve been getting a hold of: make your code clear and readable, be specific over being just generic, and think about what those who have to work with your code after you will see. For all those reasons, although $_REQUEST isn’t bad, it’s often helpful to use $_GET and $_POST and $_COOKIE when that’s what you’re dealing with.

In the case of signin.php, you know you’re getting a POST request. Given that, use $_POST when you can. If you know you’re getting a GET request, use $_GET. And if you’re looking for a cookie, use $_COOKIE. Your code will be clearer and more specific…and you’ll know exactly what it’s intended to do.

Displaying the page

Whether the user got to this page by submitting incorrect credentials, or by not submitting credentials at all, he should see a form. So now you’re ready to display some HTML.

NOTE

If the user logs in successfully, your code will need to redirect them elsewhere. So that code block that checks usernames and passwords needs to eventually forward the user on to another location if his login is successful.

  // Still in the "not signed in" part of the if

  // Start the page, and we know there's no success or error message

  //   since they're just logging in

  page_start("Sign In");

?>

<html>

  <div id="content">

    <h1>Sign In to the Club</h1>

    <form id="signin_form" action="signin.php" method="POST">

      <fieldset>

        <label for="username">Username:</label>

        <input type="text"  name="username" id="username" size="20" />

        <br />

        <label for="password">Password:</label>

        <input type="password"  name="password" id="password" size="20" />

      </fieldset>

      <br />

      <fieldset class="center">

        <input type="submit" value="Sign In" />

      </fieldset>

    </form>

  </div>

  <div id="footer"></div>

 </body>

</html>

Don’t miss that opening comment block; it’s an important one. This code—including the HTML—is all still part of the opening if block:

// If the user is logged in, the user_id cookie will be set

if (!isset($_COOKIE['user_id'])) {

In other words, all this HTML is shown if and only if the user’s not logged in.

There’s another small improvement you can make here, in the same vein as using $_POST instead of $_REQUEST. Take a look at this line:

<form id="signin_form" action="signin.php" method="POST">

This tells the form to submit to the same script that’s generating the form. Nothing wrong with it, but what if you rename signin.php? It might be a small possibility, but all the same, it’s not unrealistic. (It wasn’t that long ago that you moved away from calling a script admin.php and instead went with the more functionally named delete_user.php and show_users.php.)

Remember that PHP loves this script-submitting-to-script paradigm. In fact, just to make it a bit easier, you’ve got a property in $_SERVER that tells you the current script name. No, it’s not there just for self-referential scripts, but it sure does help. Update signin.php to take advantage of$_SERVER[‘PHP_SELF’]:

<form id="signin_form"

      action="<?php echo $_SERVER['PHP_SELF']; ?>"

      method="POST">

Now the form submits, literally, to itself. A small change, but a good one… and one you’ll find yourself coming back to over and over again.

Redirecting as Needed

The only thing left—at least in this pseudocode version—is to redirect the user if she’s logged in:

<?php

} else {

  // Now handle the case where they're logged in

  // redirect to another page, most likely show_user.php

}

?>

So now you’ve got the basic flow. But there’s a ton missing. Time to dig in and start piecing this code into a usable form.

POWER USERS’ CLINIC: PSEUDOCODE WITH COMMENTS AND REAL CODE

It might seem strange to think of signin.php as it currently exists as pseudocode, but that’s just what it is. It’s certainly not a complete working script; there are holes through which you could drive a truck all over the place. But those holes are generally indicated with a helpful, clear comment. And although those comments don’t do anything, they do remind you of what needs to be done, andwhere it needs to be done.

Truth be told, pseudocode is often best done just like this. You’re not truly wasting time writing nonexistent function names like check_the_user_credentials(). But you’re accomplishing the same goal with comments like:

// Look up the user

// If user not found, issue an error

Those comments are just as useful, and they can stay put as you write code under each comment that fills out the script’s functionality.

Before you begin, though, you can already get a good idea of this flow. Right now, a non-logged in user will get the HTML output, without all the PHP that runs when there’s a username coming in through a POST request. So Figure 12-3 is the default view, so to speak.

Try and submit the form—with a good or bad username—and you get the same form over again. Not so great…but still, this is a starting point. Now you can begin to tackle each individual piece of functionality.

It might seem a bit odd, but the same PHP script that checks login credentials now grabs them from your users. This is just a simple HTML form, since there’s no user_id cookie and no username in the POST data.

Figure 12-3. It might seem a bit odd, but the same PHP script that checks login credentials now grabs them from your users. This is just a simple HTML form, since there’s no user_id cookie and no username in the POST data.

Logging the User In

The next bit of code is really a copy-paste-and-modify job from authorize.php. Here’s where that script was left:

// Look up the user-provided credentials

$query = sprintf("SELECT user_id, username FROM users " .

                 " WHERE username = '%s' AND " .

                 "       password = '%s';",

            mysql_real_escape_string(trim($_SERVER['PHP_AUTH_USER'])),

            mysql_real_escape_string(

              crypt(trim($_SERVER['PHP_AUTH_PW']),

                    $_SERVER['PHP_AUTH_USER'])));

$results = mysql_query($query);

if (mysql_num_rows($results) == 1) {

  $result = mysql_fetch_array($results);

  $current_user_id = $result['user_id'];

  $current_username = $result['username'];

} else {

  header('HTTP/1.1 401 Unauthorized');

  header('WWW-Authenticate: Basic realm="The Social Site"');

  exit("You need a valid username and password to be here. " .

       "Move along, nothing to see.");

}

Pretty good, although it all depends on HTTP authentication. Now you can drop that into signin.php, change the successful block to set some cookies, and redirect somewhere useful:

<?php

require_once '../scripts/database_connection.php';

require_once '../scripts/view.php';

// If the user is logged in, the user_id cookie will be set

if (!isset($_COOKIE['user_id'])) {

  // See if a login form was submitted with a username for login

  if (isset($_POST['username'])) {

    // Try and log the user in

    $username = mysql_real_escape_string(trim($_REQUEST['username']));

    $password = mysql_real_escape_string(trim($_REQUEST['password']));

    // Look up the user

    $query = sprintf("SELECT user_id, username FROM users " .

                     " WHERE username = '%s' AND " .

                     "       password = '%s';",

                     $username, crypt($password, $username));

    $results = mysql_query($query);

    if (mysql_num_rows($results) == 1) {

      $result = mysql_fetch_array($results);

      $user_id = $result['user_id'];

      setcookie('user_id', $user_id);

      setcookie('username', $result['username']);

      header("Location: show_user.php");

    } else {

      // If user not found, issue an error

    }

  }

  // Still in the "not signed in" part of the if

  // Start the page, and we know there's no success or error message

  //   since they're just logging in

  page_start("Sign In");

?>

Open up signin.php, and you should get the login form (check out Figure 12-3 to make sure you’re on the right page with the right HTML). Login with some valid credentials, and you should get logged in, have a cookie set, and be passed over to show_user.php (see Figure 12-4).

Once again, victory is signified by getting to a page you’ve long since completed. Here, it’s getting to show_user.php with the user that just logged in. But there’s no browser magic here (not in the authentication bit, anyway), and no HTTP authentication. Just good old-fashioned PHP, MySQL, and some help from cookies.

Figure 12-4. Once again, victory is signified by getting to a page you’ve long since completed. Here, it’s getting to show_user.php with the user that just logged in. But there’s no browser magic here (not in the authentication bit, anyway), and no HTTP authentication. Just good old-fashioned PHP, MySQL, and some help from cookies.

Did you notice anything odd in that last bit of redirection, though? Here’s the line where the redirect is sent to the browser:

if (mysql_num_rows($results) == 1) {

  $result = mysql_fetch_array($results);

  $user_id = $result['user_id'];

  setcookie('user_id', $user_id);

  setcookie('username', $result['username']);

  header("Location: show_user.php");

} else {

  // If user not found, issue an error

}

If no bells are ringing, check out create_user.php (Rounding Things Out with Regular Expressions (Again)). That script creates a user and redirects him to show_user.php. Here’s the relevant line:

header("Location: show_user.php?user_id=" . mysql_insert_id());

So here, additional information is sent: the user_id of the user to display, sent as a GET parameter within the request URL. But in signin.php, there’s no user_id parameter. All the same, Figure 12-4 shows that things work.

show_user.php expects that information:

// Get the user ID of the user to show

$user_id = $_REQUEST['user_id'];

And this isn’t the only place show_user.php requests user information. At the beginning of signin.php, there’s this opening if statement.

// If the user is logged in, the user_id cookie will be set

if (!isset($_COOKIE['user_id'])) {

The else to this if is way down near the bottom of the script:

} else {

  // Now handle the case where they're logged in

  // redirect to another page, most likely show_user.php

  header("Location: show_user.php");

}

So here’s another case where the user is sent to show_user.php, and there’s no user_id parameter, but all the same, show_user.php continues to happily figure out which user should be shown, and display that user (see Figure 12-4).

So how does this work in signin.php? The answer lies in how $_REQUEST works, and what information it contains. For starters, flip to the box on $_REQUEST or $_POST: What’s the Big Deal? and then come back. You are setting a cookie in signin.php, and that cookie is accessible through the $_COOKIE superglobal. But $_REQUEST also contains what’s in $_COOKIE—along with what’s in $_POST and $_GET. So this…

$user_id = $_REQUEST['user_id'];

…is just as good as this…

$user_id = $_COOKIE['user_id'];

…at getting the value in a cookie.

NOTE

The obvious question is, “Which should you use? $_COOKIE or $_REQUEST. As usual, it depends. Here, if you switch to $_COOKIE, you’ll need to update create_user.php. It might be best to leave this as $_REQUEST, at least for now, as it makes show_user.php a little more flexible. It accepts request parameters and cookies, and that’s a nice thing. Later, if you move to using only cookies, you can update show_user.php to use $_COOKIE and be more specific.

Blank Pages and Expiring Cookies

At some point in here, as you’re trying things out, you may get a very strange response. You enter in signin.php in your URL bar. You hit Enter. And you get a blank page, as in Figure 12-5.

Nobody said that testing authentication wasn’t a hassle. This blank screen means that your logging in and cookie setting is working…so why are you getting a blank screen? Any ideas?

Figure 12-5. Nobody said that testing authentication wasn’t a hassle. This blank screen means that your logging in and cookie setting is working…so why are you getting a blank screen? Any ideas?

You try it again. You try to reload. You try to clear your cache. Nothing! Finally, you restart your browser, and things start to behave. But no sooner have you signed in through signin.php before it’s happening again. So what’s up?

Actually, this is a sign that things are working correctly. Remember that in your script, the first conditional checks for a cookie:

// If the user is logged in, the user_id cookie will be set

if (!isset($_COOKIE['user_id'])) {

If this cookie is set, then the script jumps all the way down to this bit at the bottom of your file:

<?php

} else {

  // Now handle the case where they're logged in

  // redirect to another page, most likely

}

?>

But there’s nothing there! So you get a blank browser. You can fix this (kind of) by setting up a default action for users that get sent to signin.php and yet are already logged in. In fact, it’s the same thing you did earlier for a login: redirect them to show_user.php:

} else {

  // Now handle the case where they're logged in

  // redirect to another page, most likely

  header("Location: show_user.php");

}

Now, no more blank screen. show_user.php will pick up on the “user_id” cookie and show the currently logged in user. Good, right?

Sort of. It still leaves you in an endless loop. It’s just that now you’re looping on the nice looking show_user.php rather than a crummy-looking blank page. You’ll need to completely close out your browser to stop the madness.

But that’s normal behavior. Just as when you logged in via HTTP authentication, logging in here and setting a cookie, by default, sets that cookie to exist until the browser is closed.

NOTE

Calling setcookie with no value for the third parameter defaults it to “0”, meaning the cookie expires at the end of the user’s session, which is when the user closes their browser.

So if you need to get out of this loop, just close your browser out. Make sure you close the program, not just the current tab or window. That will cause the cookie—set with a default expiration value—to expire.

If you really want to set the cookie to last longer (or shorter), you can. Just pass in a third parameter to setcookie. That third parameter should be in the seconds from what Unix and Linux systems call the epoch, January 1, 1970, at 0:00 GMT. So you usually pass in time, which gives the current time—conveniently also in seconds since the epoch—and then add to that. So time() + 10 would be 10 seconds in the future, as reckoned from the epoch.

Here are just a few examples of setcookie with an expiration time passed in:

// Expire in an hour (60 seconds * 60 minutes = 3600)

setcookie('user_id', $user_id, time() + 3600);

// This actually deletes the cookie, since it indicates an

//  expiration in the past

setcookie('user_id', $user_id, time() - 3600);

// The default: expire on browser close

setcookie('user_id', $user_id, 0);

You can also supply a time via mktime, which takes an hour, date, second, month, day, and year, and returns the number of seconds since the epoch (again). So…

setcookie('user_id', $user_id, mktime(0, 0, 0, 2, 1, 2021);

…would set a cookie to expire on February 1, 2021, GMT. That’s a little far away, wouldn’t you say? In general, the default value is perfectly reasonable. Most users are comfortable signing in again when their browser closes. In fact, many users would not be comfortable with their login lasting on and on, perpetually.

NOTE

The notable exceptions here are sites like Facebook and Twitter that don’t contain a lot of valuable user information. Most financial sites like banks don’t even wait for your browser to close; they’ll expire your session every 10 minutes or so.

So close your browser, which will expire your cookies, and open up signin.php again for some more improvement.

Errors Aren’t Always Interruptive

Now you’ve got a potential error with which you’ve got to deal. There’s the else that is run when the user’s username and password don’t match what’s in the database:

if (mysql_num_rows($results) == 1) {

  // set a cookie and redirect to show_user.php

} else {

  // If user not found, issue an error

}

Now, the typical error handling so far has been handle_error. But that’s no good; you don’t want to short-circuit the login process by throwing the user out to an error page. They’d have to get back to the login page, try again and potentially go to the error page yet again. That’s no good at all.

What you need is a means by which you can show any errors without interrupting the overall application flow. When something goes badly wrong, handle_error makes perfect sense; a major error deserves to interrupt your application. But here, you need a non-interruptive way to show errors, other than handle_error.

But you do have another way to show errors: the page_start function in view.php. Right now, you’re calling this function in signin.php, but without anything other than the page title:

page_start("Sign In");

But look back in view.php; here’s the complete set of arguments this method takes:

function page_start($title, $javascript = NULL,

                    $success_message = NULL, $error_message = NULL) {

Normally, you’ve been passing in any request parameters as the values for $success_message and $error_message. But that’s not a requirement. So you can create a new variable called $error_message, fill it with text as your script progresses, and then hand it off to page_start as the HTML output is begun.

Here’s what you should do:

<?php

require_once '../scripts/database_connection.php';

require_once '../scripts/view.php';

$error_message = "";

// If the user is logged in, the user_id cookie will be set

if (!isset($_COOKIE['user_id'])) {

  // See if a login form was submitted with a username for login

  if (isset($_POST['username'])) {

    // Try and log the user in

    // Look up the user

    if (mysql_num_rows($results) == 1) {

      $result = mysql_fetch_array($results);

      $user_id = $result['user_id'];

      setcookie('user_id', $user_id);

      setcookie('username', $result['username']);

      header("Location: show_user.php");

    } else {

      // If user not found, issue an error

      $error_message = "Your username/password combination was invalid.";

    }

  }

  // Still in the "not signed in" part of the if

  // Start the page, and pass along any error message set earlier

  page_start("Sign In", NULL, NULL, $error_message);

?>

<!-- Rest of HTML output -->

<?php

} else {

  // Now handle the case where they're logged in

  // redirect to another page, most likely show_user.php

  header("Location: show_user.php");

}

?>

WARNING

Remember, this cookie-based solution is a step toward a final solution, not the final solution itself. In the next chapter, you’ll add support for sessions, and move information like a username and user ID out of a user’s cookie and onto the server.

Whatever you do, keep reading! You’ll need the cookie skills you’re learning here, but you’ll add to those skills sessions in the next chapter. Woe to the PHP programmer who uses cookies—and only cookies—for authentication.

Now visit signin.php (or index.html and click the Sign Up button). Uh oh…Figure 12-6 reveals there’s a problem somewhere.

That’s a strange sight: an error-less error. This is an issue, sure…but this is probably the second screen all your users will ever see, so it’s a big issue. Still, by now, you’re probably already thinking about what the problem is, and how you’ll fix it pretty quickly.

Figure 12-6. That’s a strange sight: an error-less error. This is an issue, sure…but this is probably the second screen all your users will ever see, so it’s a big issue. Still, by now, you’re probably already thinking about what the problem is, and how you’ll fix it pretty quickly.

Problems pop up when you’re writing applications all the time. You write a function ages ago—the code in view.php that shows an error—and then use it in a different way much later. That’s when the bugs appear.

In this case, the problem is that you’re calling page_start with $error_message, but in some cases, $error_message is blank. It’s an empty string, “”, and so nothing should be shown. Check out view.php, and find display_message:

function display_messages($success_msg = NULL, $error_msg = NULL) {

  echo "<div id='messages'>\n";

  if (!is_null($success_msg)) {

    display_message($success_msg, SUCCESS_MESSAGE);

  }

  if (!is_null($error_msg)) {

    display_message($error_msg, ERROR_MESSAGE);

  }

  echo "</div>\n\n";

}

In this case, $error_message isn’t null. It’s an empty string. So the if lets it pass, and a blank error message is shown in a red box. Not so good.

The fix is trivial, though; just see if $error_message is not null, and that it’s got a length greater than 0. While you’re at it, make the same improvement to the handling of success messages:

function display_messages($success_msg = NULL, $error_msg = NULL) {

  echo "<div id='messages'>\n";

  if (!is_null($success_msg) && (strlen($error_msg) > 0)) {

    display_message($success_msg, SUCCESS_MESSAGE);

  }

  if (!is_null($error_msg) && (strlen($error_msg) > 0)) {

    display_message($error_msg, ERROR_MESSAGE);

  }

  echo "</div>\n\n";

}

Now you should see a proper sign-in form (Figure 12-7).

It makes no sense to present a user with an error the first time they see the sign in form. But after they’ve tried an incorrect username or password… that’s when you want to let them know there’s a problem.

Figure 12-7. It makes no sense to present a user with an error the first time they see the sign in form. But after they’ve tried an incorrect username or password… that’s when you want to let them know there’s a problem.

Try and enter in an incorrect username or password, and you should see a nice, clear error—without pulling you our of the login process. Figure 12-8 shows this message, and users can immediately re-enter their information.

Now this is a solid non-interruptive error. It’s impossible to miss, it creates a change that lets the user know something needs her attention, but it’s not over the top. Now the user can try again… and again…and again.

Figure 12-8. Now this is a solid non-interruptive error. It’s impossible to miss, it creates a change that lets the user know something needs her attention, but it’s not over the top. Now the user can try again… and again…and again.

An Option for Repeat Attempts

At this point, your sign-in page is functionally complete. However, there’s one more option you can provide your users: reloading their username on login failure. Some sites do this reloading, and some don’t. It’s a matter of opinion, but as with most things, even if you choose not to implement this feature, you should know how to implement it.

If you need to display a username, that means the user’s already submitted the form at least once before. So that places you squarely in this portion of signin.php:

if (isset($_POST['username'])) {

    // Try and log the user in

    $username = mysql_real_escape_string(trim($_REQUEST['username']));

    $password = mysql_real_escape_string(trim($_REQUEST['password']));

    // and so on...

}

The username’s been sent, but logging in failed. So you’ve got the $username variable ready to display.

Now, move into the HTML. You can set the value of a form field with the value attribute, and you’ve got the attribute value in $username. So put that together, and you’ll end up with something like this:

<label for="username">Username:</label>

<input type="text"  name="username" id="username" size="20"

       value="<?php if (isset($username)) echo $username; ?>" />

That’s all there is to it. Enter in a username, submit the sign-in page, and you should see the sign in page with an error—and the username previously entered. Check out Figure 12-9 for the details.

You’ll have to decide for yourself if you want to reshow previously-entered usernames. On the one hand, it’s a pretty nice feature. On the other hand, though, the user-name may be part of the problem. So by reshowing it, you may be implying that the username is correct when it’s not.

Figure 12-9. You’ll have to decide for yourself if you want to reshow previously-entered usernames. On the one hand, it’s a pretty nice feature. On the other hand, though, the user-name may be part of the problem. So by reshowing it, you may be implying that the username is correct when it’s not.

Adding Context-Specific Menus

Now, truly, menus and navigation deserve a lot more than a brief mention in a chapter. There’s a ton of user interface design and usability to discuss; design principles; and the ever-raging argument over horizontal versus vertical menus. Still, these are all non-PHP issues. For you, PHP programmer extraordinaire, the concern is building out links and options that change based upon whether a user is logged in.

But you already have a sense of seeing whether a user is logged in. You can just check the “user_id:” cookie:

if (isset($_COOKIE['user_id'])) {

  // show options for logged in users

} else {

  // show options for non-logged in users

}

That’s all there is to it.

Putting a Menu Into Place

So back to view.php, which is where all the code that controls the header of your page lives anyway. Having some of your core view code tucked away in scripts that the rest of your pages can reference makes a huge difference in situations like this. The display_title function handles the first bits of a displayable page right now.

Find that function, and you can add a pretty simple if: if the “user_id” exists, show a profile link to show_user.php and a signout.php link (more on that in a bit). If they’re not signed in, show them a Sign In link. And of course, you can add a Home link that shows regardless:

function display_title($title, $success_message = NULL, $error_message = NULL)

{

echo <<<EOD

 <body>

  <div id="header"><h1>PHP & MySQL: The Missing Manual</h1></div>

  <div id="example">$title</div>

  <div id="menu">

    <ul>

      <li><a href="index.html">Home</a></li>

EOD;

  if (isset($_COOKIE['user_id'])) {

    echo "<li><a href='show_user.php'>My Profile</a>";

    echo "<li><a href='signout.php'>Sign Out</a></li>";

  } else {

    echo "<li><a href='signin.php'>Sign In</a></li>";

  }

echo <<<;EOD

    </ul>

  </div>

EOD;

  display_messages($success_message, $error_message);

}

This code has two advantages. First, this menu is now available to all scripts through view.php. So you don’t need to go rooting through all your files and insert new HTML and if statements to get a site-wide menu. And second, since you dropped this code into display_title, any of your scripts that already call display_title automatically get the menu code. Nothing to change in those at all.

And did you notice that, once again, the fact that $_REQUEST will return anything in $_COOKIE makes this script simple:

if (isset($_COOKIE['user_id'])) {

  echo "<li><a href='show_user.php'>My Profile</a>";

  echo "<li><a href='signout.php'>Sign Out</a></li>";

} else {

  echo "<li><a href='signin.php'>Sign In</a></li>";

}

You’re not worried about passing a user’s ID into show_user.php, because there’s a cookie set, and you’ve already seen that show_user.php is happy to grab that value through $_REQUEST[‘user_id’]—just as if you’d explicitly passed in a user ID through a request parameter.

FREQUENTLY ASKED QUESTION: DOES ANYONE ACTUALLY SIGN OUT THESE DAYS?

It’s true: unless people are on a financial site—their bank or perhaps a stock trading site—logging and signing out is largely never touched. Most Internet users are not very security conscious, and there’s also just an expectation that a website will remember them when they return later. Signing out would prevent that, so why do it?

All the same, there are good reasons to add Sign Out capabilities to any app. First, if users are accessing your app on a public computer or shared laptop, you want to make sure they can protect their credentials by signing out before letting anyone else use the computer. Second, just because most users aren’t very security conscious doesn’t mean that none are. Give someone the option to sign out, and if they don’t take it, no big deal. If they do, they’ll be glad your app supports the functionality they want.

And last but not least, you know how to create cookies. It would be a good thing to know how to delete them as well. So adding a sign out link forces you to get a handle on that, too.

To test this out, you should open up your various scripts that display HTML: show_user.php, show_users.php, and signin.php. Each should call page_start, rather than display HTML explicitly. Otherwise, you’ll lose the menu code you just added to page_start in view.php. Here’s what, for example, show_user.php should look like:

<?php

require '../scripts/database_connection.php';

require '../scripts/view.php';

// Lots of PHP to load the user ID from a request parameter or

//   a cookie, look up that user, and set some values.

page_start("User Profile");

?>

  <div id="content">

    <div class="user_profile">

      <h1><?php echo "{$first_name} {$last_name}"; ?></h1>

      <p><img src="<?php echo $user_image; ?>" class="user_pic" />

        <?php echo $bio; ?></p>

      <p class="contact_info">

        Get in touch with <?php echo $first_name; ?>:

      </p>

      <ul>

        <li>...by emailing him at

          <a href="<?php echo $email; ?>"><?php echo $email; ?></a></li>

        <li>...by

          <a href="<?php echo $facebook_url; ?>">checking him out on

             Facebook</a></li>

        <li>...by <a href="<?php echo $twitter_url; ?>">following him

             on Twitter</a></li>

      </ul>

    </div>

  </div>

  <div id="footer"></div>

 </body>

</html>

Now sign in and head over to show_user.php.

NOTE

Actually, just signing in should automatically take you to show_user.php. Even better!

You should see something like Figure 12-10. There’s a nice, simple menu on the right that now appears thanks to start_page, display_title, view.php, and the cookies you set in signin.php.

From HTML to Scripts

Something you might have noticed is that even once you’ve fixed up show_user.php, show_users.php, and signin.php, there are still pages left in your application. There’s index.html, the initial page, as well as create_user.html. But these pages obviously don’t get the benefit of start_page andview.php, because they’re HTML, not PHP. For index.html, that probably makes sense. The only two places you want users to go is the sign-in page, or the sign-up page; both are clearly accessible through those big green buttons.

This particular menu is pretty simple. Still, it’s easy to build this thing out now that you’ve got a basic mechanism for displaying it for authenticated users, and hiding it for others. You can add all the links and sublinks that your application needs, and as long as they’re in the portion of the if in display_title that requires a cookie, you’re good to go.

Figure 12-10. This particular menu is pretty simple. Still, it’s easy to build this thing out now that you’ve got a basic mechanism for displaying it for authenticated users, and hiding it for others. You can add all the links and sublinks that your application needs, and as long as they’re in the portion of the if in display_title that requires a cookie, you’re good to go.

But that’s not the case with create_user.html. Suppose someone clicks through to that form, and wants to return to the main page. Or, more likely, they may want to sign in, rather than sign up. That possibility becomes even more the case as you add other options to the menu, like an About page. So create_user.html needs that menu.

Any HTML File Can Be Converted to PHP

Converting create_user.html to be create_user.php is easy. But wait…create_user.php already exists. So, as a starting point, rename create_user.html to signup.php. That goes nicely with the wording on index.html, and it really is a form for signing up users.

[~/www/phpMM/ch12]# cp create_user.html create_user.html.orig

[~/www/phpMM/ch12]# mv create_user.html signup.php

NOTE

There’s never a bad time to back things up, create copies of original files, and do the work of ensuring you can reverse any change you make. That can be accomplished through a full-fledged site-wide backup strategy, or just a duplicate of a file with a clear backup-related name.

Then, you can simply cut out the opening HTML and replace it with a PHP-driven call to page_start. You’ll have to pass through all that inline validation JavaScript, but that’s easy now; you can just use heredoc.

<?php

require_once "../scripts/view.php";

$inline_javascript = <<<EOD

    $(document).ready(function() {

      $("#signup_form").validate({

        rules: {

          password: {

            minlength: 6

          },

          confirm_password: {

            minlength: 6,

            equalTo: "#password"

          }

        },

        messages: {

          password: {

            minlength: "Passwords must be at least 6 characters"

          },

          confirm_password: {

            minlength: "Passwords must be at least 6 characters",

            equalTo: "Your passwords do not match."

          }

        }

      });

    });

EOD;

page_start("User Signup", $inline_javascript);

?>

  <div id="content">

    <h1>Join the Missing Manual (Digital) Social Club</h1>

    <p>Please enter your online connections below:</p>

    <form id="signup_form" action="create_user.php"

          method="POST" enctype="multipart/form-data">

      <!-- Form content -->

    </form>

  </div>

  <div id="footer"></div>

 </body>

</html>

Now is also a good time to update view.php to include the jQuery and validation scripts and CSS that signin.php needs. No reason to not make those available to all your site’s pages:

function display_head($page_title = "", $embedded_javascript = NULL) {

echo <<<EOD

<html>

 <head>

  <title>{$page_title}</title>

  <link href="../css/phpMM.css" rel="stylesheet" type="text/css" />

  <link href="../css/jquery.validate.password.css" rel="stylesheet"

        type="text/css" />

  <script type="text/javascript" src="../js/jquery.js"></script>

  <script type="text/javascript" src="../js/jquery.validate.js"></script>

  <script type="text/javascript"

           src="../js/jquery.validate.password.js"></script>

EOD;

  if (!is_null($embedded_javascript)) {

    echo "<script type='text/javascript'>" .

         $embedded_javascript .

         "</script>";

  }

  echo " </head>";

}

Update your links in index.html to reference signup.php rather than create_user.html:

<div id="content">

  <div id="home_banner"></div>

  <div id="signup">

    <a href="signup.php"><img src="../images/sign_me_up.png" /></a>

    <a href="signin.php"><img src="../images/sign_me_in.png" /></a>

  </div>

</div>

Now you can check out the new page—and what should be a new menu. The results are shown in Figure 12-11. This is the “not logged in” version of the menu, so now you’ve tested both versions. Excellent.

The menu in its current state doesn’t offer a ton of new functionality. Still, lots of users forget they’re signed up for a site, and need a simple way to get to the sign-in page, rather than the sign-up page. And you can add About information, Contact information, and anything else you want that might not require authentication, and it all becomes available to this form now.

Figure 12-11. The menu in its current state doesn’t offer a ton of new functionality. Still, lots of users forget they’re signed up for a site, and need a simple way to get to the sign-in page, rather than the sign-up page. And you can add About information, Contact information, and anything else you want that might not require authentication, and it all becomes available to this form now.

Challenge: Be Self-Referential with User Creation

Surely by now you realize that you don’t need two scripts to handle user creation, right? You could create a single script that submits to itself. That would allow you to not only do the client-side validation already in place with jQuery and JavaScript, but check usernames and emails against the database, and return errors if there are duplicates.

But by this stage, you don’t need to see another big long code listing. You can just do it yourself. So go ahead. Set this book down and get after combining signin.php and create_user.php. As always, there’s swag to be had by tweeting your solution to @missingmanuals or hopping on Facebook at www.facebook.com/MissingManuals.

Log Users Out

You’ve got logging in working, but don’t forget to add logging out. Whether you set your cookie’s expiration value to a short one—even just a few minutes—or a long one, always allow users to control their own authentication. They should be able to log in when they want, and log out when they want.

Logging in involved setting a cookie name, value, and optionally a time for expiration:

setcookie('user_id', $user_id);             // Defaut expiration:

setcookie('username', $result['username']); // Log out on browser close

Logging out is much the same, inverted. Just set the cookie’s value to an empty value, and set the expiration in the past:

// Expire the user_id cookie with a date a year in the past

setcookie('user_id', '', time()-(60*60*24*365));

In this case, the “user_id” cookie’s value is set to nothing (an empty string), and the expiration date is set to a year in the past.

NOTE

Be sure you set the expiration well into the past. That way, if the system time on your server is off by a few minutes or even days, it doesn’t affect your code. If the system time is more than a year off, then you have bigger issues.

Turning this into a script is awfully simple. Just expire the two cookies your app uses—”user_id” and “username”—and redirect the user back to a home page or sign in page.

<?php

setcookie('user_id', '', time()-(365*24*60*60));

setcookie('username', '', time()-(365*24*60*60));

header('Location: signin.php');

?>

To try it out, visit your app (after closing your browser and clearing any cookies), and sign in as a known user. You should be able to visit show_user.php, show_users.php, and delete users. That’s all working as it should.

NOTE

Well, it’s kind of working. Any old user shouldn’t be able to see all the users and delete users, but you’ll fix that shortly.

Now click the Sign Out link on the menu. You should be redirected to the sign-in page. You also can visit pages that require a user ID, and you’ll not see your user’s profile. That’s good…but what happens isn’t. Check out Figure 12-12.

So signing out appears to be working, but it’s revealed a nasty hole in the app: pages that shouldn’t be accessible at all are accessible. They just error out, which is arguably worse than just letting unauthorized users see them. No matter how you cut it, there’s an issue to be resolved.

There’s definitely no value in the user_id cookie. But this really isn’t what users should see. Instead, some sort of error about not being logged in should appear. Or, even better, the sign-in page should be displayed, so the user can sign in and access your system.

Figure 12-12. There’s definitely no value in the user_id cookie. But this really isn’t what users should see. Instead, some sort of error about not being logged in should appear. Or, even better, the sign-in page should be displayed, so the user can sign in and access your system.

Require the Cookie to Be Set

Fortunately, this isn’t hard to fix. Earlier, show_user.php and other restricted scripts required authorize.php, which did all sorts of database work to see if a user could log in, all using basic HTTP authentication. But in that, you got a nice wall around your scripts.

By removing authorize.php, it became possible to have signin.php handle logins. But in the process, you tore out that wall around your other scripts. So you need the wall, but you still need to let signin.php handle authentication. That’s not hard.

First, you can drastically simplify authorize.php. Chop it down to do little more than check for a valid cookie:

<?php

if ((!isset($_COOKIE['user_id'])) ||

    (!strlen($_COOKIE['user_id']) > 0)) {

}

?>

If there’s no cookie, or if the cookie has an empty value, just redirect the user to the sign-in page with a message that explains what’s going on:

<?php

if ((!isset($_COOKIE['user_id'])) ||

    (!strlen($_COOKIE['user_id']) > 0)) {

  header('Location: signin.php?' .

         'error_message=You must login to see this page.');

  exit;

}

?>

WARNING

The exit here is pretty important. Since this code will run and then pass control back to the calling script—show_user.php or delete_user.php or whatever else—then you need to make sure those scripts don’t continue to try and run. Send the redirect headers and bail out of any further action.

Perfect. Now, you can add the require_once back in to show_user.php, show_users.php, and delete_user.php.

Try this out. Make sure you’re signed out (signout.php via the menu’s link makes this trivial now). Then try and access show_user.php. You get signs of progress, although things aren’t yet perfect. Figure 12-13 is a good start, though.

Now attempts to access secure pages are sent packing. Excellent… but where’s the helpful message? Notice that it’s on the request URL, but doesn’t show up on the page.

Figure 12-13. Now attempts to access secure pages are sent packing. Excellent… but where’s the helpful message? Notice that it’s on the request URL, but doesn’t show up on the page.

So what gives? Well, there’s nothing in signin.php that deals with a potential message on the request URL. But you’ve actually got the mechanics for this in place. Open up signin.php, and check out the opening section:

require_once '../scripts/view.php';

$error_message = "";

// If the user is logged in, the user_id cookie will be set

if (!isset($_COOKIE['user_id'])) {

This is great. And you’ve already got code to display $error_message as an error:

// Still in the "not signed in" part of the if

// Start the page, and pass along any error message set earlier

page_start("Sign In", NULL, NULL, $error_message);

So now you just need to see if there’s a request parameter back up at the top:

<?php

require_once '../scripts/database_connection.php';

require_once '../scripts/view.php';

$error_message = $_REQUEST['error_message'];

// If the user is logged in, the user_id cookie will be set

if (!isset($_COOKIE['user_id'])) {

Piece of cake. Now, you can try things one more time. Hit show_user.php without a cookie set, and you should see something like Figure 12-14.

So what’s left? Take a look back at your original list:

§  A better login screen. Nobody likes a bland gray pop-up; they want a branded, styled login form. (Done!)

§  Better messaging indicating whether a user is logged in or not. (Done!)

§  A way to log out. (Done!)

§  Two levels of authentication: one to get to the main application, and then admin-level authentication to get to a page like show_users.php and delete_user.php(Hmmm, nothing here yet at all.)

§  Some basic navigation—and that navigation should change based on a user’s login and the groups to which they belong. (Sort of done…)

Pretty good. So take a quick breath, and it’s time for the home stretch: group-based authentication, and the reason that cookies are cool, but maybe not your final authentication destination.

Perfect! Blocked access, a helpful error message, and an immediate chance to sign in. That’s just what you want. Things are looking pretty good.

Figure 12-14. Perfect! Blocked access, a helpful error message, and an immediate chance to sign in. That’s just what you want. Things are looking pretty good.