Posts etiquetados ‘código’

Dada la gran cantidad de preguntas que he recibido acerca de cómo implementar un sistema basado en roles en CakePHP, he decidido hacer este post para aclarar todas esas cuestiones, ya que creo que con un ejemplo completo todo quedará más claro.
Dicho ejemplo contará tanto con un apartado de registro de nuevos usuarios, así como con el sistema de autenticación correspondiente y, evidentemente, con el meollo de la cuestión: un sistema de acceso a zonas de la aplicación basada en roles. Además todo ello funcionando bajo la versión 2.6 de CakePHP. ¿Qué más se puede pedir?
Debido a que poner todo el código completo en el post lo haría demasiado extenso e intimidatorio, voy a mostrar y explicar solo aquellas partes relevantes que componen esta aplicación. Doy por hecho que el lector ya tiene conocimientos sobre CakePHP, al menos los esenciales.
De todas formas, como no podía ser de otra manera, al final de todo, pondré un enlace a mi repositorio en Github para que podáis descargar y probar el proyecto.
Comenzemos pues.
Lo primero será crear nuestra base de datos:

 

CREATE TABLE users (
    `id` INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
    `username` VARCHAR(128),
    `password` VARCHAR(128),
    `email` VARCHAR(128),
    `role` VARCHAR(64),
    `created` DATETIME DEFAULT NULL,
    `modified` DATETIME DEFAULT NULL,
    `status` tinyint(1) NOT NULL DEFAULT '1'
);

El campo ‘status’ lo usaremos para poder activar o desactivar usuarios sin necesidad de eliminarlos totalmente de nuestra BBDD. Lo veremos más adelante.

Importante: Antes de nada, accediendo a nuestra BBDD, deberemos poner el campo ‘role’ con un valor predeterminado personalizado: ‘usuario’ (sin las comillas, obviamente).

Deberemos tener en cuenta que hay que modificar los datos en el archivo app/Config/database.php.default para establecer los datos de conexión que hayamos elegido: nombre de la BBDD, usuario, contraseña, tipo de BBDD…, y guardar los cambios como database.php. De esta forma ya estaremos listos para conectar nuestra aplicación con la BBDD.
En el archivo routes.php de la misma carpeta tenemos las siguientes rutas definidas:

 

<?php
/**
 * Routes configuration
 *
 * In this file, you set up routes to your controllers and their actions.
 * Routes are very important mechanism that allows you to freely connect
 * different urls to chosen controllers and their actions (functions).
 *
 * PHP 5
 *
 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 * @link          http://cakephp.org CakePHP(tm) Project
 * @package       app.Config
 * @since         CakePHP(tm) v 0.2.9
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
 */
/**
 * Here, we are connecting '/' (base path) to controller called 'Pages',
 * its action called 'display', and we pass a param to select the view file
 * to use (in this case, /app/View/Pages/home.ctp)...
 */
//Router::connect('/', array('controller' => 'pages', 'action' => 'display', 'home'));
Router::connect('/', array('controller' => 'users', 'action' => 'login'));
/**
 * ...and connect the rest of 'Pages' controller's urls.
 */
//Router::connect('/pages/*', array('controller' => 'pages', 'action' => 'display'));
Router::connect('/gestion', array('controller' => 'users', 'action' => 'index'));
Router::connect('/login', array('controller' => 'users', 'action' => 'login'));
Router::connect('/logout', array('controller' => 'users', 'action' => 'logout'));

/**
 * Load all plugin routes. See the CakePlugin documentation on
 * how to customize the loading of plugin routes.
 */
	CakePlugin::routes();

/**
 * Load the CakePHP default routes. Only remove this if you do not want to use
 * the built-in default routes.
 */
	require CAKE . 'Config' . DS . 'routes.php';

Esto nos permitirá tener URL más limpias.
Para resumir, lo que vamos a realizar es un sistema de gestión de usuarios. Al ejecutar la aplicación nos va a redirigir automáticamente al login. Allí podremos loguearnos, si ya estamos registrados, o bien acceder a la vista de creación de nuevo usuario para registrarnos.
Por defecto, todo usuario nuevo tendrá un rol de ‘usuario’ y, por lo tanto, una vez rellene el formulario de login correctamente, será redirigido a una página donde solo podrá ver la lista de usuarios, pero no editarlos ni eliminarlos. A esto último solo tendrán acceso aquellos usuarios que tengan un rol de ‘admin’, los cuales serán redirigidos a una página especial que aparecerá en la URL como ‘gestion’.

Dado entonces que tan solo los administradores podrán crear, editar y eliminar usuarios, necesitamos antes de nada crear a mano nuestro super usuario en la base de datos.

Podemos hacerlo de dos formas:

La primera consistiría en ejecutar la aplicación y registrarnos. Esto nos proporciona un rol de ‘usuario’, pero no es lo que queremos. Así que no tendremos más remedio que entrar a nuestra BBDD y cambiar el rol a ‘admin’. De esta forma, ahora sí, ya somos los únicos que tendremos acceso a todo el sistema. Si nos fijamos observaremos que la contraseña está encriptada. Esto es así porque hemos usado las herramientas que nos ofrece CakePHP para hacerlo (lo veremos a continuación). Por defecto CakePHP usa SHA1, lo que nos lleva a la segunda forma de poder crear nuestro administrador que, aunque es más complicada, nos ayudará a comprender mejor cómo encripta CakePHP las contraseñas o cualquier otro campo que queramos así.

Dado que la aplicación trabaja con SHA1 para encriptar las contraseñas de todos los usuarios, administradores o no, deberemos introducir en el campo ‘password’ de la BBDD la contraseña de nuestro administrador encriptada también si lo vamos a hacer a mano.
Para ello lo primero que haremos será ir a nuestro archivo app/Config/core.php y consultar cuál es nuestro Security.salt. En el caso de este ejemplo se encuentra en la línea 197 y luce tal que así:

Configure::write(‘Security.salt’, ‘Orange6f7cead48bd13a0cBlack0d61eb8a3502c68cacec32caYellow’);

Como ya sabéis, cuando comenzamos con una instalación limpia de Cake, la primera vez que la ejecutamos nos recuerda que debemos cambiar por seguridad los strings tanto en el Security.salt como en el Security.cypher. Es decir, el Security.salt que os he mostrado es solo a modo de ejemplo, cada uno puede poner lo que desee tanto aquí como en el string del Security.cypher.

Una vez aclarado esto, seguimos. Copiamos el string que tengamos en el Security.salt (sin las comillas, claro está) y nos vamos a la siguiente página:

Sha1 online generator

Se trata de un generador de SHA1 online. Debajo de donde se indica “Text to convert” pegamos nuestro Security.salt. A continuación, sin espacios, escribimos la clave que deseamos para nuestro administrador. Por ejemplo algo así:

Orange6f7cead48bd13a0cBlack0d61eb8a3502c68cacec32caYellowmi_contraseña_admin

Presionamos en el botón “Generate SHA1” y entonces obtendremos un poco más abajo nuestro SHA1 completo. Ahora copiamos el resultado y lo introducimos en el campo password de nuestro usuario administrador (role ‘admin’) en la BBDD.

Ya está, ya lo tenemos. Solo tendremos que acordarnos, lógicamente, de la contraseña que pusimos anteriormente como mi_contraseña_admin antes de generar el SHA1. Con esto ya podremos acceder a nuestra aplicación sin restricciones.

Ahora, una vez terminados los preparativos, vamos con las partes esenciales de la aplicación para que todo funcione perfectamente.
En el archivo app/Controller/AppController nos encontramos con esto:

 

<?php
/**
 * Application level Controller
 *
 * This file is application-wide controller file. You can put all
 * application-wide controller-related methods here.
 *
 * PHP 5
 *
 * CakePHP(tm) : Rapid Development Framework (http://cakephp.org)
 * Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 *
 * Licensed under The MIT License
 * For full copyright and license information, please see the LICENSE.txt
 * Redistributions of files must retain the above copyright notice.
 *
 * @copyright     Copyright (c) Cake Software Foundation, Inc. (http://cakefoundation.org)
 * @link          http://cakephp.org CakePHP(tm) Project
 * @package       app.Controller
 * @since         CakePHP(tm) v 0.2.9
 * @license       http://www.opensource.org/licenses/mit-license.php MIT License
 */
App::uses('Controller', 'Controller');

/**
 * Application Controller
 *
 * Add your application-wide methods in the class below, your controllers
 * will inherit them.
 *
 * @package		app.Controller
 * @link		http://book.cakephp.org/2.0/en/controllers.html#the-app-controller
 */
class AppController extends Controller {

	// added the debug toolkit
	// sessions support
	// authorization for login and logut redirect
	public $components = array(
		'DebugKit.Toolbar',
		'Session',
        'Auth' => array(
            'loginRedirect' => array('controller' => 'users', 'action' => 'index'),
            'logoutRedirect' => array('controller' => 'users', 'action' => 'login'),
			'authError' => 'Debes estar logueado para continuar',
			'loginError' => 'Nombre de usuario o contraseña incorrectos',
			'authorize' => array('Controller') 
        ));
	
	// only allow the login controllers only
	public function beforeFilter() {
        $this->Auth->allow('login');
    }
	
	public function isAuthorized($user) {
		// Admin puede acceder a todo
		// Si no es así entonces se trata de un usuario común y lo redirigimos a otra página.
		// En este caso a la acción usuario del controller users
	    if (isset($user['role']) && $user['role'] === 'admin' && $this->action='index') {
	        return true;
	    }
		elseif ($user['status'] == 1){
            $this->Session->setFlash('Bienvenido, '. $this->Auth->user('username'));
            $this->redirect('usuario');
            return true;
        }
	 	//Por defecto se deniega el acceso
	    return false;
	}
	
}

Cargamos los componentes que vamos a necesitar: Debug (opcional), Session y Auth. En Auth indicamos las redirecciones de login y logout, así como los mensajes que aparecerán cuando se producen errores al autorizar o loguear a un usuario.

En la función beforeFilter, autorizamos solo el uso de la vista login a cualquiera que inicie la aplicación.

En la función isAuthorized vamos a cotrolar el acceso según el rol de usuario que, evidentemente, ya esté autorizado a visitar otras partes de nuestra aplicación además del login.

Primero comprobamos si el usuario tiene un rol asignado, si este rol es admin, y si a lo que intenta acceder es al index de nuestra aplicación, es decir, a la página de gestión solo para administradores. Si esto se cumple devolvemos cierto. Si no es así entonces sabemos que el usuario tiene un rol de ‘usuario’, valga la redundancia, y comprobamos si está activo, es decir, si no ha sido borrado temporalmente de la BBDD (esto lo veremos más adelante). Si es así, entonces, y solo entonces, le permitimos la acción ‘usuario’ de nuestro controlador que le redirigirá a la página para los no administradores y devolvemos cierto.

Si ninguna de las anteriores condiciones se cumple entonces devolvemos false.

Veamos nuestro controlador en app/Controller/UsersController. En él figuran los típicos métodos para añadir (add) y modificar (edit) en los que no entraré en detalles. Sobre la propiedad $paginate creo que tampoco hay nada que explicar. Detengámonos sin embargo en la función beforeFilter. Podemos observar que sobreescribe el método de AppController para dejar acceso a ‘add’, ya que sino no sería posible que alguien pudiera registrarse al iniciar la aplicación. Seguidamente hacemos uso de nuestro componente Auth y comprobamos si el usuario (supuestamente ya autorizado) tiene el rol de administrador. Si es así le autorizamos cualquier acción en nuestro Controller. Si, por el contrario, su rol es el de un usuario común, entonces le dejamos solo acceso a las acciones logout y usuario.

 

<?php

//...

public function beforeFilter() {
        parent::beforeFilter();
        $this->Auth->allow('login','add');

        //Si el usuario tiene un rol de admin entonces le dejamos paso a todo.
        //Si no es así se trata de un usuario común y le permitimos solo la acción
        //logout y la correspondiente a usuario (página solo para ellos)
	    if($this->Auth->user('role') === 'admin') {
	        $this->Auth->allow();
	    } elseif ($this->Auth->user('role') === 'usuario') { 
	        $this->Auth->allow('logout', 'usuario');
	    } 
    }
?>

//...

La función usuario, tal y como puede comprobarse, se limita a paginar y renderizar la vista usuario sin más.

 

<?php
//...    

    //Acción para redirigir a los usuarios con rol usuario común
    public function usuario() {
    	$this->paginate = array(
			'limit' => 10,
			'order' => array('User.username' => 'asc' )
		);
    	$users = $this->paginate('User');
		$this->set(compact('users'));
    	$this->render('/Users/usuario');
    }

//...
?>

La función login se encarga, como no podía ser de otra manera, de comprobar si el usuario está autenticado. Si lo está entonces lo redirige a donde le corresponda mediante la instrucción:

$this->redirect($this->Auth->redirectUrl());

El encargado de redirigir correctamente ya lo hemos visto. Se trata de nuestra función isAuthorized del AppController. Ella es realmente quien vigilará dónde y dónde no puede entrar según quién.
Si alguno de los datos del formulario no son correctos o, por alguna razón, alguien intenta acceder por la fuerza modificando la URL, será reconducido de nuevo al formulario de login con un mensaje de aviso.

 

<?php
//...
	public function login() {
		
		//if already logged-in, redirect
		if($this->Session->check('Auth.User')){
			$this->redirect(array('action' => 'index'));		
		}
		
		// if we get the post information, try to authenticate
		if ($this->request->is('post')) {
			if ($this->Auth->login()) {
				$this->Session->setFlash(__('Bienvenido, '. $this->Auth->user('username')));
				$this->redirect($this->Auth->redirectUrl());
			} else {
				$this->Session->setFlash(__('Nombre de usuario o contraseña incorrectos'));
			}
		} 
	}
//...
?>

La función logout no tiene más misterio que usar nuestro componente Auth para dicha operación.

La función index, a la cual, como ya sabemos, solo podrán acceder los administradores, se encarga simplemente de paginar los resultados en la vista correspondiente. Recordemos que en nuestra URL aparecerá ‘gestion’ ya que así lo indicamos en nuestro archivo routes.php

Las funciones delete y activate lo que hacen es cambiar el estado de ‘status’ del usuario en la BBDD en función de si ya estaba o no activado. Se ejecutará una u otra dependiendo del estado en que se encuentre el usuario previamente.
En la vista index.ctp tenemos este trozo de código para controlar esto:

Html->link(“Borrar”, array(‘action’=>’delete’, $user[‘User’][‘id’]));}else{
echo $this->Html->link(“Reactivar”, array(‘action’=>’activate’, $user[‘User’][‘id’]));
}
?>

Si el usuario tiene un estado distinto de 0 entonces es que está activo y mostramos el enlace para la acción delete. Si no es así es que el usuario no está activado y, por lo tanto, mostraremos entonces el enlace para reactivarlo. Simple pero efectivo, ¿no?

 

<?php
//...
    public function delete($id = null) {
		
		if (!$id) {
			$this->Session->setFlash('Es necesario proveer un ID de usuario!!!');
			$this->redirect(array('action'=>'index'));
		}
		
        $this->User->id = $id;
        if (!$this->User->exists()) {
            $this->Session->setFlash('ID inválido');
			$this->redirect(array('action'=>'index'));
        }
        if ($this->User->saveField('status', 0)) {
            $this->Session->setFlash(__('Usuario eliminado'));
            $this->redirect(array('action' => 'index'));
        }
        $this->Session->setFlash(__('Hubo un error y no se pudo eliminar al usuario'));
        $this->redirect(array('action' => 'index'));
    }
	
	public function activate($id = null) {
		
		if (!$id) {
			$this->Session->setFlash('Es necesario proveer un ID de usuario!!!');
			$this->redirect(array('action'=>'index'));
		}
		
        $this->User->id = $id;
        if (!$this->User->exists()) {
            $this->Session->setFlash('ID inválido');
			$this->redirect(array('action'=>'index'));
        }
        if ($this->User->saveField('status', 1)) {
            $this->Session->setFlash(__('Usuario reactivado'));
            $this->redirect(array('action' => 'index'));
        }
        $this->Session->setFlash(__('Hubo un error y no se pudo reactivar al usuario'));
        $this->redirect(array('action' => 'index'));
    }
//...
?>

Nota: Debemos tener en cuenta que un usuario administrador no puede desactivar a otro usuario administrador. Aunque en la base de datos aparezca como status = 0 seguirá pudiendo acceder a cualquier parte de la aplicación. Es lógico. Le hemos dicho a nuestra aplicación que un administrador siempre tiene acceso a todo precisamente por tener esa característica. Claro está que un administrador puede cambiar el rol de otro administrador a ‘usuario’. Entonces sí podrá desactivarlo de la aplicación realmente.

Las vistas index.ctp y usuario.ctp solamente difieren en la parte de contenido que se quiere mostrar según sea el usuario administrador o no.

Vista Index.ctp:

 

<!-- app/View/Users/index.ctp -->
<div class="users form">
<h1 style="font-weight: bold; color: red; text-decoration: underline;"> Página solo para administradores</h1>
<h1>Usuarios:</h1>
<table>
    <thead>
		<tr>
			<th><?php echo $this->Form->checkbox('all', array('name' => 'CheckAll',  'id' => 'CheckAll')); ?></th>
			<th><?php echo $this->Paginator->sort('username', 'Usuario');?>  </th>
			<th><?php echo $this->Paginator->sort('email', 'E-Mail');?></th>
			<th><?php echo $this->Paginator->sort('created', 'Creado');?></th>
			<th><?php echo $this->Paginator->sort('modified','Modificado');?></th>
			<th><?php echo $this->Paginator->sort('role','Rol');?></th>
			<th><?php echo $this->Paginator->sort('status','Estado');?></th>
			<th>Acciones</th>
		</tr>
	</thead>
	<tbody>						
		<?php $count=0; ?>
		<?php foreach($users as $user): ?>				
		<?php $count ++;?>
		<?php if($count % 2): echo '<tr>'; else: echo '<tr class="zebra">' ?>
		<?php endif; ?>
			<td><?php echo $this->Form->checkbox('User.id.'.$user['User']['id']); ?></td>
			<td><?php echo $this->Html->link( $user['User']['username']  ,   array('action'=>'edit', $user['User']['id']),array('escape' => false) );?></td>
			<td style="text-align: center;"><?php echo $user['User']['email']; ?></td>
			<td style="text-align: center;"><?php echo $this->Time->niceShort($user['User']['created']); ?></td>
			<td style="text-align: center;"><?php echo $this->Time->niceShort($user['User']['modified']); ?></td>
			<td style="text-align: center;"><?php echo $user['User']['role']; ?></td>
			<td style="text-align: center;"><?php echo $user['User']['status']; ?></td>
			<td >
			<?php echo $this->Html->link("Editar", array('action'=>'edit', $user['User']['id']) ); ?> | 
			<?php
				if( $user['User']['status'] != 0){ 
					echo $this->Html->link("Borrar", array('action'=>'delete', $user['User']['id']));}else{
					echo $this->Html->link("Reactivar", array('action'=>'activate', $user['User']['id']));
					}
			?>
			</td>
		</tr>
		<?php endforeach; ?>
		<?php unset($user); ?>
	</tbody>
</table>
<?php echo $this->Paginator->prev('<< ' . __('previous', true), array(), null, array('class'=>'disabled'));?>
<?php echo $this->Paginator->numbers(array(   'class' => 'numbers'     ));?>
<?php echo $this->Paginator->next(__('next', true) . ' >>', array(), null, array('class' => 'disabled'));?>
</div>				
<?php echo $this->Html->link( "Crear usuario",   array('action'=>'add'),array('escape' => false) ); ?>
<br/>
<?php 
echo $this->Html->link( "Logout",   array('action'=>'logout') ); 
?>

Vista usuario.ctp

 

<!-- app/View/Users/usuario.ctp -->
<div class="users form">
	<h1 style="font-weight: bold; color: red; text-decoration: underline;"> Página de usuario común</h1>
	<h1>Usuarios:</h1>
	<table>
	    <thead>
			<tr>
				<th><?php echo $this->Form->checkbox('all', array('name' => 'CheckAll',  'id' => 'CheckAll')); ?></th>
				<th><?php echo $this->Paginator->sort('username', 'Usuario');?>  </th>
				<th><?php echo $this->Paginator->sort('email', 'E-Mail');?></th>
				<th><?php echo $this->Paginator->sort('created', 'Creado');?></th>
				<th><?php echo $this->Paginator->sort('modified','Modificado');?></th>
				<th><?php echo $this->Paginator->sort('role','Rol');?></th>
				<th><?php echo $this->Paginator->sort('status','Estado');?></th>
				<th>Actions</th>
			</tr>
		</thead>
		<tbody>						
			<?php $count=0; ?>
			<?php foreach($users as $user): ?>				
			<?php $count ++;?>
			<?php if($count % 2): echo '<tr>'; else: echo '<tr class="zebra">' ?>
			<?php endif; ?>
				<td><?php echo $this->Form->checkbox('User.id.'.$user['User']['id']); ?></td>
				<td><?php echo $this->Html->link( $user['User']['username']  ,   array('action'=>'edit', $user['User']['id']),array('escape' => false) );?></td>
				<td style="text-align: center;"><?php echo $user['User']['email']; ?></td>
				<td style="text-align: center;"><?php echo $this->Time->niceShort($user['User']['created']); ?></td>
				<td style="text-align: center;"><?php echo $this->Time->niceShort($user['User']['modified']); ?></td>
				<td style="text-align: center;"><?php echo $user['User']['role']; ?></td>
				<td style="text-align: center;"><?php echo $user['User']['status']; ?></td>
			</tr>
			<?php endforeach; ?>
			<?php unset($user); ?>
		</tbody>
	</table>
	<?php echo $this->Paginator->prev('<< ' . __('previous', true), array(), null, array('class'=>'disabled'));?>
	<?php echo $this->Paginator->numbers(array(   'class' => 'numbers'     ));?>
	<?php echo $this->Paginator->next(__('next', true) . ' >>', array(), null, array('class' => 'disabled'));?>
</div>				
<br/>
<?php 
echo $this->Html->link( "Logout",   array('action'=>'logout') ); ?>

Por otro lado, para terminar, tenemos el archivo del modelo User.php
En él se declara toda la lógica de validación de los campos de formulario que tenemos en todas nuestras vistas. Por supuesto podemos modificar cualquier criterio de validación para adaptarlo a nuestras necesidades.
Podemos observar que a continuación de estas reglas de validación figuran varias funciones personalizadas que se encargan de comparar la coincidencia de los campos para las contraseñas, de si un usuario o un email ya existen en nuestra BBDD, y de, mediante el método beforeSave(), encriptar a SHA1 nuestras contraseñas antes de guardarlas. Dicho método está en estrecha consonancia con lo que hemos hecho al principio de este tutorial cuando creábamos nuestro usuario administrador en la BBDD.

 

<?php
//...
	/**
	 * Before Save
	 * @param array $options
	 * @return boolean
	 */
	 public function beforeSave($options = array()) {
		// hash our password
		if (isset($this->data[$this->alias]['password'])) {
			$this->data[$this->alias]['password'] = AuthComponent::password($this->data[$this->alias]['password']);
		}
		
		// if we get a new password, hash it
		if (isset($this->data[$this->alias]['password_update'])) {
			$this->data[$this->alias]['password'] = AuthComponent::password($this->data[$this->alias]['password_update']);
		}
	
		// fallback to our parent
		return parent::beforeSave($options);
	}
//...
?>

Pues ya está, hemos terminado. Ahora, basándonos en lo que hemos visto, podríamos añadir todas las acciones y vistas que deseemos y ajustar la autorización según roles en los apartados relevantes para ello. De esta forma, cualquier aplicación que desarrollemos estará protegida ante cualquier intento de acceso inesperado o no autorizado.

El código completo de la aplicación lo podéis descargar de mi repositorio en Github:

Descargar proyecto completo

Espero que os haya gustado y os sea útil en todos vuestros proyectos realizados con este maravilloso framework CakePHP.

Para cualquier duda poneos en contacto conmigo escribiendo a través del blog o enviándome un email por privado a hardwebnet@hardwebnet.es

Un saludo.-

Hoy vamos a ver cómo crear un formulario web que, además de estar validado, tenga la virtud de poder ser ampliado con muy poco esfuerzo y que todas sus funcionalidades sigan en marcha.
Primero aclarar que con HTML5 los campos de tipo entrada pueden validarse automáticamente con la propiedad required (para comprobar que no estén vacíos) o, por ejemplo en el caso de un campo de entrada de email, con type=”email” poder testar que se escribe una dirección de correo correctamente. Esto está bien si supiéramos de antemano que todos nuestros visitantes usan un navegador que soporta HTML5. Pero, como sabemos, esto, desgraciadamente, no es así en más de las ocasiones de las que imaginamos. Conozco gente que todavía usa Internet Explorer 7.
Dicho esto, vamos a confeccionar este formulario a la vieja usanza, es decir, validado con JavaScript. Así que no me enrollo más y comenzamos con el código del HTML.

<!DOCTYPE html>
<html>
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> 
	<title>Ejemplo POO Javascript</title>
	<link rel="stylesheet" type="text/css" href="css/estilo.css"/>
</head>


<body>

	<h1>Ejemplo POO Javascript</h1>

	<fieldset><legend>Formulario POO javascript</legend>
		<form id="formulario" method="POST" action="ejemplo.php">
			Nombre *:
				<br />
				<input class="negro" name="campo" type="text" id="nombre" />
				<br/>
				<br/>
			Email *:
				<br />
				<input  class="negro" name="campo" type="text" id="email"/>
				<br/>
				<br/>
			Teléfono:
				<br />
				<input  class="negro" name="telefono" type="text" id="telefono"/>
				<br/>
				<br/>
			Comentario *:
				<br/>
				<textarea class="negro" name="campo" id="comentario" ></textarea>
				<br/>
				<br/>
			<noscript>
				<input type="submit" id="enviar" value="Enviar"/>
			</noscript>
			<button type="button" id="enviar">Enviar</button>
			<button type="reset" id="borrar">Borrar</button>
		</form>
		<br />
		<p id="mensaje"></p>
		<br />
		<progress id="progreso" max="100" value="0">Progreso</progress>
	</fieldset>
</body>

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

</html>

Como vemos se trata de un formulario que contiene cuatro campos, tres obligatorios y uno opcional. El método action del form apunta a una supuesta página de envío que, en este ejemplo, solo está a modo de test para comprobar que el formulario funciona como debe hacerlo.
Lo realmente importante de aquí son los class, name e id de los campos así como, por supuesto, sus valores. Observamos que todos los campos obligatorios tienen el mismo class y el mismo name. Los id, sin embargo, hacen alusión al objetivo de cada uno de ellos. Bien, queda claro que el llamarles así ha sido de mi elección. Tú puedes llamarlos como quieras, eso sí, respetando que se repita en todos los campos obligatorios así como tenerlo en cuenta a la hora de ser referenciados desde nuestro archivo js, el cual, cargamos al final de nuestro archivo, antes del cierre.
Fuera del formulario tenemos una etiqueta de párrafo vacía con id mensaje que usaremos para ir informando al usuario si algo no está correcto. Además hemos puesto una barra de progreso que irá indicando visualmente qué tanto por ciento correcto se ha rellenado del formulario.
También hemos indicado en el head una hoja de estilos externa que veremos más adelante.
Vamos entonces paso a paso con nuestro archivo script.js, que lo tendremos dentro de una carpeta llamada js tal y como lo hemos indicado en el HTML.

//------añadir listeners sobre el evento onblur de cada campo del formulario
//------con nombre "campo"

var elementos = document.getElementsByName("campo");

// Recorremos todos los elementos
for (var i=0; i < elementos.length; i++) {

      // Añadimos el evento onblur a cada campo del formulario con nombre "campo"
	  //llamando a la funcion crearEvento que los añade dinámicamente
      crearEvento(elementos[i], "blur");
	  
}

//Esta función es llamada desde el bucle for anterior y añade dinámicamente
//los eventos a los elementos que se le pasan como argumento. En este caso todos
//los elementos con nombre "campo"
function crearEvento(elemento, evento) {
	
	if (elemento.addEventListener) {
	elemento.addEventListener(evento, 
		function(){var lanzando = new comprobarCampo(elemento);}, false);	  
	} 
	//-----Para IE
	else {
	elemento.attachEvent("on" + evento, 
		function(){var lanzando = new comprobarCampo(elemento);});
	}
}

Lo primero que hacemos es añadir dinámicamente un evento blur a todos los elementos de nuestro formulario que tengan el nombre campo que, como ya sabemos, son aquellos campos obligatorios de nuestro formulario. Para ello primero los guardamos en un array llamado elementos, y después los recorremos llamando a la función crearEvento pasándole como argumentos tanto el mismo elemento como el evento a escuchar. Esta función es la que se encarga de hacer el trabajo dinámicamente. Observamos que dentro de esta función tenemos un condicional ya que, Internet Explorer, como siempre, hace las cosas distintas a los demás navegadores. Internet Explorer no dispone de un método addEventListener, así que debemos hacerlo con attachEvent.
Con este evento controlaremos cuándo un usuario pasa de un campo a otro dentro del formulario y llamaremos a la función comprobarCampo pasándole como argumento el propio elemento.

//clase que comprueba el contenido del campo individual que se le pasa como
//argumento
var comprobarCampo = function(campo){
		
	this.campo = campo;
		
	if (this.campo.value == ""){
			
		ponerImagen(this.campo, "error");
				
				
	}
		
	else if (this.campo.id == "email" && 
	!(/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(this.campo.value)))
	{
		
		ponerImagen(this.campo, "error");
				
				
	}
		
	else{
		
		ponerImagen(this.campo, "ok");
				
	}

		progreso();
		mensajes();
}

La función comprobará que el campo que se le pasa no esté vacío y que, en caso de que sea el campo que contiene el email, compruebe la sintaxis de la dirección de correo a través de una expresión regular. Vemos que, tanto si se supera la comprobación como si no, se llama a una función ponerImagen pasando como argumentos el elemento campo en cuestión y un string que indica si ha ocurrido un error o todo es ok. Además llamamos consecutivamente a las funciones progreso y mensajes. Veámoslas.

var ponerImagen = function(campo,estado){

		this.campo = campo;
		this.estado = estado;
						
		var imagen = document.createElement("img");
		imagen.className = "image";
		
		if (estado == "error"){
			
			if(this.campo.nextSibling.className == "image"){			
				borrarImagenIndividual(this.campo.nextSibling);
				imagen.src = "img/error.png";
				this.campo.className = "rojo";
				this.campo.style.border = "2px solid red";
				var padre = this.campo.parentNode
				padre.insertBefore(imagen, this.campo.nextSibling);			
			}
			else{
				imagen.src = "img/error.png";
				this.campo.className = "rojo";
				this.campo.style.border = "2px solid red";
				var padre = this.campo.parentNode
				padre.insertBefore(imagen, this.campo.nextSibling);
			}
		
		}
		else{
		
			if(this.campo.nextSibling.className == "image"){			
				borrarImagenIndividual(this.campo.nextSibling);
				imagen.src = "img/ok.png";
				this.campo.className = "verde";
				this.campo.style.border = "2px solid green";
				var padre = this.campo.parentNode
				padre.insertBefore(imagen, this.campo.nextSibling);
			}
			else{
				imagen.src = "img/ok.png";
				this.campo.className = "verde";
				this.campo.style.border = "2px solid green";
				var padre = this.campo.parentNode
				padre.insertBefore(imagen, this.campo.nextSibling);
			}
					
		}

}

Esta función crea dinámicamente una etiqueta image a la que damos un nombre de clase llamado image. Dependiendo de si el string indica error o no, usará un png de 24×24 para mostrarlo al lado del campo input correspondiente. Las imágenes son de tu elección, y están alojadas dentro de una carpeta img como puede verse en la ruta. Para comprobar si el campo disponía anteriormente de una imagen a su lado usamos this.campo.nextSibling.className == “image”. Si ya existía una la borramos con la función borrarImagenIndividual pasándole el target en cuestión. Si no había ninguna entonces le añadimos la nueva que le corresponda.
Usamos un estilo dinámico para el contorno del input. Si hay error rojo, sino verde.
El código para borrarImagenIndividual es simplemente:

var borrarImagenIndividual = function(elemento){
	
	elemento.parentElement.removeChild(elemento);

}

Sigamos con las funciones progreso y mensajes.

var progreso = function(){

	this.elementos = document.getElementsByName("campo");
	this.porcentaje = Math.round((100 / elementos.length) * Math.pow(10, 2)) / Math.pow(10, 2);
	this.barra_progreso = document.getElementById("progreso");
	this.contador = 100;
		
	// Recorremos todos los elementos
	for (var i=0; i < elementos.length; i++) {	
				
		if (elementos[i].value == ""){			
			
			contador -= porcentaje;
				
		}		
		else if (elementos[i].id == "email" 
		&& !(/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(elementos[i].value))){
			
			contador -= porcentaje;
		
		}
		
		barra_progreso.value = contador;

	}	

}

La función progreso calcula dinámicamente el porcentaje de campos a rellenar en base a la cantidad de campos obligatorios del formulario. Ponemos una variable contador a 100, es decir, el total de la barra de progreso. A partir de ahí, recorriendo todos los elementos obligatorios del formulario, irá restando la variable porcentaje a la variable contador para determinar qué tanto por ciento está correctamente cumplimentado. El resultado se lo aplicamos a la barra de progreso que tenemos en nuestro HTML para que lo muestre visualmente.

var mensajes = function(){
	
	this.pMensaje = document.getElementById("mensaje");
	pMensaje.innerHTML = "";
	this.elementos = document.getElementsByName("campo");
	this.mensaje1 = "";
	this.mensaje2 = "";
	
	for (var i=0; i < elementos.length; i++) {		
		
		if (elementos[i].value == ""){			
			
			mensaje1 = "Por favor, rellene todos los campos obligatorios"; 
				
		}		
		else if (elementos[i].id == "email" && 
		!(/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(elementos[i].value))){
			
			mensaje2 = "Por favor, introduzca una dirección de email correcta";
		
		}

	}	

	pMensaje.innerHTML = mensaje1 + "<br />" + mensaje2;
}

La función mensajes se encargará de recorrer todos los elementos obligatorios del formulario y, en caso de error, añadir dinámicamente un texto informativo dentro de la etiqueta párrafo vacía con id mensaje que tenemos en nuestro HTML. Previamente limpiamos el contenido por si ya existiera alguno. Además, como cabe la posibilidad de que hayan varios mensajes distintos, declaramos una variable por cada comprobación y las concatenamos al final para mostrarlo.
Vamos ahora con el botón borrar de nuestro formulario:

//listener sobre el evento onclick del botón borrar formulario

if (document.getElementById("borrar").addEventListener){

	document.getElementById("borrar").addEventListener("click", function evento(){
		
		borrarTodasImagenes("image");
		
		var elementos = document.getElementsByName("campo");
		
		for (var i=0; i < elementos.length; i++) {
			
			elementos[i].className = "negro";
			elementos[i].style.border = "1px solid black";
		}

	} , true);

}
//-----Para IE
else if (document.getElementById("borrar").attachEvent){

	document.getElementById("borrar").attachEvent("onclick", function evento(){
	
		borrarTodasImagenes("image");
	
		var elementos = document.getElementsByName("campo");
		
		for (var i=0; i < elementos.length; i++) {
			
			elementos[i].className = "negro";
			elementos[i].style.border = "1px solid black";
		}
	
	});

}

Añadimos dinámicamente, como ya hicimos anteriormente, el evento click tanto para IE como para el resto de navegadores. Aparte de resetear el formulario, el botón devolverá el contorno negro a los input y se encargará de borrar todas las imágenes que hubieran de error u ok llamando a la función borrarTodasImagenes , pasando como argumento, en este caso, el nombre de clase que queremos eliminar (image).
La función borrarTodasImagenes quedaría así:

var borrarTodasImagenes = function(clase){

	this.clase = clase;

	var elementos = document.getElementsByClassName(this.clase);
	
	// Recorremos todos los elementos
	for(var k = elementos.length-1; k >= 0; --k){
		elementos[k].parentElement.removeChild(elementos[k]);
	}

}

Vamos ya con el botón enviar:

//listener sobre el evento onclick del botón enviar formulario

if (document.getElementById("enviar").addEventListener){

	document.getElementById("enviar").addEventListener("click", function evento(){
	
	var elementos = document.getElementsByName("campo");
	var lanzando = new comprobarTodo(elementos);
	
	

	} , true);

}
//-----Para IE
else if (document.getElementById("enviar").attachEvent){

	document.getElementById("enviar").attachEvent("onclick", function evento(){
	
	
	var elementos = document.getElementsByName("campo");
	var lanzando = new comprobarTodo(elementos);
	
	
	});

}

Igualmente asociamos el evento click al botón como hemos hecho hasta ahora, recuperamos todos los input obligatorios del formulario y se los pasamos como argumento a la función comprobarTodo.

//clase para comprobar todo el formulario al intentar enviarlo
var comprobarTodo = function(elementos){
	
	this.ok = true;
	this.elementos = elementos;
	borrarTodasImagenes("image");
	
	// Recorremos todos los elementos
	for (var i=0; i < elementos.length; i++) {
		
		if (elementos[i].value == ""){
			
			this.ok = false;
			ponerImagen(elementos[i], "error");
		
		
		}
		else if (elementos[i].id == "email" 
		&& !(/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$/.test(elementos[i].value))){
		
			this.ok = false;
			ponerImagen(elementos[i], "error");
		
		}
		else{
		
			ponerImagen(elementos[i], "ok");
		
		}		
	  
	}

	if (this.ok == true){
	
		document.getElementById("formulario").submit();
	
	}

}

Esta última función lo que hace es recorrer todos los input obligatorios y llamar a la función ponerIMagen tanto si está rellenado correctamente como si no. En el caso de que todo esté ok, el formulario será enviado.
Para terminar veamos nuestro CSS:

img {
	padding-left: 10px;
}
input{
	height:30px;
    font-size:14pt;
}

input,textarea{
	padding:10px;
	transition: all 0.15s ease-in-out;
	border-radius:3px;
	border:1px solid rgba(0,0,0,0.2);
}
#mensaje{
	font-weight:bold;
	color:red;
	font-size:10pt;
}
        
.rojo:focus {box-shadow: 0 0 5px rgba(255,0,0,1);border:1px solid rgba(255,0,0,0.8);}
.verde:focus {box-shadow: 0 0 5px rgba(0,255,0,1);border:1px solid rgba(0,255,0,0.8);}
.negro:focus {box-shadow: 0 0 5px rgba(178,166,167,1);border:1px solid rgba(0,0,0,0.8);}


progress[value] {
/* Eliminamos la apariencia por defecto */
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;

/* Quitamos el borde que aparece en Firefox */
border: none;
border-radius:5px;

/* Aplicamos las dimensiones */
width: 250px;
height: 20px;

/* Aplicamos color a la barra */
color: green;
}

/* Compatibilidad de color en Firefox y Chrome */
progress::-moz-progress-bar { background: #00A693; }
progress::-webkit-progress-value { background: #00A693; }

Lo más importante a destacar en este archivo es el aspecto personalizado que le damos a nuestra barra de progreso así como las clases rojo, verde y negro que le van a dar al input un efecto glow muy atractivo cuando el foco esté sobre él. Estas clases, si repasamos nuestro código anterior, veremos que se aplican dinámicamente cuando un campo es erróneo, cuando es ok o cuando se resetea con el botón borrar respectivamente.
Pues ya está. Terminado.
Ahora podemos añadir todos los input obligatorios que queramos a nuestro formulario, teniendo en cuenta fundamentalmente que su name debe ser campo y su class negro. De esta forma, todas las funcionalidades de nuestro código JavaScript serán aplicadas automáticamente sin hacer nada más. Bien es cierto que en este ejemplo solo tenemos en cuenta la comprobación de que el campo no esté vacío y que, en el caso de ser el campo email, sea una dirección de correo correcta, pero añadir nuevas validaciones no es nada complicado dado cómo tenemos estructurado nuestro código. Es obvio que nuestro formulario es quizás demasiado redundante en sus validaciones (imágenes, textos, barra de progreso), pero puedes adaptarlo fácilmente a tus necesidades sin demasiado esfuerzo. Además debemos tener en cuenta que por muy inteligente que sea nuestro JavaScript, debemos validarlo en el PHP destino para evitar posibles inyecciones de código no deseadas. Pero eso lo dejo para vosotros 🙂

Desde hace algún tiempo he comprobado que hay muchos desarrolladores que al tratar de implementar un sistema de autenticación en CakePHP con AuthComponent tienen problemas y no funciona como debería. Sea por motivos de versión del framework o, simplemente, porque no está bien implementado el código, la cuestión es que la aplicación no funciona.
Vamos entonces a ver a continuación cómo programar un sistema de autenticación sin la necesidad del AuthComponent.
Partimos de que tenemos una base de datos conectada correctamente a través de la configuración de nuestro archivo database.php y, además, suponemos que tenemos ya una aplicación en CakePHP que debemos proteger de usuarios no autenticados.
Con lo primero que debemos contar es con una tabla en nuestra base de datos que yo he llamado users. Esta tabla cuenta a su vez con tres campos: id, username y password. El campo id será nuestra clave primaria autoincremental y los otros dos campos de tipo varchar.
Una vez hecho esto, vamos con el código. Abrimos nuestro archivo AppController que se encuentra en la ruta CarpetaAplicacion/app/Controller donde CarpetaAplicacion es el nombre de la carpeta donde guardas tu proyecto. Escribimos el siguiente código:

<?php

App::uses('Controller','Controller');

class AppController extends Controller {

public$components=array('Session');

    // Comprobar si el usuario está logueado
    function authenticate()
    {
        // Comprobamos si la variable de sesión existe. Si es así quiere decir que el usuario se ha logueado
       //correctamente y se le redirecciona a la aplicación.
      //Si no existe se le devuelve al login.
        if(!$this->Session->check('User'))
        {
            $this->redirect(array('controller'=>'users','action'=>'autenticar'));
            exit();
        }

    }
 
    //Autenticación obligatoria si se quiere entrar a cualquier parte de la aplicación
    function afterFilter()
    {
        if($this->action!='autenticar')
        {
            $this->authenticate();
        }
    }
}

La función afterFilter va a permitirnos controlar cuándo un usuario intenta entrar en una zona donde se ejecuta una acción distinta a ‘autenticar’. Inmediatamente llama a la función authenticate que nos va a permitir saber si se ha creado ya una sesión de usuario. Si es así significará que el usuario esta autenticado, con lo que automáticamente será redireccionado al index de la aplicación. Si no es así se le redireccionará de nuevo a la acción autenticar del controlador users (lo veremos a continuación) que, por supuesto, contará con una vista que contendrá el formulario de login. Sigamos.
Ahora vamos a crear, dentro de la carpeta de nuestros modelos, un archivo con el nombre User.php. La ruta será CarpetaAplicacion/app/Model/User.php. Escribimos lo siguiente:

<?php
App::uses('AppModel','Model');
/**
 * User Model
 *
 */
class User extends AppModel {

   var$name='User';

/**
 * Display field
 *
 * @var string
 */

   public$displayField='username';

   var$validate=array(
       'username'=>array(
                    'rule'=>array('minLength',1),
                    'required'=>true,
                    'allowEmpty'=>false,
                    'message'=>'Por favor, introduce un nombre'
                    ),
       'password'=>array(
                    'rule'=>array('minLength',1),
                    'required'=>true,
                    'allowEmpty'=>false,
                    'message'=>'Por favor, introduce la contraseña'
                    ),

   );

}

Lo que hemos hecho es crear nuestra clase modelo que conectará con nuestra tabla users de la BD. También usamos la validación de los campos username y password del formulario que posteriormente crearemos en nuestra View.
Continuamos. Volvemos a nuestra carpeta de controladores y creamos un archivo llamado UsersController.php. La ruta será CarpetaAplicacion/app/Controller/UsersController.php. Aquí escribimos ahora el siguiente código:

<?php
App::uses('AppController','Controller');
/**
 * Users Controller
 *
 * @property User $User
 * @property PaginatorComponent $Paginator
 */
class UsersController extends AppController {

/**
 * Components
 *
 * @var array
 */
    public$components=array('Session');
 
    publicfunction autenticar(){
        // Aunque los campos username y password tienen validación para que no estén vacíos,
// volvemos a asegurarnos con el condicional que el campo username del formulario tiene
        //algún valor
        if(empty($this->data['User']['username'])==false)
        {
    	 //Consulta SQL para buscar al usuario con sus credenciales en la BD
$user=$this->User->find('all',array('conditions'=>array(
                'User.username'=>$this->data['User']['username'],
                'User.password'=>$this->data['User']['password'])));
         //Si se ha encontrado al usuario en la consulta
         if($user!=false)
         {
          //Si existe se redirecciona al usuario a la aplicación creando una variable de sesión 
          $this->Session->setFlash(__('Gracias por loguearse!'));
          $this->Session->write('User',$user);
          $this->Redirect(array('controller'=>'ControllerDeTuAplicacion','action'=>'index'));
          exit();
         }
         else
         {
//Si los datos no son correctos se comunica al usuario y se le devuelve al mismo
          //formulario de login
          $this->Session->setFlash(__('Nombre de usuario y/o password incorrectos'));
          $this->Redirect(array('action'=>'autenticar'));
          exit();
         }
        }
    }
}

Veamos qué hace nuestra clase controller. Es imprescindible, como en el archivo AppController, tener cargado el componente Session. Como podemos ver lo tenemos en nuestra variable pública $components. Lo primero que va a hacer la clase es comprobar que se han pasado datos en el campo username desde el formulario. Es cierto que los campos ya están validados desde nuestro Model, pero siempre es interesante comprobarlo también aquí. Si no es correcto, es decir, si el campo está vacío, entonces se redirecciona de nuevo al mismo formulario. De esto se encarga nuestro Model como sabemos, ya que al no haber ninguna variable Session creada reaccionará en consecuencia. Si, por el contrario, hay datos, se ejecuta una consulta en la BD para comprobar que, efectivamente, el usuario y password existen. Si no existen, entonces se le redireccciona al usuario de nuevo a la acción autenticar que hará que nuestro afterFilter del archivo AppController (¿Recuerdas?) compruebe que exista una variable de sesión. Como no es así se le reenvía automáticamente de nuevo al formulario con un mensaje flash indicando que uno o los dos campos no son correctos. Si existe el usuario, entonces se le redirecciona a la página index de la aplicación, no sin antes crear la variable de sesión que nos permitirá superar con éxito el método authenticate de nuestro AppController que es llamado automáticamente desde nuestro vigilante afterFilter.
Ya lo único que nos queda es crear nuestra View que mostrará el formulario para poder loguearse. Vamos a la ruta CarpetaAplicacion/app/View y creamos la carpeta Users. Despues, dentro de ella, creamos el archivo autenticar.ctp con el siguiente código:

<div>
<?php echo $this->Form->create('User'); ?>
	<fieldset>
		<legend><?php echo __('Login aplicación'); ?></legend>
	<?php
		echo $this->Form->input('username');
		echo $this->Form->input('password');
	?>
	</fieldset>
<?php echo $this->Form->end(__('Continuar')); ?>
</div>

Como se puede observar se trata de un simple formulario muy sencillo donde no creo que haya nada que explicar. Pues ya está, con esto hemos terminado. Ahora tenemos un sistema que permite proteger el acceso a nuestra aplicación de todos aquellos usuarios que, previamente, no estén en la base de datos. Obviamente, para poder probarlo, necesitamos entonces tener al menos un usuario en nuestra tabla users. Este ejemplo no contempla la posibilidad de registrarse pero creo que, con este punto de partida, no es nada complicado implementarlo. Pero eso lo dejo para vosotros 🙂 Un saludo.-

He decidido compartir con vosotros un ejercicio que suelo poner a mis alumnos de JavaScript. Se trata de implementar el típico mensaje de autosugerencia cuando escribimos algo en una entrada de texto destinada a tal fin. Vamos a comenzar creando un archivo HTML que puedes llamar como quieras, aunque en mi caso le he llamado index.html para seguir la costumbre ya que se trata del punto de entrada. Si estás usando un IDE como Aptana, Dreamweaver o cualquier otro, crea un nuevo proyecto y añade este archivo. Si eres de los que prefiere hacer las cosas “a pelo”, pues crea el archivo en el mismo escritorio y solucionado.
Bien, vamos allá. Escribimos en el susodicho archivo lo siguiente:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
 "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">

<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=iso-8859-1" />
<title>Autosugerencia</title>
</head>

<body>

Escribe una palabra: <input type="text" id="palabra"/>

<ul id="suggest"></ul>

</body>

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

</html>

Simplemente, como podemos ver, este archivo contiene un campo de entrada de texto con el id palabra, y una lista desordenada vacía con el id suggest. Al final, antes del cierre de la etiqueta HTML, cargamos el JavaScript que se encargará de todo dentro de una carpeta llamada js. Al archivo le he llamado autosugerencia.js, tú puedes llamarle como quieras, siempre y cuando en el paso siguiente crees ese archivo con el mismo nombre. Con solo esto en el HTML es con lo que vamos a trabajar.
Ahora creamos una carpeta llamada js y dentro un archivo llamado autogerencia.js en mi caso o, como ya he advertido, con el nombre que hayas decidido ponerle en el HTML. Entonces, una vez creado, escribimos el siguiente código:

// JavaScript Document

var palabras = ["enero", "febrero", "marzo", "abril", "mayo", "junio",
				"julio", "agosto", "septiembre", "octubre", "noviembre", "diciembre"];

document.getElementById("palabra").addEventListener("keyup", autosugerir, false);

function autosugerir(){

	var contador = 0;

	var escribiendo = document.getElementById("palabra").value.toLowerCase();

	document.getElementById("suggest").innerHTML="";

		if (escribiendo != ""){

			var caracteres = escribiendo.length;

			for (x=0; x < palabras.length; x++){

				var pArray = new String(palabras[x]);

				var sugerencia = pArray.substring(0,caracteres);

				if (sugerencia == escribiendo){

					contador++;
					var ul = document.getElementById("suggest");
					var new_li = document.createElement('li');
					new_li.className = pArray;
					new_li.style.cursor = "pointer";
					new_li.innerHTML = "Quizás quiso decir " + pArray;
					ul.appendChild(new_li);
					new_li.addEventListener("click", aceptar, false);

				}

			}

			if (contador == 0){

				var ul = document.getElementById("suggest");
				var new_li = document.createElement('li');
				new_li.innerHTML = "No hay sugerencias";
				ul.appendChild(new_li);

			}

		}

		else {

			var ul = document.getElementById("suggest");
			var new_li = document.createElement('li');
			new_li.innerHTML = "No hay sugerencias";
			ul.appendChild(new_li);

		}

}

function aceptar(){

		document.getElementById("suggest").innerHTML="";
		var palabraSeleccionada = this.className;
		var cajaTexto = document.getElementById("palabra");
		cajaTexto.value = palabraSeleccionada;

}

Veamos el código paso a paso.
Lo lógico sería que toda la información que va a ser auto sugerida fuera suministrada por una base de datos pero, para no complicar el ejemplo, vamos a hacerlo desde un array. Eso es precisamente lo que hacemos al comienzo, declarar un array que contiene los meses del año. Acto seguido vinculamos un listener en tiempo de ejecución al campo de texto donde el usuario irá escribiendo. Como vemos, al método addEventListener encargado de ello, se le pasan tres argumentos. El primero es el tipo de evento, el segundo la función destino que se ejecutará cuando el evento se produzca, y el tercero es un argumento booleano opcional. Si es true, todos los eventos del tipo especificado serán lanzados al listener registrado antes de comenzar a ser controlados por algún EventTarget que esté por debajo en el arbol DOM del documento.
Como vemos, el evento que vamos a “escuchar” es el keyup, es decir, cuando el usuario termine de pulsar una tecla. La función de llamada será autosugerir, y el último argumento lo ponemos a false, ya que no queremos que reaccione como hemos explicado hace un momento.
Hasta ahora lo único que tenemos son los datos para sugerir y un escuchador de eventos que estará alerta vigilando cuándo se pulsa una tecla sobre el campo de texto de nuestro HTML.
Bien, veamos ahora qué hace la función autosugerir que es la que va a realizar todo el trabajo. Primero iniciamos una variable contador a cero, después guardamos en la variable escribiendo el resultado de capturar el contenido del campo de texto y convertirlo directamente a minúsculas con la función de JavaScript toLowerCase(). Para asegurarnos que la lista desordenada se encuentra vacía, lo hacemos en la siguiente línea. Ahora, con el condicional, comprobamos que la variable escribiendo no esté vacía, es decir, que haya algún dato en el campo de texto. Si lo hay, guardamos en la variable caracteres la longitud de caracteres, valga la redundancia, de lo que haya escrito en el campo de texto. Seguidamente, con el bucle for, vamos a iterarnos desde cero hasta el total de elementos contenidos en nuestro array. Dentro de este bucle es donde se haya el meollo de la cuestión. Veamos.
Lo primero es guardar en la variable pArray el elemento del array situado en la posición x (variable que, como sabemos, ira aumentando en cada iteración y que comienza por cero). Después, en la variable sugerencia, guardamos el resultado de substraer del contenido de nuestra variable pArray (que, recordemos, contendrá un elemento del array distinto en cada iteración) los caracteres que existan entre cero y el total de caracteres escritos por el usuario hasta ese momento, que ya guardamos anteriormente en la variable caracteres. En el condicional comprobamos si existe alguna coincidencia entre el trozo que hemos obtenido del elemento de nuestro array y lo que haya escrito el usuario. Si es así, aumentamos la variable contador en uno (ya veremos para qué), capturamos la lista desordenada de nuestro HTML en la variable ul y creamos en tiempo de ejecución una etiqueta HTML li en nuestra variable new_li. A continuación le añadimos un nombre de clase que hacemos igual a lo que contenga en ese momento nuestra variable pArray, le añadimos un estilo de cursor y un texto que mostrará la sugerencia obtenida. Para que la etiqueta li creada en tiempo de ejecución pueda aparecer en el HTML, debemos añadirla con appendChild a nuestra lista desordenada. Finalmente, añadimos un listener a la etiqueta li que controlará el evento click sobre cualquiera de ellas y disparará la función aceptar que veremos en un momento. Como ya he comentado, en este bucle for se encuentra la lógica más importante del programa y hará que se muestren todas y cada una de las coincidencias encontradas entre lo que escribamos y nuestro array.
Después del bucle for vemos otro condicional que comprueba si la variable contador es igual a cero. Si recordamos, esta variable la inicializamos así al comienzo de nuestra función. Esto nos sirve para controlar si se ha encontrado alguna sugerencia o no dentro del bucle for, ya que, como hemos visto, la variable va sumando uno cada vez que se encuentra con una coincidencia. Si no es así, la variable seguirá valiendo cero y, por lo tanto, mostramos un mensaje de ello al usuario a través de una etiqueta li creada en tiempo de ejecución y que añadimos a nuestra lista desordenada en el HTML. También, en el else del if contenedor, mostramos lo mismo si la variable escribiendo se encuentra vacía (esto ocurrirá si el usuario escribe algunos caracteres y, posteriormente, decide borrarlos todos). Con esto tenemos visto toda la función autosugerir completa.
Al final del archivo nos encontramos con la función aceptar vinculada a cada una de las etiquetas li que se creen en tiempo de ejecución. Con esto haremos que si un usuario desea aceptar cualquier sugerencia mostrada haciendo clic sobre ella, se muestre automáticamente en el campo de texto. Como vemos, vaciamos la lista desordenada, guardamos en la variable palabraSeleccionada el nombre de la clase que tenga la etiqueta y que, a su vez, será igual al elemento del array, capturamos en la variable cajaTexto el campo de texto de nuestro HTML y, finalmente, le asignamos el valor de la variable palabraSeleccionada.
Y con esto ya lo tenemos.
Ahora podemos probarlo y si, por ejemplo, escribimos solo la letra m en nuestra entrada de texto, veremos como mágicamente aparecen las sugerencias de los meses marzo y mayo.
Como dije al principio, lo suyo es que la fuente de datos se encuentre en una BD y que, además, obtengamos esos datos mediante Ajax. Teniendo este código como punto de partida no es demasiado complicado implementarlo.
Pero eso lo dejo para vosotros 🙂
Espero que os haya gustado y os sea de utilidad.