CGI::Aplication el fino arte de hacer web perl-style


Perl es una maravilla en cuanto a robustes y disponibilidad de módulos para hacer lo que sea, no solo es posible analizar logs al más puro estilo geek, sino que se pueden hacer cosas como “parsear” un archivo de excel para importarlo a una base de datos, o crear sitios web con todas las características que necesitan.

Para crear aplicaciones Web, Perl tiene un monton de opciones, desde hacer un CGI a mano que haga toda la magia de entender los headers, extraer los argumentos de GET y POST, y desplegar HTML intercalado en el código, al más puro estilo de PHP (porque lo heredó de Perl), sino que existen muchos módulos (CPAN) que nos ayudan a hacer las cosas de manera más ordenada y mantenible.

En esta nota quiero abordar la utilización de un módulo muy robusto, casi podríamos decir que es realmente un Framework de desarrollo web, aunque a mi no me gustan los frameworks,  uno no deja de admirar la simplicidad con la que se puede crear una simple paginita web siguiendo bastante fielmente algunos de las más famosas “mejores prácticas”, como el MVC, el uso de capa de abstraccion en la base de datos, y sobre todo el crear código seguro contra ataques de injeccion SQL y XSS.

Para empezar vamos a plantear el clásico ejemplo de crear un blog muy sencillo, que tenga autenticación, guarde en mysql, y nos permita mandar noticias y poner comentarios en ellas, nada exótico.

Empezemos por crear una base de datos:
[codesyntax lang=’sql’ container=’div’]mysql> create database perlblog;
Query OK, 1 row affected (0.09 sec)

mysql> grant all privileges on perlblog.* to blog@localhost identified by `20y1337` ;
Query OK, 0 rows affected (0.27 sec)
mysql>[/codesyntax]
Necesitamos varias columnas, una para los datos del usuario (user), otra para las noticias (news), y otra para comentarios (comments), luego veremos si se requiere algo extra.
[codesyntax lang=’sql’ container=’div’]mysql> create table user (id int unsigned not null auto_increment, login varchar(50) unique, password char(32), name varchar(150), created datetime, last_log datetime, primary key (id));
Query OK, 0 rows affected (0.21 sec)

mysql> create table news (id int unsigned not null auto_increment, title varchar(254), summary tinytext , news text, posted datetime, last_edit datetime, user_id int unsigned not null default 1, primary key (id));
Query OK, 0 rows affected (0.43 sec)

mysql> create table comment (id int unsigned not null auto_increment, title varchar(254), comment text, posted datetime, last_edit datetime, user_id int unsigned not null default 0, name varchar(255), email varchar(255), primary key (id));
Query OK, 0 rows affected (0.09 sec)
[/codesyntax]

El la columna del usuario al menos nos hace falta su correo electrónico, agreguémoslo:
[codesyntax lang=’sql’ container=’div’]mysql> alter table user add column email varchar(255) after password;
Query OK, 0 rows affected (0.18 sec)
Records: 0  Duplicates: 0  Warnings: 0
[/codesyntax]

Ya estamos listos para empezar, posiblemente lo primero que queremos hacer es poder insertar una noticia, así que vamos a crear una paginita de captura. Para esto necesitamos crear el ambiente inicial:

[codesyntax lang=’perl’ container=’pre’]package Blog;
use strict;
use warnings;
use base ‘CGI::Application’;
use CGI::Application::Plugin::AutoRunmode;
use CGI::Application::Plugin::DBH (qw/dbh_config dbh/);

sub cgiapp_init {
my $self = shift;
$self->dbh_config(“dbi:mysql:perlblog”, “blog”, `20y1337`);
}
sub news_list : StartRunmode {
my $self = shift;
my $q = $self->query();

my $sth = $self->dbh->prepare(“SELECT * FROM news ORDER BY posted DESC”);
$sth->execute();
my @rows;
while ( my $row = $sth->fetchrow_hashref ) {
push(@rows, $row);
}
return @rows;
}
1;
[/codesyntax]

Con esto estamos armando el ambiente más mínimo posible, estamos creando un paquete que va a contener todas las rutinas que requiere la aplicación, y estamos usando uso de CGI::Application que será nuestra base para hacer prácticamente toda la programación.

La subrutina “cgiapp_init” es la encargada de inicializar todos los requerimientos, que por lo pronto solamente es la conección a la base de datos, para lo cual estamos usando un plugin para CGI::Application que se llama CGI::Application::Plugin::DBH, en síntesis es meterle la capa DBI directamente nuestro “framework”.

Para poder ejecutar lo que llevamos de aplicación tenemos que crear un script que ejecuta el paquete, lo cual es bastante sencillo, y no volveremos a tocarlo una vez escrito lo siguiente a “index.pl”:

[codesyntax lang=’perl’]#!/opt/local/bin/perl -w
use strict;
use warnings;
use Blog;
my $app = Blog->new();
$app->run();
[/codesyntax]

Noten que estoy usando un path para perl medio extraño, el 99% de los casos se usará el estandar “#!/usr/bin/perl -w”, yo uso ese pq me gusta más trabajar con el perl instalado por macports, el perl nativo de Mac OS X está medio chafita en algunos aspectos y prefiero algo más estandar, con suerte casi nadie que lea este post va a desarrollar sobre Mac 🙂

Una vez creados estos 2 archivos en el directorio de los “cgi-bin”, entonces visitamos en el navegador algo como http://localhost/cgi-bin/index.pl y vamos a ver un número entero, posiblemente cero, que será el número de renglones que existan en la columna “news” de la base de datos.

Con esto estamos aprendiendo un truco útil cuando estemos usando CGI::Application, siempre que quieras debugear algo, puedes poner un return de la variable que sea interesante, funciona como el típico “print” para debuguear, la diferencia es que el return termina en ese punto la ejecución de la rutina, y nos devuelve a la pantalla del navegador solamente el valor de dicha variable. Podríamos haber puesto “return ‘hola mundo'”, pero no sería tan divertido.
Ahora insertemos un renglon en “news”, ejecutando:

[codesyntax lang=’sql’ container=’div’]insert into news values (null, ‘titulo 1’, ‘estes es un resumen’, ‘y esta es la noticia extendida’, now(), now(), 1);
[/codesyntax]

Si recargamos la página en el navegador veremos un “1”, nada excitante, pero podemos meterle más emoción retornando “$rows[0]{‘title’}” en vez de “@rows”, con lo cual ahora veremos un excitante “titulo 1” !!! solo estamos demostrandonos a nosotros mismos que “podemos interactuar con una base de datos de verdad”.

Ahora vamos al paso de presentar esa información en un formato decenton, vamos a crear una plantilla en HTML muy sencilla, solo para llenar el requisito, una vez entendido todo el proceso, será trivial ir a buscar un diseño gratuito y adaptarlo.

[codesyntax lang=’html4strict’]<?xml version=”1.0″ encoding=”UTF-8″?>
<!DOCTYPE html PUBLIC “-//W3C//DTD XHTML 1.1//EN”
“http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd”>
<html xmlns=”http://www.w3.org/1999/xhtml” xml:lang=”en” >
<head>
<title>[% title %]</title>
</head>
<body>
<p>[% title %]<</p>
</body>
</html>
[/codesyntax]

Tenemos un documento básico de XHTML, pero noten que el título tiene algo extraño ([% title %]), es la presentación del contenido de una variable (title) en el “lenguaje” de Template Toolkit (TT), que será lo que utilizaremos para las plantillas.

El código de nuestro paquete necesita unos cambios ligeros, necesitamos inicializar el sistema de plantillas, y necesitamos decirle a “news_list” que interprete la plantilla “index.html”, vamos a agregar a la rutina “cgiapp_init” esto:

[codesyntax lang=’perl’ container=’div’]$self->tt_config(
TEMPLATE_OPTIONS => {
INCLUDE_PATH => ‘/Users/max/templates/’,
},
);
[/codesyntax]

Y en “news_list” vamos a sustituir la última linea, la del return, por esto:

[codesyntax lang=’perl’ container=’div’]my $vars = {
title => ‘Soy el blog m&aacute;s 1337’,
rows => \@rows,
};
#return $rows[0]{‘title’};
return $self->tt_process(‘index.html’, $vars);
[/codesyntax]

Con esto estamos mandandole a la plantilla 2 variables, aunque solo está utilizando una de ellas. Puedes darle “recargar”, “reload”, o como diga tu navegador :).

Ahora vamos a usar la segunda variable, que en realidad es una ·$&%$&/· referencia a un arreglo que tiene por elementos hashes (o arreglos asociativos), pa su madre no ?? como nota personal, odio las referencias en perl, siempre me confundo en que es una referencia y que no, pero bueno, sigamos.

Queremos cliclar sobre las noticias, pero solo hemos insertado una, inserta unas 2 o 3 más, incluso pueden usar exactamente los mismos datos que la primera, pero a veces ayuda tener una pista de que algo diferente se está haciendo, puedes cambiar el 1 por 2 y luego 3 y luego 4 en el título, y ya con eso tenemos.

Lo que haremos en la plantilla es agregar un ciclo sobre el arreglo “@rows”, no es nada complicado, queda más o menos así:

[codesyntax lang=’html4strict’]

…..

<p>[% title %]</p>
[% FOREACH row IN rows %]
<p>[% row.title %] ([% row.posted %])</p>
<p>[% row.summary %]</p>
<p><a href=”index.pl?rm=view&id=[% row.id%]”>Ver noticia</a></p>

….

[/codesyntax]

Con esto ya tenemos el listado de las noticias, ahora falta que podamos ver la noticia en extenso, y agregarle comentarios. Con el siguiente paso vamos a aprender como se definen más actividades a través de un solo módulo.

En el último cambio a la plantilla estamos agregando una liga, la cual contiene 2 variables con sus respectivos valores, “rm”, e “id”, el primero contiene el nombre de la rutina que se desea ejecutar en el módulo “Blog”, en este caso es “view”, y la segunda variable es el dato extra que necesitamos para saber cual noticia desean ver.

Lo que necesitamos ahora es tener la rutina “view”, donde vamos a seleccionar de la base de datos la noticia con el “id” que el usuario desea ver, y la vamos a mostrar en su respectiva plantilla.

La vista es sencilla, solamente tenemos que seleccionar el ID de la noticia que se desea ver, y mandarla a la plantilla:

[codesyntax lang=’perl’ container=’pre’]sub view : Runmode {
my $self = shift;
my $q = $self->query();
my $sth = $self->dbh->prepare(“SELECT * FROM news where id = ?”);
$sth->execute($q->param(‘id’));
my $row = $sth->fetchrow_hashref;
my $vars = {
title => $row->{‘title’},
row => $row,
};
return $self->tt_process(‘view.html’, $vars);
}
[/codesyntax]
Podemos ver que la vista se define solamente con “Runmode”, esto quiere decir que este módulo solo se va a ejecutar cuando sea llamado explícitamente (rm=view en GET). Y la plantilla puede ser tan sencilla como esto:
[codesyntax lang=’html4strict’]<p>[% title %]</p>
<div>
<h1>[% row.title|html %] ([% row.posted|html %])</h1>
<h2>[% row.summary|html %]</h2>
<div>[% row.news|html %]</div>
<p><a href=”index.pl”>Ir al Inicio</a></p>
</div>
[/codesyntax]
Con esto tenemos la base de la aplicación, pero solo visualmente, todavía no podemos mandar comentarios, ni mandar noticias.
Lo que necesitamos hacer es crear una forma en la plantilla, y recibir en una vista lo que se envía por POST. El modulo “view” cambia un poco, queda mas o menos como esto:
[codesyntax lang=”perl” container=’pre’]sub view : Runmode {
my $self = shift;
my $sth;
my $q = $self->query();
if ( $q->param(‘submit’) ) { # POST METHOD, let’s save the comment
$sth = $self->dbh->prepare(“INSERT INTO comment (title, comment, posted, last_edit, user_id, name, em
ail, news_id) VALUES ( ?, ?, now(), now(), 0, ?, ?, ?)”);
$sth->execute( $q->param(‘title’), $q->param(‘comment’), $q->param(‘name’), $q->param(’email’), $q->param(‘id’)
);
}
$sth = $self->dbh->prepare(“SELECT * FROM news WHERE id = ?”);
$sth->execute($q->param(‘id’));
my $row = $sth->fetchrow_hashref;
$sth = $self->dbh->prepare(“SELECT * FROM comment WHERE news_id = ? ORDER BY posted DESC”);
$sth->execute($q->param(‘id’));
my @comms;
while ( my $comm = $sth->fetchrow_hashref ) {
push(@comms, $comm);
}
my $num_c = @comms;
my $vars = {
title => $row->{‘title’},
num_c => $num_c,
row => $row,
comms => \@comms,
};
return $self->tt_process(‘view.html’, $vars);
}
[/codesyntax]
Con esto está casi completo el objetivo de este pequeño tutorial, pero falta una parte importante, la autenticación, pero como estamos haciendo todo al menor estilo de CGI::Application, solo tenemos que agregar unas cuantas lineas:
[codesyntax lang=’perl’ container=’pre’]use CGI::Application::Plugin::Session;
use CGI::Application::Plugin::Authentication;
Blog->authen->config(
DRIVER => [
‘DBI’,
TABLE => ‘user’, CONSTRAINTS => {
‘user.login’ => ‘__CREDENTIAL_1__’,
‘MD5:user.password’ => ‘__CREDENTIAL_2__’,
},
],
LOGOUT_URL => ‘http://latin.example.com/cgi-bin/index.pl’,
STORE => ‘Session’,
);
Blog->authen->protected_runmodes(qw(view));
[/codesyntax]
y un hacer un INSERT en la tabla “user”:
[codesyntax lang=’sql’]insert into user values (null, ‘admin’, MD5(‘admin’), ’[email protected]’, ‘Admin’, now(), now());
[/codesyntax]
El módulo de autenticación tiene la capacidad de reusar la instancia de DBI, y además nos crea una página de logueo, e incluso redirecciona “inteligentemente” a la página que estabamos tratando de accesar. La información que ponemos en su inicialización es la estrictamente necesaria, la tabla donde están las columnas de usuario y clave, las columnas, el método de “encriptación”, y por supuesto el método que se usará para autenticar, no tiene que ser una base de datos escrictamente, se puede crear un driver personalizado para soportar cosas más exóticas.

Y finalmente un punto importante que debemos notar, el uso de sesión, es necesario que guardemos en sesión los datos de autenticación, o el usuario final va a tener que estar metiendo su nombre usuario y clave con demasiada frecuencia, con solo agregar el módulo de session, y decirle al driver de autenticación que guarde las variables en él, ya tenemos un método de autenticación completo y funcional.

Noten que solo estoy poniendo como protegido al módulo “view”, podemos poner a cualquier módulo protegido, (ej. qw(view list otro_modulo) pero normalmente la lista de noticias nos gusta mantenerla pública, y de hecho normalmente la vista extendida de la noticia también, pero en este caso tenemos en el mísmo módulo tanto la vista extensa, como el agregar comentarios, es una buena idea separar los comentarios en otro módulo, de tal manera que podamos atomizar mejor los privilegios sobre la página.

Obviamente este ejemplo es muy mínimo, y se ve terriblemente mal, porque no tiene ni siquiera CSS, pero todo eso se corrige con la ayuda de un diseñador, los programadores no somos buenos diseñadores, así que pa que le hacemos al cuento? (tip: baja una plantilla gratuita de algun sitio 🙂

Espero que a alguien le parezca interesante este resumen de cómo usar CGI::Application, la documentación es bastante buena, pero cada módulo está documentado independientemente, y a veces es un poco complicado para alguien (como yo) de encontrarle la hilación, pero una vez que se entiende la base, lo demás es muy sencillo.
Ah !!, y una de las grandes ventajas de usar CGI::Application es que fácilmente se puede montar tanto sobre CGI como en mod_perl, aunque nunca he usado mod_perl :), pero en teoría no hay prácticamente nada que hacer para que funcione en ambos.

Eso es todo amigos, buen día 🙂


4 Comments, Comment or Ping

  1. Porque no en vez de utilizar #!/opt/local/bin/perl -w
    utilizas
    #!/usr/bin/env perl -w
    ?
    Asi cualquier persona que copie tus scripts le va a funcionar 🙂

    Saludos sensei!

    August 14th, 2009

  2. entiendo tu punto kwame, pero ese método de todas maneras requiere que andes jugando con los links suaves, copiando binarios, o el etcétera.

    Cualquiera que se meta con perl ya sabe usar sed para corregir todos los paths con un “oneliner” :), sin mencionar que este tutorial enseña como depender de un solo archivo para toda la ejecusión 🙂

    August 14th, 2009

  3. Daniel Delgado

    Hola,

    estoy en búsqueda de profesionales en perl y Linux/UNIX 100% bilingues, algun interesado????? mandame tu CV a [email protected]

    October 8th, 2009

  4. dan

    Me fascina el mundo de cgi aplicacion, estoy creando documentacion en español de varias cosas sobre Perl, pero pues espero crear una comunidad mas grande, pero ahi se logra poco a poco,,

    esta mina de oro de cgi applicacion es algo lejos para los programadores del simple ASP o PHP,, es lo facil para hacer las cosas eso no lo cambio es mi frameworks favorito por lo ligero y flexible

    February 5th, 2010

Reply to “CGI::Aplication el fino arte de hacer web perl-style”