Autentificar usuarios en PHP

passwordSe ha escrito mucho sobre este tema, y es fácil verificarlo con una sencilla búsqueda en Google, pero no me resisto a añadir mi propia versión.

A veces necesitamos crear sitios web en los que una parte o todo el contenido debe ser accesible sólo por determinados usuarios. También es posible que queramos personalizar ciertas partes del contenido, dependiendo de quién sea el usuario.

En cualquiera de estos casos, será necesario :

  1. Proporcionar una forma de acceso y verificación de usuarios.
  2. Diseñar una forma de evitar el acceso a determinadas áreas y funcionalidades del sitio.

Autentificar usuarios

La primera parte consiste en verificar el acceso a usuarios. Para ello primero obtendremos ciertos valores que nos permitan identificar a un usuario de forma clara, y después verificaremos que efectivamente, para esos datos existe un usuario con acceso.

Los datos a obtener puede que no sean tan evidentes como parece. Por supuesto, como todos estamos acostumbrados a usar formularios de identificación (o login), damos por hecho que se necesitan al menos dos valores para autentificar la identidad de un usuario. En la mayor parte de las aplicaciones esto es cierto. Un dato identifica al usuario, y el segundo lo autentifica.

Si sólo leemos un identificador, cualquier persona que conozca el identificador de otro usuario podría entrar al sistema. El identificador suele ser conocido, o al menos no siempre es un secreto. Normalmente se usa un nombre, un apodo (un nick), o una dirección de correo. Con sólo el identificador podríamos determinar quién se ha conectado, pero no podríamos estar seguros de que el usuario es quien dice ser.

Si sólo leemos el dato de autentificación, es decir, una contraseña, probablemente estaremos seguros de quien se ha conectado; pero, a no ser que sea el administrador quien asigne las contraseñas, se trata de una técnica peligrosa, ya que es imposible asegurar que dos usuarios no usen la misma contraseña por casualidad.

llavesPodemos pensar en las contraseñas como si fuesen llaves. Las llaves son tanto más seguras cuanto más difíciles sean de copiar. Sin embargo, el acceso a una página no es como el acceso a un edificio, se parece más a una caja de seguridad de un banco. Lo que protegemos en una página es la propiedad de cada usuario. Generalmente cada usuario que accede a una página protegida sólo tiene acceso a sus datos, y sólo algunos usuarios especiales (los administradores), tienen acceso a datos de otros usuarios. Los datos de cada uno estarán seguros siempre que las llaves no se dupliquen o se presten, o en el caso de páginas web, siempre que no se compartan.

Usando los dos valores, usuario y contraseña, la aplicación sólo tiene que asegurarse de que los identificadores de usuario son únicos. En nuestras aplicaciones diseñaremos mecanismos para impedir que dos usuarios usen el mismo identificador. Aunque nada impide, en principio, que varios usuarios usen la misma clave.

Es muy frecuente usar una dirección de correo como identificador de usuario. Esto tiene algunas ventajas, por ejemplo:

  • Permite una autentificación extra. La técnica habitual es enviar un mensaje a la dirección proporcionada, que el usuario debe responder, indicando un asunto determinado o con un enlace en el propio mensaje que el usuario debe seguir. Esto asegura que el usuario es realmente el dueño de la dirección de correo indicada.
  • Es posible enviar notificaciones a los usuarios, o añadir opciones para restablecer o recordar contraseñas.
  • Hace más difícil que se repitan identificadores. En sistemas donde no se requiere una dirección de correo como identificador, es habitual que se tenga que probar varias veces hasta que se encuentra un identificador válido, que no haya sido usado por otra persona.

Como inconveniente, los usuarios que no tengan una cuenta de correo no podrán darse de alta. Aunque es relativamente sencillo conseguir una cuenta de correo gratuita, se considera una mala costumbre obligar a los usuarios a cumplir determinadas condiciones para acceder a algún servicio, y en determinados casos se puede considerar una técnica discriminatoria.

En cuanto a la autentificación, implica la consulta de los datos leídos dentro de algún tipo de base de datos. La elección de esa base de datos depende de nosotros. En sistemas flexibles, en los que se pueden asignar o eliminar usuarios de forma dinámica, se suele usar una base de datos como MySQL. Si se trata de un sistema más rígido, podemos optar por otro tipo de bases de datos, como simples ficheros de texto, o en el caso más simple posible, a la codificación dura de datos, es decir, con variables definidas en PHP.

Este último método no es demasiado práctico, ya que cualquier cambio en los datos de usuarios implica modificar el código y actualizarlo en el servidor.

En este artículo usaremos una base de datos MySQL, ya que hay algunos detalles que hay que tener en cuenta en ese caso particular.

Base de datos de usuarios

En  una  tabla  de  una  base  de  datos  almacenaremos los datos de los usuarios.  Para  este  ejemplo  nos  basta  con un identificador y una contraseña.

También se asume que la base de datos se llama «database» y la tabla «usuario», si fuera necesario, es fácil cambiar estos nombres.

Para  crear  la  tabla y añadir un par de usuarios para probar puedes ejecutar estas consultas SQL, dentro de la base de datos «database»:

----8<------
 CREATE TABLE IF NOT EXISTS `usuario`
 `iduser` int(11) NOT NULL AUTO_INCREMENT,
 `usuario` varchar(64) NOT NULL,
 `password` varchar(42) NOT NULL,
 PRIMARY KEY (`iduser`)
 ) ENGINE=MyISAM  DEFAULT CHARSET=latin1;

 INSERT INTO `usuario` (`usuario`,`password`)
 VALUES("prueba",PASSWORD("prueba"));

 INSERT INTO `usuario` (`usuario`,`password`)
 VALUES("ejemplo",PASSWORD("ejemplo"));
----8<------

Hay que destacar que no almacenaremos las contraseñas tal cual. Esto sería un problema de  seguridad,  ya que cualquiera que acceda a la base de datos, incluido el administrador, podría conocer todas las contraseñas.

Nota: aunque el administrador siempre tiene la posibilidad de saltarse esta medida no debemos menospreciar esta seguridad extra, ya que es frecuente que los usuarios usen las mismas contraseñas en varios lugares. Este sistema protege las contraseñas, aunque la cuenta no sea del todo privada para el administrador.

En  su  lugar,  lo  normal  es  almacenar un código HASH asociado a la contraseña.  MySQL  dispone de una función específica para eso, que se llama «PASSWORD». En cualquier caso, se puede usar cualquier otra función HASH, como MD5 o SHA.

La ventaja de estas funciones para este caso es que no son reversibles, es decir, no es posible obtener la contraseña a partir de su código HASH, o al menos no de una forma sencilla (siempre se puede usar la fuerza bruta).

Lo  que haremos para validar un usuario es verificar que la contraseña indicada tiene el mismo código HASH que está almacenado en la base de datos.

Formulario de entrada

Empecemos  con  la  página  de  entrada, «index.php», que mostrará el formulario de «login»:

 ----8<------
<!DOCTYPE HTML>
<html>
<head>
</head>
<body>
<fieldset>
    <legend>Formulario de entrada</legend>
    <form method="POST" action="login.php">
        <br/>
        <label>Usuario: </label>
        <input type="text" name="usuario" size="40" value="">
        <br/>
        <br/>
        <label>Contraseña: </label>
        <input type="password" name="password" size="40" value="">
        <br/>
        <br/>
        <input type="submit" name="entrar" value="Entrar">
    </form>
</fieldset>
</body>
</html>
----8<------

Verificar usuario

Cuando   el  usuario  pulsa  en  «Entrar»  se  ejecuta  el  script  PHP «login.php», que verifica si los valores suministrados pertenecen a un usuario válido de la base de datos. Si es así, se abre una sesión y se almacenan un par de variables de sesión:

----8<------
<?php
$msUser = "tu usuario MySQL";
$msPassword = "tu contraseña MySQL";
$IdConexion = mysql_connect('localhost', $msUser, $msPassword)
or die ('Imposible conectar con base de datos.');

$head = "Location: http:index.php";
if(isset($_POST["password"]) && isset($_POST["usuario"])) {
    if(isset($_POST["entrar"])) {
        $Query  = "SELECT * FROM database.usuario ";
        $Query .= "WHERE usuario=\"".$_POST["usuario"]."\" AND password=PASSWORD(\"".$_POST["password"]."\")";
        $IdConsulta = mysql_query($Query, $IdConexion);
        if((mysql_num_rows($IdConsulta) > 0)) {
            session_start();
            $_SESSION["autentificado"]= true; 
            $_SESSION["usuario"] = mysql_fetch_array($IdConsulta);
            $head = "Location: http:index2.php";
        }
    }
    mysql_close($IdConexion);
}
header($head);
?>
----8<------

Si  la  consulta tiene éxito, creamos dos variables de sesión. En una guardamos los datos del  usuario, y en la otra un bit que indica si está conectado o no.

Una  vez  creada  las  variables de sesión,  almacenamos la página que se abrirá a continuación  en  $head,  que  será  «index2.php». Si el usuario no es
válido,  $head  contienen  el  valor  inicial, que es la página con el formulario de entrada.

Al  finalizar,  indicamos  que  se cargue la página $head, mediante la directiva header().

Si  el  usuario  no  existe  o la contraseña no es válida, volvemos al formulario  de  entrada. Si existe, se visualiza  «index2.php», que de momento sólo muestra el formulario de salida.

Mecanismo de protección de contenidos

En cada página con acceso restringido tenemos que incorporar un mecanismo que evite que la página sea mostrada si el usuario no ha hecho login correctamente.

----8<------
<!DOCTYPE HTML>
<?php
    $msUser = "tu usuario MySQL";
    $msPassword = "tu contraseña MySQL";
    $IdConexion = mysql_connect('localhost', $msUser, $msPassword)
    or die ('Imposible conectar con base de datos.');

    session_start();
    if(isset($_SESSION["autentificado"]) && $_SESSION["autentificado"]) {
        $Query = "SELECT * FROM database.usuario WHERE iduser=\"".$_SESSION["usuario"]["iduser"]."\"";
        $IdConsulta = mysql_query($Query, $IdConexion);
        // Si el usuario ya no está en la base de datos, hacer logout:
        if(mysql_num_rows($IdConsulta) == 0) {
            // Logout:
            $_SESSION["autentificado"] = false; 
            session_destroy();
        }
    } else {
        // Logout:
        $_SESSION["autentificado"] = false; 
        session_destroy();
    }

    mysql_close($IdConexion);

    if(!$_SESSION["autentificado"]) {
        header("location: http:index.php");
        exit(0);
    }
?>
<html>
<head>
</head>
<body>
    <h1>Usuario <?php echo $_SESSION["usuario"]["usuario"]; ?> ingresado con éxito.</h1>
    <fieldset>
    <legend>Salir</legend>
        <form method="POST" action="logout.php">
        <br/>
        <input type="submit" name="salir" value="Salir">
        </form>
    </fieldset>
</body>
</html>
----8<------

El código PHP anterior al tag <html> verifica si el usuario tiene abierta una sesión. Si es así, no tiene ningún efecto, y el resto de la página se carga correctamente. Si el usuario no tiene una sesión abierta, o ha sido borrado desde que la abrió, la página no sigue cargando, y en su lugar se muestra de nuevo la página de «login».

Es importante verificar si el usuario sigue estando en la base de datos. Supongamos que el administrador elimine a un usuario que está actualmente conectado; si no verificamos esta condición, mantendría su acceso hasta que cierre la sesión, que es algo que normalmente no nos interesa.

Este  ejemplo es simple, y  sólo  muestra un formulario de salida, si se pulsa «Salir» se ejecuta el script «logout.php».

----8<------
 <?php
    session_start();
    $_SESSION["autentificado"] = false;
    session_destroy();
    header("Location: http:index.php");
?>
----8<------

Modificamos el valor del bit que indica que el usuario está conectado. Cerramos la sesión, y cargamos la página de «login».

En casos más sofisticados podemos añadir a la base de datos tablas que permitan dar acceso a determinadas zonas de la página, definiendo niveles de acceso, o estableciendo permisos para determinadas áreas.

El administrador, o los usuarios con el nivel de acceso adecuado podrá otorgar o denegar el acceso de cada usuarios a cada zona.

También, como medida de seguridad adicional, es interesante limitar el tiempo de vida de la sesión, de modo que si un usuario abandona la página sin hacer «logout», su sesión se cierre automáticamente al cabo de unos minutos. El tiempo de fin de sesión se puede restablecer cada vez que se cargue una página. Esto evita que otras personas puedan acceder a la página con la cuenta de ese usuario desde el mismo ordenador. El inconveniente es que, si el usuario se demora mucho tiempo en una página, por ejemplo, completando un formulario, al seguir navegando se haya cerrado la sesión, y se pierdan los datos introducidos. Hay que medir bien estos tiempos, o al menos avisar al usuario en determinadas páginas del tiempo del que dispone para completar la tarea.

 

Formulas de validación

Validación de datosUn problema al que nos enfrentamos habitualmente cuando tenemos que obtener datos de un usuario es el de detectar información falsa o con errores.

No siempre, pero cuando se trata de cierto tipo de datos es posible aplicar fórmulas para verificar la veracidad de esos datos.

En esta entrada veremos algunas de esas fórmulas, y algunos ejemplos para implementarlas en C++.

Número DNI o NIF

Al menos en España, desde hace bastantes años, cada ciudadano tiene un DNI (Documento Nacional de Identidad), en el que figuran algunos datos sobre él. Cuando se obtiene por primera vez, se le asigna un número de ocho cifras, que es único para cada persona.

Posteriormente, con fines fiscales, se creó el NIF (Número de Identificación Fiscal). En el caso de personas físicas, el NIF es el mismo número del DNI al que se añadió una letra como control. Esa letra se obtiene aplicando una fórmula al número del DNI, de modo que si se cambia cualquiera de los dígitos, o la letra, es posible detectar el error.

No se trata de un algoritmo demasiado sofisticado, como veremos a continuación.

Algoritmo

Básicamente se compone de dos pasos:

  1. Se calcula el módulo, o resto de la división del número del DNI entre el valor 23.
  2. La letra de control se obtiene usando como índice el valor actual, en la siguiente tabla de 23 letras:
0 1 2 3 4 5 6 7 8 9 10 11
T R W A G M Y F P D X B
12 13 14 15 16 17 18 19 20 21 22
N J Z S Q V H L C K E

Se excluyen en esta tabla las letras: ‘I’, ‘Ñ’, ‘O’, ‘U’.

La ‘I’ y la ‘O’ se descartan porque es fácil confundirlas con otros caracteres, como ‘1’, ‘l’ ó ‘0’. La ‘Ñ’, porque se puede confundir con la ‘N’, y la u, seguramente, porque sobraba una letra.

Ejemplo de validación de NIF:

const int nifok = 0;
const int noesnif = 1;
const int faltaletra = 2;
const int nifincorrecto = 3;
const int niferroneo = 4;

int VerificarNIF(char *dni) {
    char letra[] = "TRWAGMYFPDXBNJZSQVHLCKET";
    unsigned int i;
    char l;
    int retval = nifok;

    if(strlen(dni) == 0) {
        retval = noesnif;
    } else
    if(isdigit(dni[strlen(dni)-1])) {
        retval = faltaletra;
    } else {
        for(i = 0; i < strlen(dni)-1; i++) {
            if(!isdigit(dni[i])) {
                retval = nifincorrecto;
            }
        }
    }
    if(!retval) {
        i = atoi(dni);
        l = dni[strlen(dni)-1];
        if(letra[i % 23] != toupper(l)) {
            retval = niferroneo;
        }
    }
    return retval;
}

Tarjetas de crédito

Para verificar los números de las tarjetas de crédito se usa el algoritmo de Luhn.

En este caso, el último dígito también es un dígito de control, que se puede calcular a partir de los dígitos restantes.

Hay más niveles de verificación, aunque nos limitaremos a este.

Por ejemplo, los seis primeros dígitos, conocidos como BIN, identifican el tipo de tarjeta, el primer dígito, y la entidad que emite la tarjeta, los cinco restantes.

Por ejemplo las tarjetas VISA empiezan siempre por ‘4’, las Mastercard por ‘5’, etc.

El algoritmo de Luhn es también muy simple. Hay que tener en cuenta que estos algoritmos de verificación están pensados para que sean sencillos y sirven para detectar pequeños errores.

  1. Tomando los dígitos de derecha a izquierda, incluyendo el dígito de control, se duplican los de las posiciones pares y si el resultado es mayor de 9 se resta 9.
  2. Se suman todos los dígitos así obtenidos.
  3. Si el resultado es divisible entre 10, es decir, el módulo 10 es cero, el número es correcto.

Por ejemplo:

5245 6585 6635 5458

1285 3575 3665 1418

8+(2*5 mod 9)+4+(2*5 mod 9)+5+(2*3 mod 9)+6+(2*6 mod 9)+5+(2*8 mod 9)+5+(2*6 mod 9)+5+(2*4 mod 9)+2+(2*5 mod 9) =

8+1+4+1+5+6+6+3+5+7+5+3+5+8+2+1 =

70

Es decir, el número es correcto.

bool VerificarTarjeta(const char *numero) {
    char *n;
    int digito;
    bool par = false;
    int res = 0;

    n = new char[strlen(numero)+1];
    strcpy(n, numero);
    do {
        digito = n[strlen(n)-1]-'0';
        n[strlen(n)-1] = 0;
        res += digito;
        if(par) {
            res += digito;
            if(digito > 4) res -= 9;
        }
        par = !par;

    } while(strlen(n));
    delete[] n;
    return !(res % 10);
}

ISBN

El ISBN (International Standard Book Number) es un código numérico que se usa para identificar libros internacionalmente. Hay dos variedades, una de diez dígitos, y posteriormente se creó una segunda, de trece dígitos.

En ambos casos el último dígito se usa como dígito de control. Pero, cada una de ellas usa un algoritmo diferente para calcularlo.

ISBN de diez dígitos

En este caso, el dígito de control se calcula de la siguiente forma:

  1. Se toman nueve dígitos de izquierda a derecha, multiplicando cada uno por la posición que ocupa, es decir, el primero por uno, el segundo por dos, etc. Y se suma cada resultado.
  2. Al valor resultante se le aplica el módulo con once. Esto da un valor entre 0 y 10.
  3. El dígito de control es ese valor, si está entre 0 y 9, ó X si es 10.

Por ejemplo, para un ISBN de diez dígitos:

84-9736-467-8

8+4*2+9*3+7*4+3*5+6*6+4*7+6*8+7*9 = 261

261 % 11 = 8

ISBN de trece dígitos

Se usa el mismo algoritmo que para códigos de barras EAN (European Article Number), que son los códigos de barras de toda la vida, que aparecen en casi todos los productos comerciales.ean13

Algoritmo:

  1. Se toman los doce caracteres de izquierda a derecha, multiplicando los impares por uno y los pares por tres, y se suma cada resultado.
  2. El dígito de control se calcula restando de diez el módulo del resultado con diez. Es decir, el valor que habría que sumar al resultado para que sea divisible entre diez

Por ejemplo, para este ISBN:

978-84-253-4025-3

(9+8+4+5+4+2)+3*(7+8+2+3+0+5) = 32+3*25 = 32+75 = 107

10-107%10 = 10-7 = 3

bool ISBN10(const char *numero) {
    int res = 0;
    char control;

    if(strlen(numero) != 10) return false;
    for(int i = 0; i < 9; i++) {
        res += (numero[i]-'0') * (i+1);
    }
    control = res % 11;
    if(control == 10) control = 'X'; else control += '0';

    return control==numero[9];
}

bool ISBN13(const char *numero) {
    int res = 0;

    if(strlen(numero) != 13) return false;
    for(int i = 0; i < 13; i++) {
        if(i%2) res += 3*(numero[i]-'0');
        else res += numero[i]-'0';
    }

    return !(res % 10);
}

Códigos de cuenta de cliente e IBAN

Los códigos de cuenta de cliente (CCC) son los números de cuenta bancaria, que actualmente se están sustituyendo por el IBAN (International Bank Account Number)

El IBAN se obtiene fácilmente a partir del CCC. Basta con añadir cuatro caracteres delante del CCC, los dos primeros son dos letras que identifican el país, y los otros dos, son dos dígitos de control.

Verificación del CCC

Pero empecemos por el CCC, ya que también tienen un formato con dígitos de control.

El formato tiene veinte dígitos con la estructura siguiente:

 EEEE OOOO CC NNNNNNNNNN

EEEE: 4 dígitos que identifican la entidad bancaria
OOOO: 4 dígitos que identifican la oficina
C Un dígito de control Entidad/Oficina
C Un dígito de control Número de cuenta
NNNNNNNNNN: 10 dígitos de número de cuenta

El primer dígito de control se obtiene de los ocho primeros dígitos, y el segundo de los diez últimos.

El algoritmo es el mismo en los dos casos, aunque para los ocho primeros dígitos añadiremos dos ceros al principio, para que ambos conjuntos tengan diez dígitos cada uno.

  1. Tomando los dígitos uno a uno, de izquierda a derecha, multiplicamos cada uno por los siguientes valores, respectivamente: 1,2,4,8,5,10,9,7,3,6.
  2. Sumamos cada uno de los productos.
  3. Calculamos el módulo once del valor obtenido.
  4. Restamos ese valor de 11.
  5. Si el módulo está entre 0 y 9, ese es el dígito de control, si es 10, tomaremos el valor 1, y si es 11, tomaremos el valor 0.

Los valores del paso 1 se calculan como 2n % 11, desde n=0 a n=9.

Por ejemplo:

1234 5678 06 1234567890

0*1+0*2+1*4+2*8+3*5+4*10+5*9+6*7+7*3+8*6 = 231

231 % 11 = 0

11-0 = 11

DC1 = 0

1*1+2*2+3*4+4*8+5*5+6*10+7*9+8*7+9*3+0*6 = 280

280 % 11 = 5

11-5 = 6

DC2 = 6

bool VerificarCCC(char *ccc) {
    // EEEE OOOO CC NNNNNNNNNN
    // 4 dígitos Entidad
    // 4 dígitos Oficina
    // Dígito de control Entidad/Oficina
    // Dígito de control Número de cuenta
    // 10 dígios Número de cuenta
    int peso[10] = {1,2,4,8,5,10,9,7,3,6};
    unsigned int i;
    int j, DC1, DC2;
    char ccc_l[21];

    if(strlen(ccc) != 20) {
        return false;
    }

    for(i = 0; i < strlen(ccc)-1; i++) {
        if(!isdigit(ccc[i])) {
            return false;
        }
    }
    strcpy(ccc_l, ccc);

    for(DC1 = i = 0; i < 8; i++) {
        DC1 += peso[i+2] * (ccc[i]-'0');
    }
    DC1 = 11-(DC1%11);
    if(DC1 == 10) DC1 = 1;
    if(DC1 == 11) DC1 = 0;

    for(DC2 = i = 0; i < 10; i++) {
        DC2 += peso[i] * (ccc[i+10]-'0');
    }
    DC2 = 11-(DC2%11);
    if(DC2 == 10) DC2 = 1;
    if(DC2 == 11) DC2 = 0;

    if(ccc[8]-'0' != DC1 || ccc[9]-'0' != DC2) {
        return false;
    }
    return true;
}

Verificación del IBAN

Para formar el IBAN se usan veinticuatro caracteres. Los dos primeros son dos letras que identifican el país, en el caso de España, «ES». Los dos siguientes son un número de control. A continuación se añaden los veinte dígitos del CCC.

Para verificar el IBAN se usa el siguiente algoritmo:

  1. Los cuatro nuevos caracteres se eliminan y se añaden al final de número
  2. Las letras se sustituyen por un valor numérico de dos dígitos cada uno. Ese valor numérico es 10 para la ‘A’, 11 para la ‘B’, etc.
  3. Se calcula el módulo del número resultante con 97.
  4. Si es 1, el código IBAN es correcto.

Por ejemplo:

ES68 1234 5678 06 1234567890

12345678061234567890ES68

E: 14, S:28

12345678061234567890142868 % 97 = 1

Tenemos que resolver un pequeño problema adicional: C++ no dispone de enteros lo suficientemente grandes como para manejar números de 26 cifras, y el valor 97 se escogió deliberadamente por ser el mayor número primo menor de 100, de modo que…

Afortunadamente disponemos de un algoritmo para calcular restos de divisiones cuando el dividendo es un número grande y el divisor es relativamente pequeño, al menos lo suficiente para poder manejarlo con un int de C++. Se trata del algoritmo Big Mod, que vimos en una entrada anterior.

En el caso de los códigos IBAN, la verificación es doble, ya que los veinte dígitos finales deben verificar el algoritmo para CCC, y el código completo, el de IBAN:

bool VerificarIBAN(const char *iban) {
    char *iban2;
    int resto;

    if(!VerificarCCC(&iban[4])) return false;

    iban2 = new char[27];
    strcpy(iban2, &iban[4]);
    sprintf(iban2, "%s%02d%02d%c%c", 
        &iban[4], 10+iban[0]-'A', 10+iban[1]-'A', iban[2], iban[3]);
    resto = moduloNG(iban2, 97);
    delete[] iban2;
    return resto == 1;
}

int moduloNG(const char *numero, int d) {
    int i = 0, n = 0;

    while(numero[i]) n = (n * 10 + numero[i++] - '0') % d;
    return n;
}

Enlaces de interés:

NIF: Wikipedia

Código de cuenta cliente: Luciano.com

Código IBAN: CienciaExplora.com

Tarjetas: Wikipedia, eHow Español

Elegir el nombre de columnas en tablas SQL

sqlDurante el diseño o modelado de una base de datos llega un momento en que debemos asignar un nombre a cada tabla y a cada atributo.

Por supuesto, hay ciertas reglas que siempre deberemos cumplir, como que cada tabla tenga un nombre diferente, y en cada tabla, que cada atributo o columna, tenga también un nombre diferente.

Sin embargo, sin ser reglas de obligado cumplimiento, también podemos formular algunas normas que nos eviten problemas durante el uso de nuestras bases de datos. Veremos algunas de ellas.

La primera regla puede ser elegir nombres descriptivos para cada tabla y para cada columna. Conviene elegir nombres que describan claramente el contenido de cada una de ellas, ya que eso facilitará mucho la tarea cuando tengamos que crear nuestras consultas.

Una segunda regla puede ser no usar nombres demasiado genéricos, como «nombre», «id», «numero», etc. Puede que cuando decidas usar uno de estos nombres tengas muy claro a qué te refieres, pero seguramente, al cabo de un tiempo no estará tan claro. Es preferible usar identificadores más largos que aporten más detalles, como «nombre_usuario», «id_cliente», «numero_serie», etc.

La tercera regla que se me ocurre, y que normalmente se pasa por alto, es no usar el mismo nombre para columnas en distintas tablas de la misma base de datos. Por ejemplo, si tenemos dos tablas: autor y editorial, no es buena idea tener una columna en autor con el identificador «nombre» y otra en editorial con el mismo identificador. O por ejemplo, que los campos «clave» en ambas tablas se llamen clave.

¿Por qué no es buena idea que haya columnas con nombres iguales en tablas diferentes de la misma base de datos?

Lo veremos con un ejemplo. Crearemos un par de tablas:

CREATE TABLE `personas` (
 `id` int(11) NOT NULL,
 `nombre` varchar(40) DEFAULT NULL,
 `fecha` date DEFAULT NULL
 );

ALTER TABLE `personas`
 ADD PRIMARY KEY (`id`);

INSERT INTO `personas` (`id`, `nombre`, `fecha`) VALUES
 (1, 'Fulanito', '1998-04-14'),
 (2, 'Menganito', '1758-06-18'),
 (4, 'Tulanito', '1984-07-08');

CREATE TABLE `telefonos3` (
 `numero` char(12) DEFAULT NULL,
 `id` int(11) NOT NULL
 );

ALTER TABLE `telefonos`
 ADD KEY `id` (`id`);

INSERT INTO `telefonos` (`numero`, `id`) VALUES
 ('12321321', 1),
 ('56546545', 1),
 ('56546545', 4),
 ('66684654', 4);

Y hagamos una mezcla natural:

SELECT * FROM personas NATURAL JOIN `telefonos`
id nombre fecha numero
1 Fulanito 1998-04-14 12321321
1 Fulanito 1998-04-14 56546545
4 Tulanito 1984-07-08 56546545
4 Tulanito 1984-07-08 66684654

Ahora añadimos un campo extra a la tabla de teléfonos:

ALTER TABLE `personas` ADD `nombre` VARCHAR(32) NOT NULL AFTER `fecha`;

Y repitamos la consulta.

Pero, ahora el resultado no proporciona ninguna salida, ¿por qué?

Porque ahora sólo se generan resultados cuando coinciden los valores de «id» y «nombre» en las dos tablas, y probablemente no es eso lo que queríamos hacer.

 

Algoritmo Big Mod

numerosPongamos que tenemos que calcular el módulo o resto de la división de un número de muchas cifras entre otro más pequeño, lo suficiente para que pueda almacenarse en un int, o en un long int.

No es algo que no nos vayamos a encontrar nunca. De hecho, la razón de esta entrada es que he necesitado hacer estas operaciones, y pronto veremos un ejemplo en una entrada posterior.

En principio no parece fácil, pero las matemáticas vienen en nuestra ayuda. Concretamente la teoría de números.

El algoritmo que vamos a ver, como dice el título se llama Big Mod.

Este algoritmo se basa en estas dos propiedades:

(a*b*c) mod m =((a mod m)*(b mod m)*(c mod m)) mod m (1)
(a+b+c) mod m =((a mod m)+(b mod m)+(c mod m)) mod m (2)

Primero veamos qué significa exactamente calcular el módulo de una división.

dividendo  = cociente * divisor + resto

Hay que tener en cuenta que, a la hora de calcular el resto, nos podemos despreocupar del valor del cociente. Esto nos puede ayudar, porque el resto es el mismo si restamos del dividendo el divisor un número entero de veces:

dividendo – n * divisor = (cociente-n) * divisor + resto

Por otra parte, si descomponemos el dividendo en dos factores, a y b:

a*b = cociente * divisor + resto

(a-n*divisor)*(b-m*divisor) = (cociente-n-m) * divisor + resto

Es decir, el resto es el mismo si restamos de cualquiera de los factores un múltiplo del divisor.

Lo mismo sirve para sumas, si descomponemos el dividendo en dos sumandos c y d:

c+d = cociente * divisor + resto

(c-n*divisor)+(d-m*divisor) = (cociente-n-m) * divisor + resto

Esto es, el resto es el mismo si restamos de cualquiera de los sumandos un múltiplo del divisor.

¿Cómo nos ayuda esto a resolver nuestro problema?

Veamos un ejemplo:

5657866541284565 % 67 =

(565*1013+7866541284565) % 67

Ahora, podemos restar del primer número, el 565 el valor 67, tantas veces como queramos. Lo haremos el máximo, es decir, nos quedaremos con el resto de 565/67, o lo que es lo mismo, 565 % 67 = 29.

(29*1013+7866541284565) % 67

O:

297866541284565 % 67

Como se puede ver, hemos disminuido en una unidad los dígitos del número grande.

Podemos aplicar esta técnica recursivamente, o mejor, iterativamente.

Algoritmo

  1. Partimos de n igual a cero.
  2. Tomamos un dígito de la izquierda del número grande y lo añadimos a la derecha de n.
  3. Calculamos el módulo n % d, y lo guardamos en n.
  4. Repetimos desde 2, hasta terminar los dígitos del número grande.
  5. n contiene el valor del módulo.

5657866541284565 % 67

5 % 67 = 5

56 % 67 = 56

565 % 67 = 29

297 % 67 = 29

298 % 67 = 30

306 % 67 = 38

etc.

Código C++

Una implementación sencilla es esta:

int moduloNG(const char *numero, int d) {
    int i = 0, n = 0;

    while(numero[i]) {
        n = n * 10 + numero[i] - '0';
        n = n % d;
        i++;
    }
    return n;
}

Otra, más compacta:

int moduloNG(const char *numero, int d) {
    int i = 0, n = 0;

    while(numero[i]) n = (n * 10 + numero[i++] - '0') % d;
    return n;
}

Conectar y desconectar unidades de red

Unidad de redAlgunas veces nos encontramos con la necesidad de que nuestra aplicación acceda a archivos almacenados en un ordenador diferente, aunque conectado a la misma red.

En esta entrada veremos cómo conectarse y cómo desconectarse a una unidad de red, usando el API de Windows. Mientras exista esa conexión, podremos acceder a ficheros en la unidad remota del mismo modo que a cualquier fichero local.

Básicamente usaremos dos funciones del API de Windows: WNetAddConnection2 y WNetCancelConnection2, para crear y eliminar una conexión, respectivamente.

Estas funciones están en la librería de Windows «Mpr«, por lo tanto, tendremos que incluirla en la fase de enlazado de nuestro programa.

Para el programa de ejemplo usaremos una carpeta en una unidad de disco local, por ejemplo, C:\temp. Aunque si estamos en una red podremos conectar a cualquier recurso compartido de otros ordenadores, siempre que nuestro usuario tenga los permisos adecuados.

Antes de poder crear una unidad de red deberemos compartir ese recurso en la red, para ello usaremos el explorador de archivos de Windows, y accederemos a las propiedades de la carpeta que queramos compartir. Vamos a la pestaña de «Compartir», y pulsamos el botón de «Compartir». En el siguiente cuadro de diálogo nos permitirá elegir con qué usuarios queremos compartir el recurso, y una vez elegidos, nos mostrará los datos relativos a ese recurso. En nuestro ejemplo algo como:

temp (\\<etiqueta>)
\\<etiqueta>\temp

Donde <etiqueta> es el nombre de nuestro ordenador.

Este nombre se puede sustituir por la IP, en nuestro ejemplo»127.0.0.1″, o por el nombre que tenga asignado en el DNS local, por ejemplo «localhost».

En general, la etiqueta corresponderá al nombre de un equipo conectado a la red, y el nombre de la unidad o carpeta compartida será diferente.

Las funciones para conectarse y desconectarse a una unidad remota tendrán esta forma, más o menos:

bool ConectarRecurso()
{
    NETRESOURCE nr;
    DWORD ret;
    const char *letra = "Z:";
    const char *recurso = "\\\\localhost\\temp";
    const char *usuario = "usuario";
    const char *password = "contraseña";
    bool conexionRemota;

    nr.dwType = RESOURCETYPE_ANY;
    nr.lpLocalName = (LPSTR)letra;
    nr.lpRemoteName = (LPSTR)recurso;
    nr.lpProvider = NULL;
    ret = WNetAddConnection2(&nr, password, usuario, 0);

    if(ret !=NO_ERROR)
        switch(ret) {
            case ERROR_ACCESS_DENIED: 
               std::printf("Access to the network resource was denied."); 
               break;
            case ERROR_ALREADY_ASSIGNED: 
               std::printf("The local device specified by lpLocalName is already connected to a network resource."); 
               break;
            case ERROR_BAD_DEV_TYPE: 
               std::printf("The type of local device and the type of network resource do not match."); 
               break;
            case ERROR_BAD_DEVICE: 
               std::printf("The value specified by lpLocalName is invalid."); 
               break;
            case ERROR_BAD_NET_NAME: 
               std::printf("The value specified by lpRemoteName is not acceptable to any network resource provider. The resource name is invalid, or the named resource cannot be located."); 
               break;
            case ERROR_BAD_PROFILE: 
               std::printf("The user profile is in an incorrect format."); 
               break;
            case ERROR_BAD_PROVIDER: 
               std::printf("The value specified by lpProvider does not match any provider."); 
               break;
            case ERROR_BAD_USERNAME: 
               std::printf("The specified user name is not valid."); 
               break;
            case ERROR_BUSY: 
               std::printf("The router or provider is busy, possibly initializing. The caller should retry."); 
               break;
            case ERROR_CANCELLED: 
               std::printf("The attempt to make the connection was cancelled by the user through a dialog box from one of the network resource providers, or by a called resource."); 
               break;
            case ERROR_CANNOT_OPEN_PROFILE: 
               std::printf("The system is unable to open the user profile to process persistent connections."); 
               break;
            case ERROR_DEVICE_ALREADY_REMEMBERED: 
               std::printf("An entry for the device specified in lpLocalName is already in the user profile."); 
               break;
            case ERROR_EXTENDED_ERROR: 
               std::printf("A network-specific error occured. Call the WNetGetLastError function to get a description of the error."); 
               break;
            case ERROR_INVALID_ADDRESS: 
               std::printf("An attempt was made to access an invalid address.");
               break;
            case ERROR_INVALID_PARAMETER: 
               std::printf("A parameter is incorrect.");
               break;
            case ERROR_INVALID_PASSWORD: 
               std::printf("The specified password is invalid."); 
               break;
            case ERROR_LOGON_FAILURE: 
               std::printf("A logon failure because of an unknown user name or a bad password."); 
               break;
            case ERROR_NO_NET_OR_BAD_PATH: 
               std::printf("A network component has not started, or the specified name could not be handled."); 
               break;
            case ERROR_NO_NETWORK: 
               std::printf("There is no network present."); 
               break;
            default: 
               std::printf("Error %d, desconocido", ret); 
               break;
        }
    conexionRemota = ((ret == NO_ERROR) || (ret == ERROR_ALREADY_ASSIGNED));
    return conexionRemota;
}

void DesconectarRecurso()
{
    const char *letra = "Z:";

    WNetCancelConnection2(letra, 0, TRUE);
}

Habría que sustituir «usuario» y «contraseña» por los valores adecuados en cada caso.

Se puede descargar un ejemplo completo desde aquí.

Recibir correo POP3

CorreoYa vimos en una entrada anterior cómo podemos enviar mensajes de correo desde una aplicación Windows. En esta entrada veremos cómo podemos recibir correo usando el protocolo POP3.

Explicaré algo de teoría (no mucha), pero no te preocupes, verás que leer correos será realmente sencillo, otra tema muy diferente será procesar su contenido.

Antes que nada, necesitaremos varias cosas para poder recibir correo:

  • Lo primero, por supuesto, es una cuenta de correo POP3. Puede servir, por ejemplo, una de Gmail. (Ver notas al final para ver una explicación de cómo se activa ese protocolo en Gmail).
  • Conocer el protocolo POP3, ya que lo usaremos directamente en nuestro programa.
  • Debido a que hay que tener ciertas medidas de seguridad, será necesario conseguir una librería para soporte del protocolo SSL.

El protocolo POP3

Se trata de un protocolo realmente simple. Si te interesa conocer más detalles, consulta esta entrada de la Wikipedia.

Lo que nos interesa ahora son las órdenes o comandos disponibles en ese protocolo. Como son pocas, las veremos todas.

  • USER <nombre> Identificación de usuario (Solo se realiza una vez).
  • PASS <password> Envía la clave del servidor.
  • STAT Da el número de mensajes no borrados en el buzón y su longitud total.
  • RETR <número> Solicita el envío del mensaje especificando el número (no se borra del buzón).
  • LIST Muestra todos los mensajes no borrados con su longitud.
  • NOOP Permite mantener la conexión abierta en caso de inactividad.
  • TOP <número> <líneas> Muestra la cabecera y el número de líneas requerido del mensaje especificando el número.
  • DELE <número> Borra el mensaje especificando el número.
  • RSET Recupera los mensajes borrados (en la conexión actual).
  • UIDL <número> Devuelve una cadena identificatoria del mensaje persistente a través de las sesiones. Si no se especifica <número> se devuelve una lista con los números de mensajes y su cadena identificatoria de los mensajes no borrados.
  • QUIT Salir.

Una sesión POP3 sigue este esquema:

  1. Establecer una conexión con el servidor, se puede usar telnet, o sockets. Nosotros usaremos una librería, de modo que esta parte es transparente en nuestro caso.
  2. Identificarse, usando el comando USER.
  3. Enviar la contraseña, usando el comando PASS.
  4. Ahora, los comandos a utilizar dependerán de lo que queramos hacer. Si sólo nos interesa saber si hay correo pendiente de leer, podemos usar el comando STAT, que nos devuelve el número total de mensajes y su tamaño en conjunto. Si queremos recuperar los mensajes, tendremos que usar el comando LIST para obtener una lista de los mensajes disponibles, con su número y tamaño, y el comando RETR para recuperar cada uno de ellos. Opcionalmente podemos borrarlos después de descargarlos, usando DELE.
  5. Cerrar la sesión, usando el comando QUIT.

Establecer una conexión segura SSL

El protocolo POP3 no es seguro. Aunque haya que identificarse, las claves se transmiten por la red en forma de texto, por lo que es posible que sean interceptadas.

Para evitar esto, lo normal es usar POP3 sobre SSL, puedes informarte sobre ello en Wikipedia.

Afortunadamente para nosotros, no tenemos que preocuparnos demasiado por esta capa, ya que hay librerías en Internet que permiten conectarse a servidores usando SSL.

En concreto, usaremos OpenSSL, cuya página oficial es http://www.openssl.org.

Y más concretamente, para Windows, usaremos el paquete que se puede descargar desde http://gnuwin32.sourceforge.net/packages/openssl.htm, en esa página descargaremos el primer fichero, «Complete package, except sources», que es un ejecutable que contiene la documentación, bibliotecas dinámicas, estáticas y ficheros de cabecera.

Como con otras librerías, copiaremos cada fichero en el lugar correspondiente:

  • Los ficheros de cabecera, que están en la carpeta «openssl», dentro de la carpeta «include», los copiaremos a la carpeta «include» de nuestro compilador. Copiaremos la carpeta completa «openssl», no sólo los ficheros de cabecera.
  • Los ficheros de biblioteca estática, con la extensión «.a», que están en la carpeta «lib», los copiaremos a la carpeta «lib» de nuestro compilador.
  • Los ficheros de biblioteca de enlace dinámico (DLL), que están en la carpeta «bin», los copiaremos a la carpeta de trabajo de nuestra aplicación, o en un lugar donde sean localizables por el sistema.

Funciones de inicio

Las librerías de OpenSSL disponen de muchas funciones, pero la mayor parte de ellas no las vamos a necesitar para obtener mensajes. Entre ellas usaremos las siguientes:

SSL_library_init()
Debe ser invocada antes que tenga lugar cualquier otra acción. Registra algunos de los algoritmos necesarios para usar SSL.
SSL_load_error_strings()
Registra los mensajes de error.
ERR_load_BIO_strings()
Registra los mensajes de la librería BIO.
OpenSSL_add_all_algorithms()
Registra y crea una tabla interna de algoritmos SSL.
SSL_CTX
Objeto creado como un marco para establecer conexiones TLS/SSL. Varias opciones, como certificados, algoritmos, etc, se pueden asignar mediante este objeto.
SSL_CTX_new()
Crea un objeto SSL_CTX, el valor de retorno es un puntero a un objeto de este tipo.
SSLv23_client_method()
El valor de retorno de esta función se usa como argumento de la anterior.
BIO
Objeto para establecer conexiones.
BIO_new_ssl_connect()
Establece una conexión y devuelve un puntero a un objeto BIO. Como argumento se usa un objeto de tipo SSL_CTX.
SSL
Objeto para conexión segura.
BIO_get_ssl()
recupera el puntero SSL de BIO, que puede ser entonces manipulado usando SSL estándar. Como primer argumento se debe especificar un objeto BIO, y como segundo un objeto SSL.
SSL_set_mode()
Asigna el modo a un SSL. El primer argumento es un puntero a un objeto SSL, el segundo el modo.

Todas estas funciones son sólo para preparar la conexión. No te preocupes si no está claro cómo funcionan en conjunto (ni por separado), el bloque de código para iniciar SSL queda como sigue:

    SSL *tunelSSL;
    BIO *conexionBIO;
    SSL_CTX *datosConexion;

    /* Inicializacion */
    SSL_library_init();
    SSL_load_error_strings();
    ERR_load_BIO_strings();
    OpenSSL_add_all_algorithms();
    datosConexion=SSL_CTX_new(SSLv23_client_method());
    conexionBIO = BIO_new_ssl_connect(datosConexion);
    BIO_get_ssl(conexionBIO, &tunelSSL);
    SSL_set_mode(tunelSSL, SSL_MODE_AUTO_RETRY);

Funciones de cierre

De forma simétrica, cuando terminemos la tarea, deberemos cerrarlo todo. Para ello usaremos las siguientes funciones:

BIO_reset()
Retorna el objeto BIO, cuyo puntero se pasa como argumento, a su estado inicial.
BIO_free_all()
Libera toda la cadena BIO, de nuevo pasamos un puntero al objeto BIO como argumento.
SSL_CTX_free()
Libera el objeto SSL_CTX cuyo puntero se pasa como argumento.

El proceso de cierre queda así:

    BIO_reset(conexionBIO);
    BIO_free_all(conexionBIO);
    SSL_CTX_free(datosConexion);

Funciones de conexión, lectura y escritura

Entre la inicialización y el cierre es donde haremos nuestra tarea, usando el protocolo POP3.

Primero, indicamos con qué servidor queremos establecer una conexión, usando la función BIO_set_conn_hostname. El primer argumento es un puntero a un objeto BIO, que hemos obtenido en la inicialización. El segundo es una cadena con el nombre y puerto del servidor. En este ejemplo, el de Gmail.

    BIO_set_conn_hostname(conexionBIO,"pop.gmail.com:995");

Para establecer la conexión se usa la función BIO_do_connect, indicando como argumento un puntero a un objeto BIO (el mismo objeto). Si el valor de retorno es menor o igual a cero, es que no se ha podido establecer la conexión.

    if(BIO_do_connect(conexionBIO) <= 0) cout << "ERROR: No es posible crear la conexion." << endl;

En este punto estamos conectados al servidor, y recibiremos un mensaje desde él. Para recuperarlo usamos la función BIO_read, indicando en el primer parámetro el puntero al objeto BIO, en el segundo la dirección del buffer de entrada, y en el tercero, el tamaño máximo de bytes a leer. El valor de retorno es el número de bytes leídos.

    int nbytes=0;
    char BufferDeSalida[512];
    char BufferDeEntrada[1025];

    nbytes=BIO_read(conexionBIO, BufferDeEntrada, 1024);
    BufferDeEntrada[nbytes] = 0;
    cout << BufferDeEntrada << endl;

Recibiremos un mensaje de bienvenida, cuyo texto depende del servidor. Aunque siempre empezará por «+OK». En el caso de Gmail tiene esta forma:

+OK Gpop ready for requests from nnn.nnn.nnn.nnn <id>

Básicamente indica que está esperando a que nos identifiquemos. Para eso, lo primero es enviar nuestro nombre de usuario, usando el protocolo POP3, es decir, con el comando «USER». Usaremos la función BIO_write, indicando como primer argumento el puntero a la estructura BIO, como segundo la dirección de la cadena con el comando, terminada en ‘\n’, y como tercero, la longitud de la cadena. El servidor responderá con otra cadena de reconocimiento o de error, dependiendo de si el usuario es reconocido o no.

    strcpy(BufferDeSalida,"USER usuario@gmail.com\n"); // Cambia la cadena según  tu caso
    BIO_write(conexionBIO, BufferDeSalida, strlen(BufferDeSalida));

    nbytes=BIO_read(conexionBIO, BufferDeEntrada,sizeof(BufferDeEntrada));
    BufferDeEntrada[nbytes] = 0;
    cout << BufferDeEntrada << endl;

La respuesta será de este tipo, más o menos:

+OK send PASS

Ahora hay que enviar la contraseña, usando el comando «PASS».

    strcpy(BufferDeSalida,"PASS contraseña\n");
    BIO_write(conexionBIO, BufferDeSalida, strlen(BufferDeSalida));

    nbytes=BIO_read(conexionBIO, BufferDeEntrada, 1024);
    BufferDeEntrada[nbytes] = 0;
    cout << BufferDeEntrada << endl;

En este caso, la respuesta, si el usuario y contraseña son correctos, será de este tipo:

+OK Welcome.

En caso de no ser reconocido, será un mensaje de error:

-ERR [AUTH] Username and password not accepted.

Es el momento de empezar a comunicarse con el servidor de correo para recuperar información.

Podemos, por ejemplo, obtener el número de mensajes pendientes de leer, usando el comando «STAT». Como respuesta obtendremos una cadena con el formato:

+OK <n> <tamaño>

Donde <n> es el número de mensajes y <tamaño> el número de bytes que ocupan todos los mensajes en total.

Podemos usar el comando «LIST» para obtener una lista de los mensajes, cada uno con su tamaño. Como respuesta obtendremos un mensaje de varias líneas, de este tipo:

+OK 5 messages:
1 31052
2 7460
3 12834
4 13749
5 45141
.

Observa que cuando la respuesta tiene más de una línea, la última contiene un punto.

Hay que tener cuidado cuando las repuestas pueden ser muy largas. Es el caso del comando «LIST» y de «RETR». Las lecturas de respuestas largas pueden requerir llamar a la función BIO_read varias veces, porque el buffer de entrada no será lo bastante grande para leer la respuesta completa.

Otro problema es que no podemos conocer con antelación el tamaño de la respuesta que tenemos que leer. En el caso del comando «LIST», sabemos el número de líneas, pero no el tamaño de cada una. En el caso del comando «RETR», sabemos el tamaño del mensaje, pero no podemos tener una confianza ciega en ese dato.

Por ejemplo, me he encontrado con que algunos mensajes de Gmail son mucho más largos que lo que se indica en la respuesta al comando «LIST».

Hay dos posibles soluciones que se pueden aplicar a este problema:

  1. Crear un buffer de lectura tan grande que garantice que tengamos espacio suficiente.
  2. Agrandar el buffer cada vez que se detecte que una lectura producirá un desbordamiento.

En el caso de lecturas de respuestas a la orden «LIST», podemos asumir un tamaño de buffer que permita almacenar 14 caracteres por línea. El número de líneas lo podemos conocer mediante una orden «STAT». Podemos considerar que 14 caracteres por línea es más que suficiente, considerando 4 para el número de mensaje, un espacio separador, 7 para el tamaño del mensaje y dos para el CRLF final.

En el caso de los mensajes, si la información del servidor no es fiable, no hay forma de predecir el tamaño. Por lo tanto optaremos por redimensionar el buffer cada vez que se nos quede pequeño.

Para averiguar cuando se nos acaba el buffer usaremos un bucle en el que primero intentaremos leer tantos caracteres como quepan en él, y después averiguaremos si quedan caracteres pendientes de leer, usando la función BIO_ctrl_pending. Si quedan caracteres por leer, agrandaremos el buffer en el tamaño suficiente para que quepan los caracteres pendientes.

La condición de salida será que en la última lectura, los últimos caracteres sean un punto seguido de CRLF:

    do {
        leido = BIO_read(cBIO, &buffer[nbytes], tambuffer-nbytes);
        nbytes += leido;
        if(BIO_ctrl_pending(cBIO) > 0) {
            // Reasignar buffer.
            Reubicar(tambuffer+BIO_ctrl_pending(cBIO)+1);
            continue;
        }
    } while(buffer[nbytes-3] != '.' || buffer[nbytes-2] != '\r' || buffer[nbytes-1] != '\n');

Procesar mensajes

Cada mensaje se puede dividir en dos partes, las cabeceras y el cuerpo. La separación es una simple línea en blanco, es decir, una línea con un CRLF.

La cabecera contiene información sobre el remitente y el destinatario del mensaje, los servidores por los que ha pasado, fecha de creación, asunto, etc. Los datos no son fijos, ni en el orden ni en la cantidad ni en tipo. Puede haber más o menos, algunos pueden no estar presentes siempre. Los más habituales, que suelen aparecer casi siempre son:

  • Delivered-To: dirección de correo de entrega.
  • Received: que puede y suele aparecer varias veces. Permite conocer el camino que ha seguido el mensaje desde su origen hasta su destino. Cada entrada contiene información sobre el servidor, la hora a la que se procesó el mensaje.
  • Return-Path: dirección de respuesta por defecto, en caso de que el mensaje no pudiera ser enviado. Es la dirección de «rebote». Los servidores pueden procesar esos rebotes para saber si un usuario no ha recibido el mensaje, o si la dirección no existe, o si tiene algún error.
  • From: dirección de correo del remitente, a menudo contiene también un nombre, en el formato nombre <direccion de correo>.
  • Message-ID: un identificador único del mensaje. Estos identificadores se usan para identificar el mensaje, y para establecer jerarquías de mensajes, para determinar qué mensajes responden a cuales.
  • To: dirección del destinatario original del mensaje. No tiene por qué coincidir con el valor de Delivered-To, por ejemplo, si el destinatario es una lista de correo, o está dirigido a varios destinatarios.
  • Date: fecha de creación del mensaje.
  • Importance: prioridad del mensaje, generalmente definida por el remitente.
  • Precedence: indica el tipo de mensaje, puede ser bulk (basura), junk (masivo), list (lista) o un tipo definido por alguna aplicación. Está previsto para que determinados clientes de correo respondan de diferente modo, dependiendo del tipo de mensaje.
  • Reply-To: dirección de correo que se usará para responder al mensaje.
  • Sender: dirección de correo del que ha enviado el mensaje. Puede no coincidir con el campo From, por ejemplo, si se trata de una lista de correo, el que ha escrito el mensaje lo envía a la lista, pero es la lista la que distribuye los mensajes al resto de los miembros, por eso aparecerá la dirección de la lista como «Sender».
  • In-Reply-To: contiene el identificador del mensaje del que el actual es respuesta.
  • References: contiene una lista de identificadores de mensaje que establece una cadena de respuestas.
  • MIME-Version: versión de la codificación MIME usada para codificar el mensaje, normalmente 1.0.
  • Subject: texto del asunto del mensaje.
  • Errors-To: dirección de correo para notificar errores.
  • Content-Type: tipo del contenido del mensaje, dependiendo del formato: texto, html o ambos, de si tiene adjuntos, etc. Este campo es más complicado, y lo veremos en próximas entradas del blog.

Como los nombres pueden estar codificados en diferentes formatos, a menudo en los textos en «From», «Subject», etc. aparece también la información del conjunto de caracteres en el que está codificada la cadena. Por ejemplo: =?iso-8859-1?x?texto?=

Aparecen tres campos, el primero entre =? y ? es el nombre del conjunto de caracteres. El segundo, entre ? y ?, el número de caracteres que contiene la cadena codificada. El tercero, entre ? y ?= la propia cadena.

En próximas entradas del blog veremos más sobre cómo decodificar el formato MIME de los mensajes. De momento nos conformaremos con procesar las cabeceras.

Aplicaciones

Las aplicaciones de leer mensajes desde nuestro programas son muchas. Por ejemplo, podemos crear un programa que nos avise cuando tenemos correo nuevo, o crear un filtro que borre del servidor determinados mensajes. Podríamos crear nuestro propio programa de correo electrónico, o algo más simple, como un programa que sea capaz de procesar órdenes recibidas por correo. Programas para crear listas de correo, etc.

En esta entrada voy a añadir dos programas de ejemplo.

Leer mensajes en consola

El primero se limita a leer mensajes desde un buzón y mostrar una lista de asuntos, remitentes y fechas. Los detalles de la cuenta están codificados en el programa fuente, de modo que habrá que compilarlo para cada caso. De todos modos, es relativamente simple leer esos valores desde una base de datos o desde un fichero de texto. Por supuesto, un programa seguro requeriría encriptar las contraseñas, pero eso está fuera del objetivo de esta entrada.

Descargar desde aquí.

Notificación de correo nuevo

Aprovechando una entrada anterior de este blog, insertar iconos en el área de notificación, haremos un programa que verifique una dirección de correo cada cinco minutos, y muestre un icono en el área de notificación cuando haya correo nuevo.

Descargar desde aquí.

Nota: Para activar el protocolo POP3 en Gmail, hay que ir al menú de configuración y buscar la pestaña «Reenvío y correo POP/IMAP». El punto uno permite habilitar POP3 para los nuevos mensajes que se reciban a partir de ahora, o para todos los mensajes almacenados en la cuenta.

Codificación base64

Base 64Base 64 es un sistema de numeración, como el decimal, el binario o el hexadecimal, pero que usa 64 símbolos.

En la práctica no se usa tanto para codificar números, sino sobre todo para codificar datos binarios. El motivo es que usa 64 símbolos imprimibles: 62 de los símbolos consisten en el alfabeto en mayúsculas y en minúsculas y los dígitos 0 a 9. Para los otros dos se usan distintos caracteres, dependiendo de la versión particular del código. Los más habituales son el ‘+’ y el ‘/’.

Esto hace que cualquier valor codificado en base 64 sea imprimible, lo que a su vez permite que se puedan visualizar con un editor de textos, o eludir los problemas que suelen aparecer con caracteres especiales en transmisiones de datos o manejo de ficheros.

Se suele usar base 64 para codificar los ficheros binarios que se envían adjuntos con mensajes de correo, por ejemplo. Pero se puede usar en otras muchas cosas.

En mi caso, lo he usado para transferir datos binarios usando una línea serie, ya que si lo hacía en binario surgían algunos problemas con algunos valores que tienen un significado especial para el sistema operativo.

Esto en cuanto a ventajas. Entre los inconvenientes cabe mencionar que se necesitan más bytes en la versión codificada en base 64 que en la binaria. La relación es de 4/3, es decir, por cada tres bytes binarios se necesitan cuatro en base 64. Esto es así porque sólo se usan 64 valores de cada byte, es decir, 6 bits de los 8 disponibles, 8/6 equivale a 4/3.

Empaquetado de bits

La idea es tomar los 24 bits correspondientes a tres bytes a codificar y repartirlos en cuatro bytes, de modo que sólo se usen seis de los ocho bits de cada uno. Eso nos da cuatro valores entre 0 y 63 que usaremos como índice en un array de caracteres que contiene los siguientes valores:

ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/

Así, al valor 0 le corresponde el carácter ‘A’, al 1 el carácter ‘B’, etc.

Conversión base 64La forma de repartir esos bits es simple:

  • Se toman los seis bits de mayor peso del primer byte de origen y se copian a los seis bits de menor peso del primer byte de destino.
  • Se toman los dos bits de menor peso del primer byte de origen y los cuatro de mayor peso del segundo y se copian al segundo byte de destino.
  • Se toman los cuatro bits de menor peso del segundo byte de origen y los dos de mayor peso del tercero y se copian al tercer byte de destino.
  • Se toman los seis bits del tercer byte de origen y se copian al cuarto byte de destino.

Ahora los cuatro bytes de destino contienen valores entre 0 y 63. Esos valores se toman como índice para obtener el carácter correspondiente en la codificación base 64.

A la hora de codificar en base 64 hay que tener en cuenta un detalle extra. La longitud de los datos de entrada no será siempre un múltiplo exacto de tres. Cuando no lo sea, en la última iteración de conversión pueden quedar uno o dos bytes. Si es uno, la conversión requiere sólo dos bytes, y si es dos requerirá tres.

Sin embargo, la longitud de los datos de salida siempre será un múltiplo de cuatro, de modo que si en la última iteración sólo queda un byte por convertir se calculan los dos bytes de salida correspondientes y se añaden dos caracteres especiales, que no forman parte de los caracteres permitidos en base 64. El carácter que se usa es el ‘=’. Si en la última iteración quedan dos bytes, se calculan los tres bytes de salida correspondientes y se añade un carácter de relleno.

Esto asegura que será posible no sólo hacer la conversión inversa, sino que además la longitud será la correcta.

Texto E s t
ASCII 69 115 116
Binario 0 1 0 0 0 1 0 1 0 1 1 1 0 0 1 1 0 1 1 1 0 1 0 0
Índice 17 23 13 52
Base 64 R X N 0

En caso de que sobren dos caracteres, la conversión queda así:

Texto E s
ASCII 69 115
Binario 0 1 0 0 0 1 0 1 0 1 1 1 0 0 1 1 0 0 0 0 0 0 0 0
Índice 17 23 12
Base 64 R X M =

Y en caso de que sobre uno:

Texto E
ASCII 69
Binario 0 1 0 0 0 1 0 1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
Índice 17 16
Base 64 R Q = =

Codificación en C++

Este es un ejemplo de funciones en C++ para convertir a y desde Base 64:


int CodificarB64(unsigned char* entrada, char*salida , int l) {
    char b64[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
    int i=0, j=0;
    unsigned char A, B, C;

    // Bloques de tres bytes:
    entrada[l]=0;
    if(l%3==1) entrada[l+1]=0;
    if(l%3==2) entrada[l+2]=0;
    for(i = 0; i < l; i+=3){
        A = entrada[i];
        B = entrada[i+1];
        C = entrada[i+2];
        salida[j++] = b64[A>>2];
        salida[j++] = b64[((A<<4)+(B>>4)) & 0x3f];
        if(i+1 < l) salida[j++] = b64[((B<<2)+(C>>6)) & 0x3f]; else salida[j++] = '=';
        if(i+2 < l) salida[j++] = b64[C & 0x3f]; else salida[j++] = '=';
    }
    salida[j] = 0;
    return j;
}

char Indice(const char c) {
    if(isdigit(c)) return c-'0'+52;
    if(isupper(c)) return c-'A';
    if(islower(c)) return c-'a'+26;
    if(c=='+') return 62;
    if(c=='/') return 63;
    return 0;
}

int DecodificarB64(char* entrada, unsigned char* salida) {
    int i=0, j=0;
    unsigned char A, B, C, D;

    // Bloques de tres bytes:
    while(entrada[i]) {
        A = Indice(entrada[i++]);
        B = Indice(entrada[i++]);
        C = Indice(entrada[i++]);
        D = Indice(entrada[i++]);

        salida[j++] = (unsigned char)((A << 2) + (B >> 4));
        salida[j++] = (unsigned char)((B << 4) + (C >> 2));
        salida[j++] = (unsigned char)((C << 6) + D);
    }
    return j;
}

Validar datos en C++

Validación de datosEn aplicaciones de consola C++, uno de los puntos más problemáticos es la lectura de datos por parte del usuario.

Las funciones de entrada C y los métodos del stream cin resultan poco potentes a la hora de hacer lecturas discriminadas de número enteros, o en coma flotante, fechas o cadenas que se ajuste a un formato determinado.

Después de hacer algunos programas, todos nos damos cuenta de que es mala idea usar el operador >> de cin para leer un número. Si el usuario introduce una cadena no numérica, el valor leído es cero, pero la cadena permanece en el buffer de entrada del teclado, y se intenta leer en sucesivas lecturas.

El resultado es que no se leen los datos que se pretenden leer, y generalmente el programa termina por entrar en un bucle infinito o, en el mejor de los datos, da un resultado incorrecto.

Cuando leemos cadenas también existe peligro, concretamente, de overbuffering, es decir, de sobrepasar el espacio de memoria correspondiente a la cadena a leer.

Esto es más cierto cuando leemos cadenas C, en forma de array de caracteres terminadas en nulo. Si usamos objetos de tipo string este peligro no existe.

Con esto en mente, parece claro que es mejor leer siempre cadenas. En el caso de querer capturar valores numéricos, se debe validar la cadena leída. Si la validación tiene éxito se convierte el valor leído a número, y si no, se repite el proceso.

Generalizando, para validar cualquier formato de dato, el proceso es el siguiente:

  • Leer una cadena.
  • Verificar si el formato es correcto.
    • Si es correcto: retornar el valor leído.
    • Si no es correcto: volver al principio.

Leer una cadena

Parece una tarea sencilla, ¿no?

Tal vez no tanto…

Veamos. Si usamos el operador >> de cin para leer un objeto de la clase string se pueden presentar algunos problemas. Por ejemplo, el operador >> deja de leer cuando encuentra un espacio, y además, no lo retira del buffer.

Si intentamos leer un número, y el usuario escribe «varias palabras y un numero 34», se leerá «varias» en la cadena, y el resto quedará en el buffer para siguientes lecturas.

Si usamos el método getline, pronto descubriremos que no sirve para leer objetos de la clase string, sino sólo cadenas terminadas en nulo.

Existe una función getline que evita todos estos inconvenientes: lee objetos string y lee espacios. En el primer argumento debemos indicar el objeto cin, y en el segundo, la cadena a leer:

    string cad;
    getline(cin, cad);

De este modo nos aseguramos de que cad contiene una cadena con todo lo introducido por el usuario, y podemos pasar al siguiente paso.

Verificar el formato correcto

Por supuesto, esta tarea es diferente dependiendo del formato del dato a leer.

Si se trata de un entero, y no somos demasiado exigentes con el formato, podemos usar un stream de cadena para verificar la entrada del usuario.

    int i;
    stringstream mystream(cad);
    if(mystream >> i) cout << "entero leido" << endl;
    else cout << "error" << endl;

En este ejemplo usamos un stringstream creado a partir de la cadena leída, y después intentamos leer un entero a partir de ella. Si al comienzo de la cadena hay un entero, su valor pasa a i, el caso contrario indicará un error.

Digo que «si no somos demasiado exigentes», porque hay muchas cadenas que darán como resultado un entero, aunque no contengan sólo un puntero, por ejemplo «123.3», «123,4» o «123abc».

Si se trata de un número en coma flotante, nos sirve el mismo método, pero usando una variable float o double para la lectura. Este método interpretará correctamente entradas en notación científica, como «1e3», o «1.3e2».

Insistir hasta tener un resultado válido

El último paso es repetir la lectura si el valor leído no es válido.

Para ello pondremos todo el código anterior dentro de un bucle. Por ejemplo, para leer un entero:

    int i;
    string cad;

    while(true) {
        cout << "Introduce un entero: ";
        getline(cin, cad);
        stringstream mystream(cad);
        if(mystream >> i) break;
        cout << "error" << endl;
    }
    cout << "Valor: " << i << endl;

Podemos introducir todo este código en una función, si tenemos que leer varios números, en la que se acepte como entrada una cadena, y como valor de retorno un entero.

Verificación general

El método anterior es suficiente en los casos generales de lectura de números, pero a menudo tenemos que verificar otros tipos de datos, como fechas, o cadenas que se ajusten a un formato concreto.

Estas verificaciones generalmente requieren verificar cada carácter de la cadena capturada para comprobar si se ajusta a un patrón determinado. Por ejemplo, una fecha debe constar de dos dígitos, seguidos de un carácter separador, otros dos dígitos, otro carácter separador y finalmente otros cuatro dígitos. Además los dos primeros dígitos deben estar en el intervalo entre 1 y el número de días correspondiente al mes y año, los segundos entre 1 y 12, ya que se refieren a un mes, y los últimos cuatro, deben estar en el rango de fechas válidas para nuestra aplicación. Esto será variable, dependiendo de cada caso.

Otras lecturas deben ajustarse a otros patrones, a veces más simples y otras más complejos.

Si disponemos de soporte para expresiones regulares, la primera parte de la verificación puede ser más sencilla, y en muchos casos no necesitaremos más.

C++11 tiene soporte para expresiones regulares, dentro de las bibliotecas estándar, concretamente, en <regex>.

En cualquier caso, existen bibliotecas que implementan expresiones regulares para C, por ejemplo, pcre.

Sin embargo, la mayor parte de las veces no es necesario usar esta herramienta, y podemos simplemente verificar si la cadena se ajusta a determinadas reglas o no.

Como ejemplo, veamos una función de validación para capturar valores enteros dentro de un intervalo.

int Intervalo(string msg, int v1, int v2) {
    int i;
    string cad;
    bool valido = false;

    do {
        cout << msg;
        getline(cin, cad);
        stringstream mystream(cad);
        if(mystream >> i) {
            if(i >= v1 && i <= v2) valido = true;
        }
        if(!valido) cout << "error" << endl;
    } while(!valido);
    return i;
}
...
    i = Intervalo("Introduce un entero entre 100 y 200: ", 100, 200);
    cout << "Valor: " << i << endl;

Fuentes: cplusplus.com

Acentos y eñes en programas de consola de Windows

Letra Ñ Cuando creamos programas para consola de Windows hay algo que resulta muy frustrante: las eñes, cedillas y acentos no se muestran como debieran, y en su lugar se ven unos caracteres raros que dificultan la lectura de los textos y resultan muy antiestéticos.

Ante este problema se nos ocurren algunas soluciones, más o menos ingeniosas:

La primera, prescindir de esos caracteres: eliminamos los acentos y evitamos palabras con ‘ñ’. Esto no siempre es posible, y como solución, sinceramente, resulta un intento bastante mediocre.

La segunda consiste en editar el código fuente desde un editor de consola. Haciendo esto, el editor usa el mismo conjunto de caracteres que el fichero ejecutable, y la salida del programa coincide con lo que hemos escrito antes. El resultado es mejor, pero resulta bastante incómodo usar varios editores para cada proyecto.

La tercera es incluso más incómoda. Consiste en escribir las cadenas en una consola, y a continuación cortar y pegar el texto en el editor. Y lo peor, a veces este truco ni siquiera funciona.

Pero, ¿por qué pasa esto?

Tal vez nos resultaría más sencillo solucionar el problema si supiéramos exactamente por qué se produce.

La raíz del problema es que Windows usa un código de caracteres (o código de página) diferente para el entorno GUI y para la consola. Para configuraciones de Windows en español, en GUI se usa el código 1252 y para la consola se usa el código 850.

Si usamos un editor de textos GUI para escribir el código de un programa para consola, se usarán códigos de página diferentes en la edición y en la ejecución, y el resultado será el galimatías al que ya estamos acostumbrados.

Soluciones más interesantes

Conociendo el origen del problema es más sencillo encontrar una solución, o al menos, debería serlo.

Una primera idea sería configurar el editor para que use el código de página 850. Desgraciadamente esto no es siempre posible. En las opciones del editor de Code::Blocks, por ejemplo, no está disponible el código de página 850.

Eso nos deja la solución final, que consiste en usar un par de funciones del API de Windows.

Primero, usaremos la función SetConsoleOutputCP. Que, como su propio nombre indica, sirve para asignar un código de página a la salida de consola. Usaremos el valor 1252, que es el código de página que queremos usar, ya que es el que hemos usado para escribir el código fuente.

#include <iostream>
#include <windows.h>

using namespace std;

int main()
{
    SetConsoleOutputCP(1252);
    cout << "áéíóúñÑçÇüÜ" << endl;
    return 0;
}

Segundo, debemos asegurarnos de que nuestra consola usa fuentes Unicode, de otro modo, el código de página asignado será ignorado y no habremos conseguido nada. En la página de Microsoft hay un artículo titulado «SetConsoleOutputCP sólo es efectivo con fuentes Unicode«:

Hacer esto es sencillo, basta seguir estos pasos:

  1. Abrir una consola Windows.
  2. Desplegar el menú del sistema (en el icono de la parte superior izquierda de la ventana) y seleccionar «Predeterminados».
  3. Activar la pestaña «Fuentes».
  4. Seleccionar una fuente TrueType, si está disponible, en lugar de «Fuentes de mapa de bits». (Yo tengo «Lucida console».)

De este modo, la configuración será la usada para todas las consolas a partir de este momento.

Si sólo están disponibles fuentes de mapas de bits, mucho me temo que esta solución no es viable.

Efectos colaterales

Si nuestro programa de consola tiene que leer cadenas nos encontraremos con otro problema inesperado. Las cadenas que introduzcamos por teclado, si contienen acentos o eñes se verán correctamente, pero si posteriormente las volvemos a mostrar en pantalla veremos que vuelven a aparecer caracteres extraños.

El motivo es simple, el código de página para consola de entrada y salida no tiene por qué ser el mismo. La función SetConsoleOutputCP sólo cambia el código de página para la salida, pero el de entrada sigue siendo el 850. Así, cuando introducimos una cadena, esta se codifica incorrectamente.

La función para cambiar el código de página de entrada no es, como podría esperarse, SetConsoleInputCP. Esa función no existe (a saber por qué), la función correcta es SetConsoleCP.

#include <iostream>
#include <windows.h>

using namespace std;

int main()
{
    char cad[256];

    SetConsoleOutputCP(1252);
    SetConsoleCP(1252);
    cout << "áéíóúñÑçÇçüÜ" << endl;
    cout << "Introduce una cadena con acentos y eñes: ";
    cin >> cad;
    cout << "La cadena es: " << cad;
    return 0;
}

No es una solución ideal, lo sé, pero puede solucionar muchos problemas cuando programamos para consola.

Otra ventaja es que las cadenas leídas de este modo se codifican internamente usando el código de página de Windows GUI, de modo que es una forma de que cualquier dato leído de este modo se visualice correctamente en una aplicación GUI.

Imágenes dinámicas en PHP

Paleta de coloresPHP dispone de varias extensiones para el tratamiento y generación de imágenes.

En esta entrada usaremos la extensión GD que permite crear y manipular imágenes en varios formatos, como GIF, PNG, JPEG, SWF, TIFF y JPEG2000.

Las utilidades de crear imágenes usando PHP son muchas. Yo, por ejemplo, las he usado para crear contadores o imágenes de botones y menús, que se pueden personalizar en función de opciones del usuario.

Crear imágenes

Un fichero php de imagen se comporta exactamente igual que una imagen. Por ejemplo, podemos insertar una imagen dentro de una página usando la etiqueta img, de cualquiera de estos modos:

<img src="imagen.jpg" width="800" height="600" alt="imagen">
<img src=imagen.php width="800" height="600" alt="imagen">

En el primer caso se muestra la imagen almacenada el el fichero «imagen.jpg». En el segundo se muestra la imagen generada por el script «imagen.php».

Lo que sigue es un ejemplo de imagen generada en php:

<?php
    // Representación de funciones
    // Mayo de 2013, Con Clase
    // Salvador Pozo
    header('Content-type: image/png');

    $im = ImageCreateTrueColor(640, 480);
    $azul = ImageColorAllocate($im, 128, 128, 255);
    $azul2 = ImageColorAllocate($im, 64, 64, 230);
    $verde = ImageColorAllocate($im, 0, 64, 0);
    $verde2 = ImageColorAllocate($im, 0, 200, 0);

    // Ejes de coordenadas:
    ImageLine($im, 0, 240, 640, 240, $verde2);
    ImageLine($im, 320, 0, 320, 480, $verde2);

    // Lineas de escala:
    for($i = 50; $i < 320; $i+=50) {
        ImageLine($im, 320+$i, 0, 320+$i, 480, $verde);
        ImageLine($im, 320-$i, 0, 320-$i, 480, $verde);
    }

    for($i = 50; $i < 240; $i+=50) {
        ImageLine($im, 0, 240+$i, 640, 240+$i, $verde);
        ImageLine($im, 0, 240-$i, 640, 240-$i, $verde);
    }

    $y0 = $y1 = 240;
    $x0 = $x1 = 0;
    for($x1 = 0; $x1 < 640; $x1++) {
        $y1 = 240 + 200*sin($x1/25);
        ImageLine($im, $x0, $y0, $x1, $y1, $azul);
        $x0 = $x1;
        $y0 = $y1;
    }

    $y0 = $y1 = 240;
    $x0 = $x1 = 0;
    for($x1 = 0; $x1 < 640; $x1++) {
        $y1 = 240 + 150*cos($x1/35);
        ImageLine($im, $x0, $y0, $x1, $y1, $azul2);
        $x0 = $x1;
        $y0 = $y1;
    }

    ImagePng($im);
    ImageDestroy($im);
?>

Gráfica PHPLo primero que se debe hacer es enviar una cabecera con el tipo de contenido de la imagen. En este caso se trata de una imagen en formato png, pero se pueden generar otros formatos, como gif o jpg.

El segundo paso consiste en crear un recurso de imagen, usando la función ImageCreateTrueColor, especificando las dimensiones de la imagen en pixels en los dos argumentos, ancho y alto. También podemos crear recursos de imagen a partir de imágenes existentes, usando funciones como ImageCreateFromJpeg o ImageCreateFromGif, por ejemplo. En esos casos no es necesario especificar dimensiones, sólo la ruta de la imagen a usar como fuente.

En este ejemplo hemos creado cuatro plumas de cuatro colores diferentes, usando ImageColorAllocate. En esta función, el primer argumento es el recurso de imagen que hemos creado antes, y los tres restantes definen los componentes rojo, verde y azul, respectivamente. Se puede usar la función ImageColorAllocateAlpha, y añadir un quinto argumento, con la componente de transparencia, α.

A continuación trazamos el dibujo deseado, la extensión gd proporciona varias funciones para trazar líneas, arcos, elipses, rectángulos, etc. Para más detalles, consulta la documentación de php.

Parametrizar imágenes

Una ventaja de generar nuestras propias imágenes es que podemos parametrizarlas.

Veamos, como ejemplo, cómo crear una imagen para un contador. Pasaremos como parámetros al script la longitud, en número de dígitos, el valor del contador, el tamaño y el color.

<img src=contador.php?con=1234&lon=6&tam=20&col=114422 width="111" height="24" alt="1234">

El código para este script puede tener esta forma:

<?php
    // Genera imagen de contador
    // lon: longitud en dígitos
    // con: valor del contador
    // tam: tamaño de la fuente
    // col: color en hexadecimal: rrggbb
    // Mayo de 2013, Con Clase
    // Salvador Pozo

    Header("Content-type: image/png");

    if(isset($_GET["lon"])) {
      $lon = (int)$_GET["lon"];
    } else $lon = 4;
    if(isset($_GET["con"])) {
      $con = (int)$_GET["con"];
    } else $con = 0;
    if(isset($_GET["tam"])) {
      $tam = (int)$_GET["tam"];
    } else $tam = 15;
    if(isset($_GET["col"])) {
      $col = "0x".$_GET["col"];
    } else $col = "0x00c000";

    $b = (0+$col) % 256;
    $col = ($col-$b)/256;
    $g = $col % 256;
    $r = ($col-$b)/256;

    $contador = sprintf("%0".$lon."d", $con);
    $fuente = './fuentes/ariblk.ttf';
    $box = imageftbbox($tam , 0 , $fuente, $contador);
    $x = $box[2]-$box[0]+4;
    $y = $box[3]-$box[7]+4;
    $im = imagecreatetruecolor($x, $y);
    $azul = ImageColorAllocate($im, 128, 128, 255);
    $verde = ImageColorAllocate($im, 0, 64, 0);
    $verde2 = ImageColorAllocate($im, $r, $g, $b);

    imageline($im, 0,0,$x-1,0,$azul);
    imageline($im, 0,0,0,$y-1,$azul);
    imageline($im, 0,$y-1,$x-1,$y-1,$azul);
    imageline($im, $x-1,$y-1,$x-1,0,$azul);
    imageline($im, 1,1,$x-2,1,$verde);
    imageline($im, 1,1,1,$y-2,$verde);
    imageline($im, 1,$y-2,$x-2,$y-2,$verde);
    imageline($im, $x-2,$y-2,$x-2,1,$verde);

    $box = imageftbbox($tam , 0 , $fuente, "1");
    imagettftext($im, $tam, 0, -1, -$box[5]+1, $verde2, $fuente, $contador);

    ImagePng($im);
    ImageDestroy($im);
?>

Contador PNGEl resultado es mejorable, por supuesto. Puedes comprobar que para diferentes tamaños los dígitos no se ajustan bien al marco. Esto es debido a pequeñas diferencias en las anchuras de los caracteres, y al modo en que se trazan los textos, ya que las coordenadas se refieren a la esquina inferior izquierda del texto, en lugar de a la superior izquierda.

Para este ejemplo se ha usado una fuente truetype llamada ‘ariblk.ttf’, que se almacena en una carpeta local llamada fuentes. Puedes usar una fuente de sistema o usar la fuente que prefieras copiándola a la carpeta adecuada.

Modificar imágenes

Otro uso habitual consiste en modificar imágenes para añadir efecto o insertar marcas de agua. Una marca de agua es una segunda imagen que se superpone con la que queremos modificar, pero intentando que no dificulte demasiado su visión. Son difíciles de eliminar, y se suelen usar para evitar que las imágenes sean usadas por terceras personas. Generalmente se añade un logotipo o un texto que identifica al propietario de la imagen.

<img src="marcaagua.php?img=imagen.jpg" width="640" height="480" alt="imagen">

En este ejemplo, redimensionaremos la imagen a 640×480 pixels y añadiremos un texto superpuesto.

<?php
    // Marcas de agua
    // Mayo de 2013, Con Clase
    // Salvador Pozo
    header('Content-type: image/jpg');
    $nombre_archivo = $_GET["img"];
    $im = ImageCreateTruecolor(640, 480);
    $im1 = ImageCreateFromJpeg($nombre_archivo);
    list($ancho, $alto) = GetImageSize($nombre_archivo);
    ImageCopyResized  ( $im, $im1, 0, 0, 0, 0, 640, 480, $ancho, $alto ); 
    ImageDestroy($im1);

    $color = ImageColorAllocateAlpha($im, 255, 255, 255, 110);
    $fuente = './ariblk.ttf';
    ImageTtfText($im, 80, 30, 50, 400, $color, $fuente, "Con Clase");
    ImageJpeg($im);
    ImageDestroy($im);
?>

Marca de aguaMarca de agua + B/NEl resultado es un tamaño de imagen predecible, con una marca de agua. Sin embargo, debemos tener cuidado con las proporciones de la imagen original, ya que al redimensionarla puede quedar distorsionada.

Se pueden modificar imágenes usando la función ImageFilter. En este caso deberemos tener la precaución de verificar si la función existe en nuestra versión de PHP, ya que no siempre es así:

if(function_exists('ImageFilter'))
{
    ImageFilter($im, IMG_FILTER_GRAYSCALE);
}

Este código, por ejemplo, convierte la imagen $im a escala de grises.  Puedes consultar la documentación para ver los filtros disponibles.

Imágenes en hojas de estilo

Es posible crear hojas de estilo en las que usaremos imágenes de fondo para algunos elementos, como títulos, tablas, etc.

Esas imágenes pueden ser generadas mediante código PHP, y la hoja de estilo completa, también.

Empecemos por una página en la que se cargue una hoja de estilo generada mediante PHP:

<!DOCTYPE HTML>
<html>
<head>
   <link rel="StyleSheet" href="ejemplo.css.php?c1=002080&amp;c2=001040&amp;c3=0040c0&amp;c4=00ff00" 
       title="Estilo dinamico" media="Screen" type="text/css"/>
</head>
<body>
<h1>Titulo</h1>
<p>Texto...</p>
</body>
</html>

Sólo definiremos un estilo para la etiqueta H1, por eso el código de la página sólo muestra un título, y un pequeño texto.

Lo importante aquí es que la hoja de estilo se carga desde un script PHP llamado «ejemplo.css.php». Le pasamos cuatro parámetros, con los colores que deseamos.

El código de la hoja de estilos es:

<?php
    // Hoja de estilo dinámica
    // Mayo de 2013, Con Clase
    // Salvador Pozo

    header("Content-type: text/css");
    $Col1 = $_GET["c1"];
    $Col2 = $_GET["c2"];
    $Col3 = $_GET["c3"];
    $Col4 = $_GET["c4"];

    print("h1 { background-image: ");
    print("url(h1.php?c1=".$Col1."&c2=".$Col2."&c3=".$Col3."&c4=".$Col4."); ");
    print("background-repeat: repeat-x; background-position: 0 0; ");
    print("height:28px; font-size:22px; font-weight:normal; padding-left: 50px; overflow:hidden; color: #".$Col4."; }\n");

?>

Ahora personalizamos la etiqueta «h1», usando una imagen de fondo generada mediante un script PHP, tal como hicimos con otras imágenes anteriores.

Con los parámetros indicados, la hoja de estilos tiene el siguiente contenido:

h1 { background-image: url(h1.php?c1=002080&c2=001040&c3=0040c0&c4=00ff00); 
background-repeat: repeat-x; 
background-position: 0 0; 
height:28px; 
font-size:22px; 
font-weight:normal; 
padding-left: 50px; 
overflow:hidden; 
color: #00ff00; 
}

Hay que prestar especial atención a la primera línea:  header(«Content-type: text/css»). Esta es la que define el tipo de salida que genera PHP, en este caso, una hoja de estilo.

Para finalizar, el código PHP para h1.php es el siguiente:

<?php
    // Imagen de fondo para H1 dinámica
    // Mayo de 2013, Con Clase
    // Salvador Pozo

    function CrearColor($im, $color) {
         $r = HexDec(substr($color, 0, 2));
         $g = HexDec(substr($color, 2, 2));
         $b = HexDec(substr($color, 4, 2));
       return ImageColorAllocate($im, $r, $g, $b);
    }

    Header("Content-type: image/gif");

    $im = imagecreatetruecolor(1, 25);

    $col = array();
    $col[0] = CrearColor($im, $_GET["c1"]);
    $col[1] = CrearColor($im, $_GET["c2"]);
    $col[2] = CrearColor($im, $_GET["c3"]);
    $col[3] = CrearColor($im, $_GET["c4"]);

    imageline($im, 0, 0, 9, 0, $col[3]);
    for($i = 0; $i < 6; $i++) {
         imageline($im, 0, 1+$i*4, 9, 1+$i*4, $col[1]);
         imageline($im, 0, 2+$i*4, 9, 2+$i*4, $col[0]);
         imageline($im, 0, 3+$i*4, 9, 3+$i*4, $col[0]);
         imageline($im, 0, 4+$i*4, 9, 4+$i*4, $col[2]);
    }
    imageline($im, 0, 24, 9, 24, $col[3]);

    ImageGif($im);
    ImageDestroy($im);
?>

Salida en navegadorLa gran ventaja: que se pueden generar todos los gráficos asociados a una página con diferentes combinaciones de colores, sin tener que crear ficheros de imágenes para cada caso.

Los colores que se usan en el fichero HTLM puede ser extraídos de una base de datos, o de un fichero de configuración. Las imágenes se generan de forma dinámica a partir de los scripts PHP y usando esos colores.

Otros usos

La generación dinámica de imágenes tiene otras muchas aplicaciones, por ejemplo, crear captchas. Es relativamente sencillo crear imágenes a partir de un texto, usando diferentes fuentes y colores, y distorsionando la salida, añadir fondos, ruido, etc.

Avatar generadoOtra aplicación es la creación de imágenes para usar como avatares. Probablemente has visto este tipo de avatares en foros y blogs. Se generan a partir de un código HASH, creado a su vez a partir de una dirección de correo o una IP.

También se puede usar la generación dinámica para crear códigos QR conclase.netQR, como el de la izquierda, aunque esto resulta algo más complicado, y no por el diseño del gráfico, sino por los algoritmos que se usan para generarlos.