Sessions | Authentication Model

A POST request contains two set of arguments: those that are present in the query string (uri?arg=value&arg=value) and those that are present in the request body. The default format for the request body is the same as the query string (arg=value&arg=value), but a different format is used when sending large amounts of data, such as files. This is all you need to know about the format itself, although you can read more about it.

PHP lets you access those data sets through superglobal variables. $_GET lets you access query string arguments, whether you are in a GET or POST request (if you really need to know whether the request is a GET, a POST or something else, use $_SESSION['REQUEST_METHOD']), and $_POST lets you access request body arguments regardless of their encoding. If any files were uploaded, you can find them in $_FILES, but I will discuss that later on.

Controllers follow different patterns depending on whether they are GET or POST controllers. GET controllers:

  • Read $_GET data and validate it.
  • If the data is invalid, they redirect to a GET recovery page.
  • If the data is valid, they extract data from the persistent state and display it.
  • In rare occasions, small unimportant elements of persistent state may be changed (such as the number of views).

On the other hand, POST controllers:

  • Read $_GET and $_POST data and validate it.
  • If the data is valid, they update the persistent state.
  • They may display a success/failure message, or redirect to a GET followup page.

In practice, the data in $_GET and $_POST may need some treatment before it can be used:

<?php // utils/request.php

  if (get_magic_quotes_gpc()) {
      foreach ($_POST as & $v)    $v = stripslashes($v);
      foreach ($_GET as & $v)     $v = stripslashes($v);
      foreach ($_COOKIE as & $v)  $v = stripslashes($v);
  }

  class RequestUtils
  {
      public static function Read($keys, & $from)
      {
          $data = array();
          foreach ($keys as $key) {
              $is_array = ($key != '' && $key{strlen($key) - 1} == ':');

              if (array_key_exists($key, $from))
                  $data[$key] = $from[$key];
              elseif (!$is_array)
                  $data[$key] = null;

              if ($is_array) {
                  $subkey = substr($key, 0, -1);
                  $values = array();
                  foreach ($from as $post_key => $post_value) {
                      $split = explode(':', $post_key, 2);
                      if (count($split) == 2 && $split[0] == $subkey) {
                          $values[$split[2]] = $post_value;
                      }
                  }
                  $data[$subkey] = $values;
                  unset($values);
              }
          }
          return $data;
      }
  }

First, there’s the initial cleanup step if get/post/cookie magic quotes are enabled—the PHP site has a heap of documentation about these and they are universally considered to be a pain by developers. The basic idea is that, in order to protect the database from injections, all dangerous characters (such as quotes) are escaped with backslashes before they even reach your script. This means, mostly, that the contents of your variables is not what your user sent, and that if the user entered his name as O’Malley you will see his name as O’Malley. Even worse: if you display the data back to the user and they submit it again, you will be looking at O\’Malley, and O\\\’Malley on the third round, and so on. Magic quotes are evil, disable them whenever you can in your server configuration.

Second, it would be interesting to support dynamic forms: those where Javascript is used to add/remove form fields at runtime on the client without the server noticing. The server should then be able to extract that data.

The solution I use here is to allow arrays of fields. For instance, if the dynamic form allows an arbitrary number of fields named “address”, I wil have the Javascript code name them “address:0″, “address:1″ … “address:42″. Then, I will ask the PostUtils::Read function for the field named “address:”, and it will return me an array where the “address” key is not a simple text value, but a hash that contains every field that starts with “address:”. An example:

Input:
  'abc'       => 'abc'
  'foo:1'     => '10'
  'foo:2'     => '20'
  'foo:3:4'   => '34'
  'foo:hello' => 'world'

Output:
  'abc' => 'abc'
  'foo' => array(
             '1'     => '10'
             '2'     => '20'
             '3:4'   => '34'
             'hello' => 'world'
           )

Of course, HTTP being what it is, all data is transferred as strings, so don’t expect your integers to be integers, they will be strings like anything else (and this means both values and keys).

That being said, here comes the implementation of our user login controller:

<?php // controllers/do-login.php                                                                                                                                                                                                                                                    

  // Abort the controller and redirect back to login page
  function on_error($errors)
  {
      SessionUtils::Write('login/errors', $errors);
      RedirectUtils::SeeOther(DomainConfig::Url('/login'));
  }

  $post = RequestUtils::Read(array('mail', 'pass'), $_POST);
  $errors = array();

  $mail = $post['mail'];
  $pass = $post['pass'];

  SessionUtils::Write('login/values', array('login_mail' => $mail));

  if (!$mail) {
      $errors['login_mail'] = 'Please enter your e-mail';
      on_error($errors);
  }

  if (!$pass) {
      $errors['login_pass'] = 'Please enter your password';
      on_error($errors);
  }

  // Attempt to log in with provided data
  $token = AuthenticationModel::Login($mail, $pass);

  if (!isset($token)) {
      $errors['login_pass'] = 'Incorrect password';
      on_error($errors);
  }

  // Login succeeded. Store token and redirect
  SessionUtils::Delete('login/values', array('login_mail'));
  SessionUtils::Write('authentication', array('token' => $token));
  RedirectUtils::SeeOther(DomainConfig::Url('/'));

And the user sign-up controller looks like this:

<?php // controllers/do-signup.php                                                                                                                                                                                                                                                   

  // Abort the controller and redirect back to login page
  function on_error($errors)
  {
      Sessionutils::Write('login/errors', $errors);
      RedirectUtils::SeeOther(DomainConfig::Url('/login'));
  }

  $post  = RequestUtils::Read(array('mail', 'name', 'pass', 'pass2'), $_POST);

  $mail  = $post['mail'];
  $pass  = $post['pass'];
  $name  = $post['name'];
  $pass2 = $post['pass2'];

  SessionUtils::Write('login/values', array('signup_mail' => $mail, 'signup_name' => $name));

  if (!$mail) {
      $errors['signup_mail'] = 'Please enter your e-mail';
      on_error($errors);
  }

  if (!$name) {
      $errors['signup_name'] = 'Please enter your name';
      on_error($errors);
  }

  if (!$pass) {
      $errors['signup_pass'] = 'Please enter your password';
      on_error($errors);
  }

  if (strlen($pass) < 6) {
      $errors['signup_pass'] = 'Please enter a password of at least six letters';
      on_error($errors);
  }

  if ($pass !== $pass2) {
      $errors['signup_pass2'] = 'Passwords do not match';
      on_error($errors);
  }

  if (!AuthenticationModel::IsMailAvailable($mail)) {
     $reset '/reset-password?for=' urlencode($mail);
     $reset_url DomainConfig::Url($reset);
     $error sprintf('E-mail already registered. ' .
                      '<a href="%s">Retrieve your password</a>.', 
                      $reset_url);
     $errors['signup_mail'] = $error;
     on_error($errors);
  }

  // Create a new user and authenticate at the same time
  $token = AuthenticationModel::Register($mail, $name, $pass);

  assert(isset($token));

  SessionUtils::Delete('login/values', array('signup_mail', 'signup_name'));
  Sessionutils::Write('authentication', array('token' => $token));
  RedirectUtils::SeeOther(DomainConfig::Url('/'));

For now, the authentication model looks like this, because we don’t really need more than stubs to check whether error-handling is correct or not:

<?php // models/authentication.php                                                                                                                                                                                                                                                   

  class AuthenticationModel
  {
      public static function Login($mail, $pass)
      {
          return null;
      }

      public static function isMailAvailable($mail)
      {
          return false;
      }

      public static function Register($mail, $name, $pass)
      {
          return null;
      }
  }

Sessions | Authentication Model

0 Responses to “9. POST Requests”


  1. No Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

*

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>



1150 feed subscribers
(readers who polled a feed this week)