Feliz año a todos!!!

Quisiera compartir con vosotros un enlace al apartado de descargas de mi página web.

Allí podéis descargar gratuitamente tres juegos de escritorio y una aplicación para dispositivos Android:

  • VBAhorcado: clásico juego del ahorcado, programado íntegramente en Visual Basic .NET
  • JMastermind: emulador del clásico Matermind, programado íntegramente en Java.
  • Gana un millón: emulador del concurso de televisión “Atrapa un millón”, programado íntegramente en Visual Basic .NET
  • Aplicación “Numbering System Converter” para dispositivos Android: se trata de una calculadora que permite convertir cifras entre distintos sistemas numéricos (decimal, binario, octal y hexadecimal). La principal diferencia que presenta frente a otras muchas aplicaciones similares existentes en Google Play, es que permite operar con cifras extremadamente grandes sin ningún tipo de error, bug o desbordamiento. Muy útil para desarrolladores.

Espero que os gusten.

Enlace:

Descargas

Un saludo.

Canal de televisión dedicado a la formación e información sobre seguridad informática.

Altamente recomendable.

Canal en youtube

Página web

Una pequeña muestra:

Enlaces al canal de Chema Alonso.
Para aquellos que no lo conozcáis, decir que se trata de uno de los mejores hackers del mundo. Sí, del mundo.

Este señor es doctor en seguridad informática por la Universidad Politécnica de Madrid. Un jodido crack.

Sus vídeos y conferencias son de lo más instructivo e interesante para aquellos iniciados o profesionales dentro del fascinante mundo de la seguridad informática. Recomendables sin excusa.

Enlace al canal

Página web

Una pequeña muestra:

El polimorfismo está íntimamente ligado a la inyección de dependencias que vimos en el artículo anterior. Es una de las piezas fundamentales dentro de la programación orientada a objetos, así que intentaré poner un ejemplo muy sencillo para tratar de entender este concepto.
Supongamos que nos encontramos con este simple diagrama UML.

Diagrama UML

Diagrama UML


Como podemos observar, la clase Persona hereda de la superclase Humano, así como la clase Perro hereda de la superclase Animal. Cada una, por supuesto, tendría sus propias propiedades y métodos.
Supongamos también que nos encargan implementar el método “correr” para dichas clases. Podemos deducir que tanto la clase Persona como la clase Perro poseen la particularidad de poder correr, pero cada uno, evidentemente, lo hará de una forma diferente.
Lo inmediatamente lógico es pensar que la solución pasaría por heredar de otra superclase que implementara dicho método pero, como sabemos, Java, así como otros muchos lenguajes de programación, soportan herencia simple, con lo que esta idea cae por su propio peso. Además, aunque fuera posible, el acoplamiento entre las clases resultaría demasiado alto, con lo que la posiblidad de mantenibilidad y extensibilidad de la aplicación se vería en entredicho. Por ello, deberemos recurrir al polimorfismo. Veámoslo con un ejemplo práctico.
Lo primero que haremos será declarar una interface Corredor.
 

public interface Corredor {

	void correr();
	
}

 
La interface contiene la función común a nuestras clases Persona y Perro que, a partir de ahora tendrán que implementarla y por la tanto sobreescribir dicho método.
Así quedarían ambas clases:
 

public class Persona implements Corredor {

	@Override
	public void correr() {
		System.out.println("Yo corro como una persona");		
	}

}

 

public class Perro implements Corredor{

	@Override
	public void correr() {
		
		System.out.println("Yo corro como un perro");
		
	}

}

 
Por motivos prácticos, y para una mayor claridad del ejemplo, he obviado en el código que ambas clases heredarían cada una de su respectiva superclase. Ahora es algo irrelevante para la explicación que nos ocupa.
Como podemos apreciar, en el método “correr” de las clases he puesto simplemente una salida por consola. Con esto será suficiente para ver cómo reacciona nuestro programa.
Ahora vamos a crear una nueva clase a la que he llamado Facade (fachada), que será con la que interactue directamente el usuario de la aplicación.
 

public class Facade {

	public void correr (Corredor c){		
		c.correr();		
	}
	
}

 
La clase Facade cuenta con un método que espera recibir un objeto de tipo Corredor, es decir, que implemente dicha interface. A continuación llama al método correr de dicho objeto, pero ¿a qué método llamará? ¿Cómo sabrá hacía dónde dirigirse?. Pues aquí está lo realmente fascinante del polimorfismo. Como tanto la clase Persona como la clase Perro implementan la interface Corredor, al pasar una instancia de cualquiera de ellas como argumento, reaccionará llamando directamente al método “correr” de la clase correspondiente, sin importarle realmente de qué objeto se trata. Él solo sabe que debe llamar al método correr del objeto Corredor que le ha sido pasado.
ATENCIÓN: Aunque he llamado “correr” al método de esta clase Facade, se podría haber llamado de cualquier otra forma. En este caso no tiene nada que ver con la interface que hemos implementado anteriormente.
Por último, para terminar nuestro programa, nos falta crear nuestro punto de entrada.
 

public class Sistema {

	public static void main(String[] args) {
		
		Persona persona = new Persona();
		Perro perro = new Perro();
		
		Facade fachada = new Facade();
		fachada.correr(persona);
		fachada.correr(perro);
		
	}

}

 
Instanciamos un objeto de cada una de nuestras clases Persona y Perro. A continuación instanciamos la clase Facade, y le pasamos cada una de las instancias anteriores como argumento de su método “correr”, que como ya sabemos está esperando un objeto que implemente la interfaz Corredor.
Compilamos y tenemos la siguiente salida:
 

salida por consola

salida por consola


 
Vemos que, efectivamente, ha respondido el método “correr” de cada una de las clases. De esta forma, si mañana nos piden implementar una nueva clase Gato, no tendremos que tocar absolutamente nada del código que ya tenemos hecho. El sistema seguirá funcionando exactamente igual. Tan solo crearemos nuestra clase Gato implementando la interfaz Corredor, y declararemos aquello que nuestro Gato queramos que haga en su método “correr”. Así de sencillo.
Espero que el ejemplo, aunque muy simple, haya dejado claro las virtudes y beneficios de usar polimorfismo en nuestras aplicaciones.
Un saludo.-

Diseño de aplicaciones

Inyección de dependencias

Uno de los pilares de la programación orientada a objetos es la búsqueda del bajo acomplamiento entre las clases. Con esto entendemos que cualquier dependencia que una clase tenga sobre otra, en un futuro va a resultar en un problema a la hora de la escalabilidad y extensibilidad de la aplicación.
Para resolver dicho problema contamos con el patrón llamado inyección de dependencias (Dependency injection en inglés). En proyectos pequeños es posible que el uso de dicho patrón no sea necesario, pero en el caso de proyectos medianamente grandes, recurrir a él va a ser imprescindible. El problema en estos casos no es tanto saber qué es la inyección de dependencias sino cuándo, cómo y dónde implementarlo.
Así pues, vamos a ver un ejemplo muy sencillo donde voy a tratar de explicarlo.
Supongamos que tenemos un hardware cualquiera, el cual espera de un componente, desarrollado por un proveedor cualquiera, para poder funcionar. Imaginemos que, en un principio, disponemos de dos componentes distintos, los cuales trabajan de forma diferente para proveer al hardware de diversas funcionalidades. Con este punto de partida, lo primero que se nos ocurriría sería crear una clase Hadware que en su interior instanciara un objeto de un componente u otro según el caso. Hasta ahí bien. Pero, ¿qué ocurriría si mañana disponemos de mil componentes? Si además nos encontramos con que los componentes pueden cambiar la forma de hacer las cosas, la mantenibilidad se vería en entredicho. Tendríamos que modificar código practicamente en todo el programa, incluída por supuesto la clase Hardware. Queda claro que no es viable, que el diseño de la aplicación no es ni mucho menos elegante, y que no se respetan los principios de la programación orientada a objetos.
Dicho esto veamos cómo acometer este problema usando, evidentemente, la inyección de dependencias.
Primero crearemos una interface.
 

public interface Hardware {
	void Initialization();
	void Shutdown();
	void Prepare();
	void DoIt();
}

 
Esta interface contiene los cuatro métodos que el Hardware ha declarado para poder funcionar. Dichos métodos tendrán que ser implementados obligatoriamente por cualquier componente con el que vaya a interactuar. Crearemos ahora esos componentes que implementarán la interface.
 

public class ComponenteA implements Hardware{
	
	public void Initialization() {
		
		System.out.println("Iniciando... (componente A)");
		
	}

	public void Shutdown() {
		
		System.out.println("Apagando... (componente A)");
		
	}

	public void Prepare() {
		
		System.out.println("Preparando... (componente A)");
		
	}

	public void DoIt() {
		
		System.out.println("Trabajando... (componente A)");
		
	}

}

 

public class ComponenteB implements Hardware{

	public void Initialization() {
		
		System.out.println("Iniciando... (componente B)");
		
	}

	public void Shutdown() {
		
		System.out.println("Apagando... (componente B)");
		
	}

	public void Prepare() {
		
		System.out.println("Preparando... (componente B)");
		
	}

	public void DoIt() {
		
		System.out.println("Trabajando... (componente B)");
		
	}

}

 
Como podemos ver, he puesto una simple salida por consola en cada método para poder probar su funcionamiento.
Ahora, para que el cliente no acceda directamente a la lógica de negocio, crearemos una clase “fachada” usando el patrón Facade. Esto hará que el cliente interactue con él, ocultando toda la “sala de máquinas” de nuestro programa.
 

public class ProductFacade {

	private Hardware SomeHardware;
	
	public ProductFacade(Hardware SomeHardware)
	{
		this.setSomeHardware(SomeHardware);
	}

	public Hardware getSomeHardware() {
		return SomeHardware;
	}

	public void setSomeHardware(Hardware someHardware) {
		SomeHardware = someHardware;
	}	
	
}

 
Veamos esta última clase con detenimiento.
Declaramos una variable de tipo Hardware privada (encapsulamiento ante todo). Seguidamente tenemos el constructor que espera recibir un objeto de tipo Hardware, es decir, de cualquier clase que implemente la interface Hardware que hemos declarado al principio. Seguidamente terminamos con los getter y setter correspondientes, que nos permitirán acceder al objeto en cuestión y a todos sus métodos.
Ya solo nos queda implementar el punto de entrada, aquel que ejecutará el cliente.
 

public class Main {

	public static void main(String[] args) {

		Hardware componentA = new ComponenteA();
		ProductFacade product01 = new ProductFacade(componentA);
		product01.getSomeHardware().Initialization();
		product01.getSomeHardware().Prepare();
		product01.getSomeHardware().DoIt();
		product01.getSomeHardware().Shutdown();		
		
		Hardware componentB = new ComponenteB();
		ProductFacade product02 = new ProductFacade(componentB);
		product02.getSomeHardware().Initialization();
		product02.getSomeHardware().Prepare();
		product02.getSomeHardware().DoIt();
		product02.getSomeHardware().Shutdown();	

	}

}

 
Como podemos ver, instanciamos un objeto del componenteA y posteriormente se lo pasamos directamente al constructor de la instanciación de nuestra clase “fachada”. A partir de ahí ya podremos acceder a los métodos del componente inyectado a través de su getter.
A efectos prácticos, he instanciado los dos componentes consecutivamente para que podamos comprobar por consola cómo se comporta nuestro programa en cada momento.
Si ejecutamos el proyecto, la salida será la siguiente:
 

Salida por consola

Salida por consola


 
Las ventajas de usar este patrón son evidentes. A partir de ahora, si tenemos más componentes, no nos importará. Tan solo deberemos inyectar en cada momento el componente deseado a la clase “fachada” del método main. En el caso de que algún componente cambiara su funcionalidad, solo deberiamos efectuar los ajustes correspondientes en el método o métodos de su clase, sin tocar nada de código exterior a ella.
Mínimo esfuerzo en una aplicación escalable y mantenible. Eso es lo bueno de trabajar correctamente con la programación orientada a objetos.
Un saludo.-

A continuación vamos a ver, mediante una sencilla aplicación, un buen ejemplo de las posibilidades que puede proporcionarnos esta magnífica funcionalidad que trae consigo el estándar HTML5. Dicha aplicación consistirá en la posibilidad de votar nuestras redes sociales favoritas, haciendo uso, claro está, de la operación arrastrar y soltar.
Empecemos con el código html:

<body>
	
	<div id="header">
		<img id="cabecera" src="img/cabecera.png" />
		<h2>Vota tus aplicaciones favoritas</h2>
	</div>
		
	<hr>
		
	<div id="containerVotacion">
			
		<div id="Muy_buena" class="destino verde" ondrop="drop(event)" ondragover="allowDrop(event)" ondragleave="dragLeave(event)">
			<p>Muy buena</p>
		</div>
				
		<div id="Buena" class="destino amarillo" ondrop="drop(event)" ondragover="allowDrop(event)" ondragleave="dragLeave(event)">
			<p>Buena</p>
		</div>
				
		<div id="Regular" class="destino rojo" ondrop="drop(event)" ondragover="allowDrop(event)"	ondragleave="dragLeave(event)">
			<p>Regular</p>
		</div>
			
	</div>
		
	<div id="containerElementos" ondrop="drop(event)" ondragover="allowDrop(event)"	ondragleave="dragLeave(event)">
			
		<img id="facebook" src="img/facebook.png" draggable="true" ondragstart="drag(event)" ondragend="dragEnd(event)" width="64" height="64"/>
		
		<img id="twitter" src="img/twitter.png" draggable="true" ondragstart="drag(event)" ondragend="dragEnd(event)" width="64" height="64"/>
				
		<img id="linkedin" src="img/linkedin.png" draggable="true"	ondragstart="drag(event)" ondragend="dragEnd(event)" width="64" height="64"/>
			
	</div>
		
	<footer id="pie">
		<button id="resultados" onclick="obtenerResultados();">Ver resultados</button>
	</footer>
		
</body>

 

Aquí podemos observar tan solo la parte del “body” del html, para no distraernos con otras líneas de código que ahora no importan.
La imagen dentro del primer div simplemente aparece a modo decorativo. Todos los recursos que se emplean aquí van a estar disponibles a través del enlace a mi repositorio Github que os dejaré al final del artículo.
Vamos a fijarnos en lo importante. Hay dos div contenedores que van a diferenciar la zona disponible para votar, de la que contendrá los iconos de las redes sociales más relevantes, y que estarán disponibles parar ser arrastrados y soltados en la zona de votación que deseemos.
Así pues queda claro que el div con id containerVotación integra a los tres div que equivalen a las votaciones “muy buena”, “buena” o “regular”. Podemos observar que todos estos div tienen tres eventos in line. Veamos qué hacen:

  • ondrop: Controla si en el div se han “soltado” elementos con la propiedad draggable (que pueden ser arrastrados) sobre él. Esto, evidentemente, determina que se trata de una zona drop válida que permite alojar esos elementos provenientes de un drag (“arrastre”). Al hacerlo llamará a la función drop, pasándole como argumento el propio evento.
  • ondragover: Controla que en el div se están arrastrando elementos con la propiedad draggable hacia una zona que permita “soltar” esos elementos. Al hacerlo llamará a la función allowDrop, pasándole como argumento el propio evento.
  • ondragleave: Controla el momento en que un elemento draggable abandona la zona drop donde se encuentra.

El div con id containerElementos dispone de los mismos eventos arriba mencionados. Esto permite que de él se puedan tanto arrastrar como soltar elementos, lo mismo que ocurre con los div contenidos en el div contenedor containerVotación de forma individual.

En su interior cuenta con tres imágenes que se encargan de mostrar los logos de las tres redes sociales más relevantes del momento, las cuales tienen un id correspondiente a la red social en cuestión, y cuyos eventos pasamos a comentar:

  • draggable: Permite que el elemento puede ser “arrastrado” hacia una zona drop válida. Como vemos, se trata de una propiedad que únicamente hay que igualar a true para que funcione sobre el elemento donde estemos declarándola.
  • ondragstart: Este evento se dispara cuando el elemento comienza a ser “arrastrado” desde una zona drop válida.
  • ondragend: El evento se dispara cuando el usuario finaliza el “arrastrado” del elemento sobre una zona drop válida.

Con todo esto, y resumiendo, lo que tenemos es una zona que contiene elementos “arrastrables” y que, a su vez, permite depositar elementos “arrastrados” a nivel general. Lógicamente se trata del div containerElementos y sus img correspondientes. Por otro lado tenemos la zona que contiene a su vez zonas individuales que permiten igualmente depositar elementos “arrastrados” o que se “arrastren” a partir de ellos. Se trara del div containerVotacion y sus correspondientes div con id igual al tipo de nota que queremos otorgar al elemento draggable en cuestión.

Para finalizar, tenemos en el pie de página un botón que será el encargado de obtener los resultados de la votación.

Veamos ahora el código javascript encargado de toda la funcionalidad:

var destino;
var arrastrando;
var ancho;
var alto;
var resultado=new Array();
var resultadoFinal=new Array();

function allowDrop(ev)
{
ev.preventDefault();
destino = devuelveElemento(ev);
destino.style.opacity="0.4";
}

function drag(ev)
{
arrastrando = devuelveElemento(ev);
ancho = arrastrando.width;
alto = arrastrando.height;
arrastrando.style.width=ancho/2+"px";
arrastrando.style.height=alto/2+"px";
var dragIcon = document.createElement('img');
dragIcon.src = 'img/vote.png';
dragIcon.width = 100;
ev.dataTransfer.setDragImage(dragIcon, -10, -10);
ev.dataTransfer.setData("Text", ev.target.id);
}

function dragLeave(ev){
destino.style.opacity="1";
}

function dragEnd(ev){
arrastrando.style.width=ancho+"px";
arrastrando.style.height=alto+"px";
}

function drop(ev)
{
var contenedorUsado = destino.id;
var elementoVotado = arrastrando.id;
resultados(contenedorUsado,elementoVotado);
ev.preventDefault();
destino.style.opacity="1";
var data = ev.dataTransfer.getData("Text");
ev.target.appendChild(document.getElementById(data));
}

function devuelveElemento(ev){
var elemento = document.getElementById(ev.target.id);
return elemento;
}

function resultados(voto,votado){
resultado.push(voto+" "+votado);
}

function obtenerResultados(){

resultadoFinal = sanearResultados();

var ancho= 700;
var alto = 500;
var posicion_x;
var posicion_y;
posicion_x=(screen.width/2)-(ancho/2);
posicion_y=(screen.height/2)-(alto/2);
MiVentana = window.open("","Resultados Drag&Drop HTML5","toolbar=no, location=no, status=no, resizable=no, top="+ posicion_y +", left="+ posicion_x +", width=700, height=500");
MiVentana.document.write('<!DOCTYPE html>\n<head>\n<title>Resultados Drag&Drop HTML5</title>');
MiVentana.document.write('\n<meta charset="utf-8"/>');
MiVentana.document.write('\n<link href="http://fonts.googleapis.com/css?family=Yanone+Kaffeesatz:400,300,200,700&subset=latin,latin-ext" rel="stylesheet" type="text/css">');
MiVentana.document.write('\n<link rel="stylesheet" type="text/css" href="css/estilo.css"/>');
MiVentana.document.write('\n</head>\n<body>\n<div id="header">');
MiVentana.document.write('\n<img id="cabecera" src="img/cabecera.png" />');
MiVentana.document.write('\n<h2>Resultados</h2>\n');
for (var z=0;z<resultadoFinal.length;z++){
MiVentana.document.write(resultadoFinal[z]+"<br>\n");
}
MiVentana.document.write('<br>');
MiVentana.document.write('\n<input type="button" value="Cerrar" onclick="window.close()">');
MiVentana.document.write('\n</div>\n</body>\n</html>');
MiVentana.document.close();
}

function sanearResultados(){

var social = [];
var controlFacebook=0;
var controlTwitter=0;
var controlLinkedin=0;

for (var j=1;j<=resultado.length;j++){

var elemento = resultado[resultado.length-j].toString();

if (elemento.indexOf("Elementos")>0){
continue;
}

if (elemento.indexOf("facebook")>0 && controlFacebook==0){
social.push(elemento);
controlFacebook = 1;
}
if (elemento.indexOf("twitter")>0 && controlTwitter==0){
social.push(elemento);
controlTwitter = 1;
}
if (elemento.indexOf("linkedin")>0 && controlLinkedin==0){
social.push(elemento);
controlLinkedin = 1;
}

}

return social;
}

 
Bien, vayamos por partes.
Al principio declaramos unas cuantas variables a nivel global. Dos de ellas arrays.
Todas las funciones que se llaman cuando se desencadena algún evento ya visto, tienen como parámetro ev, es decir, el propio origen del evento mismo.

La función allowDrop, que se dispara cuando un elemento draggable pasa por una zona drop válida, lo que hace es guardar en la variable destino cuál es el elemento (en nuestro caso un div) sobre el que se está arrastrando un elemento draggable (en nuestro caso los img). Para ello se llama a la función devuelveElemento pasándole a su vez el origen del evento recibido. También damos una opacidad para que el contenedor drop disponible sobre el que se pasa, nos informe de que está listo para recibir el elemento draggable.

La función drag se dispara cuando un elemento draggable comienza a ser “arrastrado”. Para conocer de cúal se trata aprovechamos de nuevo la función devuelveElemento y lo guardamos en la variable creada para ello. Seguidamente hayamos su alto y ancho para asignarle la mitad de ambos durante el efecto de “arrastrado”. Además, creamos dinámicamente un pequeño icono bajo el elemento “arrastrado” para indicar al usuario que se está llevando a cabo el proceso. Para añadirlo definitivamente a nuestro elemento en proceso de “arrastrado”, debemos llamar al método dataTransfer, primero seteando su posición respecto al elemento “arrastrado” y después haciendo lo propio con el tipo de dato.

La función dragLeave se ejecuta cuando un elemento draggable deja de estar sobre una zona drop válida. En ese caso lo único que hacemos es eliminar la opacidad que tenga el contenedor drop sobre el que se acaba de terminar de pasar.

La función dragEnd devuelve el tamaño completo al elemento “arrastrado” cuando éste termina de “aterrizar” sobre una zona drop válida.

La función drop es llamada cuando una zona drop válida termina de recibir un elemento draggable. En ese momento obtenemos qué zona drop de nuestra aplicación está recibiendo el elemento y cuál es ese elemento (en nuestro caso el img de alguna red social). Esto ya lo sabemos gracias a las variables globales destino y arrastrando, que ya han guardado anteriormente esa información durante los pasos previos. Una vez los conocemos, llamamos a la función resultados pasándole esos datos. Lo que hace es añadirlos a un array, tal y como podemos comprobar un poco más abajo. Además eliminamos la opacidad de la zona drop y quitamos el icono que habíamos añadido para indicar que el elemento draggable estaba siendo arrastrado.
Finalmente, las funciones que nos quedan se van a encargar de obtener los resultados finales de la votación cuando pulsemos sobre el botón correspondiente.

La funcion obtenerResultados, que es la que se ejecuta al pulsar dicho botón, lo primero que hace es llamar al método sanearResultados. Dado que el usuario puede variar las veces que desee el elemento a votar por diferentes opciones drop, debemos tener en cuenta que, cada vez que lo haga, se registrará la votación dentro del array resultado visto anteriormente. Por esa razón debemos obtener siempre la última votación que haya recibido cada red social, ya que, como sabemos, un array siempre va guardando la información al final del mismo, a menos que no se le indique lo contrario. Así pues podemos ver que el bucle for comienza a recorrer el array por el final y va extrayendo la primera coincidencia que encuentra según el nombre de la red social. Todo ello se guarda en un nuevo array limpio de polvo y paja, y se devuelve como resultado.
Cuando se ha recuperado ese resultado desde la función obtenerResultados donde nos encontrábamos, lo único que hacemos es presentar una ventana flotante que mostrará finalmente, ahora sí, el resultado final, recorriendo para ello el nuevo array saneado que acabamos de recuperar.

Y eso es todo.

Quiero disculparme si la explicación es algo confusa, pero la verdad es que es algo complicado intentar plasmar a fondo línea a línea, qué es lo que hace cada una, sin ser algo redundante.
Recomiendo encarecidamente descargar la aplicación, probarla y, a continuación, con el código delante, volver a leer el artículo.
La aplicación completa cuenta con todos los recursos, fuentes y estilos, que terminan por otorgarle un buen aspecto final.

Descarga desde Github

Espero que este articulo os sirva para comprender un poco mejor cómo funciona esta maravillosa utilidad de HTML5 y, sobre todo, vislumbrar las infinitas posibilidades que nos puede ofrecer.
Gracias y un saludo.-

Capturas:

 

Drag&Drop

 

Drag&Drop

 

Drag&Drop

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.-