lunes, 6 de agosto de 2007

Curiosas propiedades de los behaviors

Recién vuelto (casi) de las vacaciones, hoy he retomado el proyecto Cake en el que estoy metido. Sigo dado toques a la fase de autentificación para mis aplicaciones web. Concretamente estoy trabajando en un behavior que me permita usar cualquier modelo para autentificar.

Esto es, en lugar de tener un Model Usuario con los métodos necesarios, lo que quiero es trasladar la funcionalidad a un Behavior de modo que el modelo quede "liberado" de la parte de autentificación y que ésta sea reutilizable. La cuestión es que las diferentes aplicaciones que tengo que poner en marcha o reescribir tienen distintos requisitos en lo que respecta al modelo de usuario.

Eso me ha permitido aprender unas cuantas cosas acerca del uso de Behaviors aparte de lo que ya había escrito anteriormente.

Una de las más importantes es la capacidad de añadir métodos al Model como si fueran propios, es decir, los puedes llamar con model->metodo (), aunque los hayas definido en el behavior.

Hay que tener una precaución según desde donde llames al método. Si es desde el propio modelo o behavior no ocurre nada especial. Pero si lo llamas desde un controlador, por ejemplo, CakePHP automágicamente añade una referencia al propio modelo como parámetro.

Por otra parte, la capacidad de integración de los behaviors con los models a mí me sigue pareciendo asombrosa. Así, aunque los behaviors no contemplan el callback beforeValidate, es posible, por ejemplo, invalidar campos de beforeSave, para realizar validaciones personalizadas.

El resultado puede parecer un poquito extraño ya que los formularios se validan entonces en dos etapas (la "normal" y la del "behavior") pero ciertamente funciona como debe. Es decir, la aplicación protesta primero por unos campos y luego por otros. Supongo que se puede mejorar esto, pero aún no he llegado tan lejos.

En cualquier caso, el resultado de este "subproyecto" me ha permitido sacar a un behavior todo el código relacionado con la autentificación, incluyendo el procesado de elementos "extraños", como todos los tejemanejes de contraseñas hasheadas procedentes del formulario pero que no son realmente parte del modelo.

Me explico. Al crear un usuario, por ejemplo, la contraseña es "hasheada" en javascript para que viaje encriptada desde del cliente al servidor. El campo hash contiene esta encriptación, mientras que el campo de la contraseña vuelve vacío. Si no hay javascript la contraseña viene desprotegida en su campo El modelo debe entonces controlar esta situación antes de guardar los datos y asegurarse de que el modelo los contiene tal y como los espera la base de datos. Y eso es justamente lo que hace el behavior en beforeSave.

Claro que el behavior forma parte de un paquete que incluye un helper y un component. La idea final es que este paquete sea una "caja negra" y que el Model no tenga que saber nada sobre ella, ocupándose sólo de sus cosas.

Este es el código que habría que poner en models/helpers/autentificacion.php.


<?php
/**
* autentificacion
*
* Created by Frankie on 2007-07-19.
* Copyright (c) 2007 Macintec. All rights reserved.
* 2007-08-06. Added abstraction to valid() method. The behavior now is model independant
* 2007-08-06. Added beforeSave() method to preprocess incoming data
**/

/**
* This behavioir adds Authentitfication abilities to a model, you can assign fields
* to act as user and password fields...
*
* @package autentificacion
* @author Frankie
**/
class AutentificacionBehavior extends ModelBehavior {
var $default_settings = array (
'user' => 'user',
'password' => 'password'
);
var $settings;
var $model;

function beforeSave (&$model) {
/**
* Si el campo hash contiene valores, significa que esa es la contraseña hasheada
* y por lo tanto, debemos poner ese valor en el campo de contraseña para guardar.
* El Javascript habrá borrado la contraseña en clave, por lo que este campo viene
* vaío.
* Por otro lado, si hash no contiene valores, entonces es que el cliente no tiene
* javascript y la contraseña viene abierta en el campo clave. Por último, en caso
* de que estemos editando el usuario, la clave puede venir vacía. En ese caso,
* se elimina el campo clave para no borrar la clave hash en la base de datos.
* El campo Hash se elimna de todas maneras porque no pertenece al modelo.
* TO DO: Establecer como preferencia que se permitan o no claves en abierto
*
* @author Frankie
*/
if (empty ($model->data[$model->name][$this->settings['password']]) && empty ($model->data[$model->name]['hash']) && empty ($model->id)) {
$model->invalidate ($this->settings['password'], __('Please, you must provide a password', true));
return false;
}

if (!empty ($model->data[$model->name]['hash'])) {
$model->data[$model->name]['clave'] = $model->data[$model->name]['hash'];
} elseif (!empty ($model->data[$model->name]['clave'])) {
$model->data[$model->name]['clave'] = sha1 ($model->data[$model->name]['clave']);
}
if (empty ($model->data[$model->name]['clave']) && !empty ($model->id)) {
unset ($model->data[$model->name]['clave']);
}
unset ($model->data[$model->name]['hash']);
return true;
}

function setup (&$model, $settings) {
// TODO: verificar que los campos pertenecen al modelo ???
$this->settings = am ($this->default_settings, $settings);
$this->model = $model;
}

/**
* Returns user record if the user is a registered user
*
* @return boolean true if user and password exists
* @author Frankie
**/
function valid (&$model, $userData, $salt = false) {
$user = $userData[$model->name][$this->settings['user']];
$password = $userData[$model->name][$this->settings['password']];
if ($salt) {
// User validation with salt
$conditions = "WHERE {$this->settings['user']} = '$user' AND sha1(concat({$this->settings['password']},'$salt')) = '$password'";
} else {
// User validation without salt, insecure login
$conditions = "WHERE {$this->settings['user']} = '$user' AND {$this->settings['password']} = '$password'";
}
if ($userFound = $this->model->find ($conditions)) {
return $userFound;
}
return false;
}


} // END class AutentifiacionBehavior extends ModelBehavior


?>




Y esta es la forma de usarlo en un modelo cualquiera. El modelo tiene los campos id, usuario, clave, email.

Al asociar el behavior indicamos que utilice usuario como campo user y clave como campo password.


<?php
/**
* usuario
*
* Created by Frankie on 2007-07-04.
* Copyright (c) 2007 Macintec. All rights reserved.
**/

/**
* Usuario
*
* @package /Applications/MAMP/htdocs/micake
* @author Fran Iglesias
* @copyright Copyright 2007 Fran Iglesias
* @version Revision: 0.1
* @modifiedby Fran Iglesias
* @lastmodified 2007-07-04
* @license http://www.opensource.org/licenses/mit-license.php The MIT License
**/

class Usuario extends AppModel
{
var $name = "Usuario";
var $validate = array (
'usuario' => array (
'esUnico' => array (
'rule' => 'usuarioEsUnico',
'message' => 'User name exists. Please, choose another.'
),
'usuarioNoVacio' => array (
'rule' => VALID_NOT_EMPTY,
'message' => 'User name empty. Please, give it a name.'
)
),
'email' => array (
'emailValido' => array (
'rule' => VALID_EMAIL,
'message' => 'Please, type a valid email.'
)
)
);

var $actsAs = array ('Autentificacion' => array ('user' => 'usuario', 'password' => 'clave'));

/**
* Verifica que el nombre de usuario aportado no existe en la bd
*
* @return boolean true if unique
* @author Frankie
**/
function usuarioEsUnico ($nombreUsuario) {
$esValido = false;
$condiciones['usuario'] = $nombreUsuario;
if ($this->id) {
$condiciones['id'] = '<> '.$this->id;
}
$esValido = $this->isUnique ($condiciones, false);
return $esValido;
}

}

// END class Usuario
?>

No hay comentarios: