1. Introduction
    1. About Agavi
    2. MVC in Agavi
    3. Overview of Agavi
    4. Overview of Application Execution Flow
    5. A Word About Actions
    6. Application filesystem layout
    7. Overview of application configuration
  2. Setting Up The Initial Application
    1. Installing Agavi
    2. Creating an Agavi Project
    3. Finishing The Setup
    4. Finishing The Basic Setup
    5. Installing a New Copy of Your Application
  3. Adding First Code
    1. Creating A New module
    2. Creating A New Action
    3. Tying Things Together — An Introduction To Routing
    4. Fixing The Bloggie Routing
    5. Accessing Request Parameters and Validation Basics
    6. Handling Validation Errors
  4. Putting The M in MVC
    1. Creating A New Model
    2. Adapting The Actions and Views
    3. Custom Validators
  5. Polishing It Up
    1. Layers and Layouts
    2. Applying Our Layout
    3. What Are Slots?
    4. Adding The Post's Title To The URL
    5. Routing Callbacks
    6. Using Callbacks for the Title in URLs
  6. Connecting to a database
    1. The Database Manager
  7. Handling Output Variants
    1. Output Types
    2. Exception Templates
    3. Generating an RSS Feed
  8. Form Processing
    1. Adding a Post
    2. Editing a Post
    3. The Form Population Filter (FPF)
  9. Creating a User Authentication System
  10. Adding To The Master Template

Creating a User Authentication System

Since we now have the basics of an admin section, it's probably wise to protect it. To do this, we need to use the power of Agavi's built in user system. Agavi has a rather useful system for this, it uses the AgaviRbacSecurityUser class.

Creating the User Class

Firstly, we need to create a class for interfacing with Agavi, so create a new class in app/lib/user (if this directory doesn't exist go ahead and create it) called BlogUser (BlogUser.class.php). Our user class will be comprised out of the follwing methods:
  • startup()
  • login()
  • logout()
  • getCurrentUser()
So now we have our empty class, we need to extend it with AgaviRbacSecurityUser and start implementing these features, so we'll do them one-by-one.
class BlogUser extends AgaviRbacSecurityUser
{
  public function startup()
  {
    // call parent
    parent::startup();

    $reqData = $this->getContext()->getRequest()->getRequestData();

    if(!$this->isAuthenticated() && $reqData->hasCookie('autologon')) {
      $login = $reqData->getCookie('autologon');
    
      try {
        $this->login($login['username'], $login['password'], true);
      } catch(AgaviSecurityException $e) {
        $response = $this->getContext()->getController()->getGlobalResponse();

        // Unset the the login cookie since it didn't work
        $response->setCookie('autologon[username]', false);
        $response->setCookie('autologon[password]', false);
      }
    }
  }
}

The startup() method is rather short and to the point, it checks if the user is already logged in and if the user has a valid cookie stored for automatically logging in.

The login() method is rather simple, it simply just grabs the correct user from the UserManager model (we'll get to that a bit later in this chapter) and then checks if the credentials supplied matches the ones stored.
public function login($username, $password, $isPasswordHashed = false)
{
  $userManager = $this->getContext()->getModel('UserManager');
  $user = $userManager->retrieveById($username);

  if(!$user->getId()) {
    throw new AgaviSecurityException('username error');
  }

  if(!$isPasswordHashed) {
    $password = sha1($password . $user->getSalt());
  }

  if($password != $user->getPassword()) {
    throw new AgaviSecurityException('password error');
  }

  $this->setAuthenticated(true);
  $this->grantRoles($user->getRoles());
  $this->user = $user;
}
To logout of the system, it's a case of just clearing the credentials stored in the session.
public function logout()
{
  $this->clearCredentials();
  $this->setAuthenticated(false);
}
Now we just have the getCurrentUser() method to implement, it's not too much different to the login code,
public function getCurrentUser()
{
  $id = $this->getAttribute('id', 'org.ditabrain.user');
  $user = null; 
  
  if ($this->isAuthenticated()) {
    $userMan = $this->getContext()->getModel('UserManager');
    $user = $userMan->retrieveById($id);
  }

  return $user;
}
Right, with our class completed
<?php

class BlogUser extends AgaviRbacSecurityUser
{
  protected $user;

  public function startup()
  {
    // call parent
    parent::startup();

    $reqData = $this->getContext()->getRequest()->getRequestData();

    if(!$this->isAuthenticated() && $reqData->hasCookie('autologon')) {
      $login = $reqData->getCookie('autologon');
  
      try {
        $this->login($login['username'], $login['password'], true);
      } catch(AgaviSecurityException $e) {
        $response = $this->getContext()->getController()->getGlobalResponse();

        // Unset the the login cookie since it didn't work
        $response->setCookie('autologon[username]', false);
        $response->setCookie('autologon[password]', false);
      }
    }
  }
  
  public function login($username, $password, $isPasswordHashed = false)
  {
    $userManager = $this->getContext()->getModel('UserManager');
    $user = $userManager->retrieveById($username);

    if(!$user->getId()) {
      throw new AgaviSecurityException('username error');
    }

    if(!$isPasswordHashed) {
      $password = sha1($password . $user->getSalt());
    }

    if($password != $user->getPassword()) {
      throw new AgaviSecurityException('password error');
    }
  
    $this->setAttribute('id', $user->getId(), 'org.bloggie.user');

    $this->setAuthenticated(true);
    $this->grantRoles($user->getRoles());

    $this->user = $user;

    return true;
  }

  public function logout()
  {
    $this->clearCredentials();
    $this->setAuthenticated(false);
  }

  public function getCurrentUser()
  {
    if ($this->isAuthenticated()) {
      return $this->user;
    }
  }
}

?>
We need to add the class to the project, there's two things we need to do for this, add it to app/config/autoload.xml
<autoload name="BlogUser">%core.lib_dir%/user/BlogUser.class.php</autoload>
and app/config/factories.xml which is already set as AgaviRbacSecurityUser so change so it looks like below
<user class="BlogUser" />

Creating the Models

Like before, you use the agavi build tool to create your models, User and UserManager with the difference of, you're using the project-model-create functionality.
class UserManagerModel extends BlogBaseModel
{
  private $users = array(
    'chuck' => array(
      'id' => 'chuck',
      'name' => 'Chuck Norris',
      'password' => 'a92b2df2a6863585637ac733044be05032bd1a7b', // roundhouse with salt
      'salt' => 'b295d117135a9763da282e7dae73a5ca7d3e5b11', // salt
      'roles' => array('admin'),
    )
  );

  public function retrieveById($id)
  {
    if (isset($this->users[$id])) {
      return $this->getContext()->getModel('User', null, array($this->users[$id]));
    }
 
    throw new Exception('invalid user specified');
  }
}
User Model
class UserModel extends BlogBaseModel
{
  protected $id;

  protected $name;

  protected $salt;

  protected $password;

  protected $roles;

  protected static $fields = array(
    'id',
    'name',
    'salt',
    'password',
    'roles',
  );

  public function __construct( $data )
  {
    $this->fromArray($data);
  }

  public function toArray()
  {
    $retval = array();
  
    foreach(self::$fields as $field) {
      $retval[$field] = $this->$field;
    }
  
    return $retval;
  }

  public function fromArray(array $data = array())
  {
    foreach (self::$fields as $field) {
      if (isset($data[$field])) {
        $this->$field = $data[$field];
      }
    }
  }

  public function getId()
  {
    return $this->id;
  }

  public function getName()
  {
    return $this->Name;
  }

  public function getPassword()
  {
    return $this->password;
  }

  public function getRoles($accountid = null)
  {
    return $this->roles;
  }
  
    return $this->roles;
  }

  public function getSalt()
  {
    return $this->salt;
  }

  public function setName($value)
  {
    $this->name = $value;
  }

  public function setPassword($value)
  {
    $this->password = $value;
  }

  public function setRoles($value)
  {
    $this->roles = $value;
  }

  public function setSalt($value)
  {
    $this->salt = $value;
  }
}

Login Page

Since Agavi creates a set of default actions when you first create a project such as "Error404", it also created a "Login" action, which we'll be using now. Firstly, we need to create the view templates for each of the views.
  st-macbook:demoapp sth$ agavi view-template-create
   [property] Loading /Users/sth/Sites/demoapp/build.properties
  [agavi.import] Importing external build file /Users/sth/Sites/demoapp/build.xml

  Agavi > project-locate:

   [property] Loading /Users/sth/Sites/demoapp/build.properties

  Agavi > module-locate:

  Module name: Default
   [property] Loading /Users/sth/Sites/demoapp/app/modules/Default/build.properties
   [property] Unable to find property file: /Users/sth/Sites/demoapp/app/modules/Default/build.properties... skipped

  Agavi > action-locate:

  Action name: Login

  Agavi > view-locate:

  View name: Input

  Agavi > view-template-create:

   [property] Loading /Users/sth/Sites/demoapp/build.properties
  [agavi.import] Importing external build file /Users/sth/Sites/demoapp/build.xml

  Agavi > project-locate:

   [property] Loading /Users/sth/Sites/demoapp/build.properties

  Agavi > module-locate:

   [property] Loading /Users/sth/Sites/demoapp/app/modules/Default/build.properties
   [property] Unable to find property file: /Users/sth/Sites/demoapp/app/modules/Default/build.properties... skipped

  Agavi > template-create:

  Template extension [php]: RETURN KEY  
       [copy] Copying 1 file to /Users/sth/Sites/demoapp/app/modules/Default/templates
You need to do this again for the "Success" view.
Lets start by adding the form code for our login.
<div id="box">
  <h2><?php echo $t['_title']; ?></h2>
  <form id="login" action="<?php echo $ro->gen('login'); ?>" method="post">
    <fieldset>
      <div class="form-row">
        <div class="form-label">
          <label for="username">Username</label>
        </div>
        <div class="form-control">
          <input type="text" id="username" name="username" />
        </div>
      </div>
      <div class="form-row">
        <div class="form-label">
          <label for="password">Password</label>
        </div>
        <div class="form-control">
          <input type="password" id="password" name="password" />
        </div>
      </div>
      <div class="form-row-nolabel">
        <input type="checkbox" id="remember" name="remember" value="remember" />
        <label for="remember">Stay Signed in for a Week</label>
      </div>
    </fieldset>
    <fieldset>
      <div class="form-row-nolabel">
        <input type="submit" name="login" value="Log in" />
      </div>
    </fieldset>  
  </form>
</div>

Login Action

For the login page to be usable, we need to add the validation rules for the three controls we've just created for our login form.
<?xml version="1.0" encoding="UTF-8"?>
<ae:configurations
  xmlns="http://agavi.org/agavi/config/parts/validators/1.0"
  xmlns:ae="http://agavi.org/agavi/config/global/envelope/1.0"
  parent="%core.module_dir%/Default/config/validators.xml"
>
  <ae:configuration>

    <validators method="write">
      <validator class="string" required="true" name="user_valid">
        <argument>username</argument>
        <error>Username error.</error>
        <ae:parameters>
          <ae:parameter name="min">4</ae:parameter>
        </ae:parameters>
      </validator>

      <validator class="string" required="true" name="password_valid">
        <argument>password</argument>
        <error>Your password is too short.</error>
        <ae:parameters>
          <ae:parameter name="min">4</ae:parameter>
        </ae:parameters>
      </validator>

      <validator class="isset" required="false">
        <argument>remember</argument>
      </validator>
    </validators>

  </ae:configuration>
</ae:configurations>
Now I guess it's time for the action code for Default_LoginAction, the code is rather simple, we just add an executeWrite() and try to login checking for exceptions.
  <?php

  class Default_LoginAction extends BlogDefaultBaseAction
  {
    /**
     * Returns the default view if the action does not serve the request
     * method used.
     *
     * @return     mixed <ul>
     *                     <li>A string containing the view name associated
     *                     with this action; or</li>
     *                     <li>An array with two indices: the parent module
     *                     of the view to be executed and the view to be
     *                     executed.</li>
     *                   </ul>
     */
    public function getDefaultViewName()
    {
      return 'Input';
    }
  
    public function executeWrite(AgaviRequestDataHolder $rd)
    {
      try {
        $user = $this->getContext()->getUser();
        $user->login($rd->getParameter('username'), $rd->getParameter('password'));
        return 'Success';
      } catch (AgaviSecurityException $e) {
        return 'Error';
      }
    }
  }

  ?>
        

By now, you could log into the site at "/login" but, it's not very functional. We created two templates for two different views earlier in this chapter, so we should add some code into these views.

The Default_LoginInputView:
<?php

class Default_LoginInputView extends BlogDefaultBaseView
{
  public function executeHtml(AgaviRequestDataHolder $rd)
  {
    $user = $this->getContext()->getUser();
    if($this->getContainer()->hasAttributeNamespace('org.agavi.controller.forwards.login')) {
      // we were redirected to the login form by the controller because the requested action required security
      // so store the input URL in the session for a redirect after login
      $user->setAttribute('redirect', $this->getContext()->getRequest()->getUrl(), 'org.agavi.bloggie.login');
    } else {
      // clear the redirect URL just to be sure
      // but only if request method is "read", i.e. if the login form is served via GET!
      $user->removeAttribute('redirect', 'org.agavi.bloggie.login');
    }
  
    $this->setupHtml($rd);
    $this->setAttribute('_title', 'Login');
  }
}

?>
The Default_LoginSuccessView:
<?php

class Default_LoginSuccessView extends BlogDefaultBaseView
{
  public function executeHtml(AgaviRequestDataHolder $rd)
  {
    $user = $this->getContext()->getUser();
    $res = $this->getResponse();
  
    // set the autologon cookie if requested
    if($rd->hasParameter('remember')) {
      $res->setCookie('autologon[username]', $rd->getParameter('username'), '+1 week');
      $res->setCookie('autologon[password]', $user->getCurrentUser()->getPassword(), '+1 week');
    }
  
    if($user->hasAttribute('redirect', 'org.agavi.bloggie.login')) {
      $this->getResponse()->setRedirect($user->removeAttribute('redirect', 'org.agavi.bloggie.login'));
      return;
    } else {
      $this->getResponse()->setRedirect($this->getContext()->getRouting()->gen('index'));
    }
  
    $this->setupHtml($rd);
    $this->setAttribute('_title', 'Login');
  }
}

?>

Making Things Secure

We now have a working authentication system for our blog, but it's no good if we're not protecting anything. To do this, we need to add a method to the actions we want to protect. A reasonable page in our blog to protect is the "add a blog entry" (Posts_AddAction) page created earlier.
public function isSecure()
{
  return true;
}

The isSecure() method is simply just added to the bottom of any action class you want to protect.

The source code for this chapter is available here: Stage 8 (http://www.agavi.org/guide/stages/stage8.tgz