Desarrollo de Aplicaciones Web

Práctica 10: PHP 4 (acceso a una base de datos)

1. Objetivos

2. Recursos

¿Qué funciones se emplean en PHP para acceder a MySQL?

¿Existen funciones en PHP para filtrar los datos recibidos?

¿Cómo se emplean las expresiones regulares con PHP?

3. ¿Qué tengo que hacer?

En está práctica debes programar el registro de un nuevo usuario, es decir, la página de respuesta a la página “registro nuevo usuario”. El registro se realiza mediante la inserción de los datos del nuevo usuario en la tabla que corresponda. En el lado del servidor, tienes que usar PHP para validar los datos introducidos por el usuario12; en concreto, el formulario para registrarse como un nuevo usuario contiene los siguientes campos y restricciones (son las mismas que se definieron en una práctica anterior mediante JavaScript, pero ahora se deben programar en el lado del servidor):

Respuesta “Pagína registro nuevo usuario”
La página de registro como nuevo usuario contiene un formulario con los datos necesarios para registrarse (nombre de usuario, contraseña, repetir contraseña, dirección de email, sexo, fecha de nacimiento, ciudad y país de residencia, foto). Una vez enviado el formulario, debes realizar las siguientes comprobaciones en el servidor:

Debes aplicar una combinación de los diferentes métodos de validación que existen en PHP: expresiones regulares, funciones de filtrado y validación programada por ti mismo.

Además, tienes que añadir las siguientes páginas a la parte privada de un usuario registrado:

Respuesta Página “Configurar”
Permite al usuario seleccionar el estilo alternativo que desea. La selección se debe conservar entre diferentes visitas al sitio web, por lo que se debe almacenar en la base de datos. La página de respuesta ya se debe mostrar con el estilo seleccionado y simplemente muestra un mensaje de confirmación del estilo seleccionado.
Respuesta Página “Mis datos”
Tanto en esta página como en [Respuesta “Pagína registro nuevo usuario”] se tienen que realizar los filtrados definidos. Por tanto, lo más conveniente es aislar el código de filtrado en un fichero a parte e incluirlo donde sea necesario. Para confirmar la modificación de los datos el usuario debe introducir su contraseña actual.
Página “Darme de baja”
Elimina al usuario de la base datos (el usuario y todos los datos asociados con el usuario). Antes de realizar el borrado, la operación debe solicitar la confirmación del usuario para evitar que se produzca la operación de forma accidental. Para ello, muestra una página de confirmación con un resumen de la información que almacena la aplicación web: un listado de los álbumes con el número de fotos que contiene cada álbum y el número total de fotos que tiene el usuario. Para confirmar la solicitud de baja el usuario debe introducir su contraseña actual. El borrado de las fotos en el sistema de archivos se realizará en una próxima práctica, por ahora el borrado solo se tiene que realizar en la base de datos.
Respuesta Página “Crear álbum”
Realiza la inserción de un álbum nuevo en la base de datos. Una vez realizada la inserción, se debe invitar al usuario a introducir su primera fotografía en el álbum que se acaba de crear.
Respuesta Página “Añadir foto a álbum”
Realiza la inserción de una foto nueva en la base de datos. Por ahora, no se tiene que gestionar el almacenamiento de la foto subida, eso se hará en una práctica posterior.

Y tienes que modificar la siguiente página:

Respuesta “Solicitar álbum”
Realiza la inserción de una solicitud de un álbum impreso.
Página “Ver álbum” (privada)
Contiene un enlace a la página “Añadir foto a álbum” para añadir una foto al álbum que se está visualizando.
Página “Añadir foto a álbum”
Si se accede desde el menú de la parte privada, muestra una lista desplegable para que el usuario seleccione el álbum en el que desea añadir la foto (por defecto aparece sin un álbum seleccionado); si se accede desde la página “Ver álbum” de la parte privada, el álbum ya aparece seleccionado en la lista desplegable, aunque el usuario lo puede cambiar si quiere.


PIC


Figura 1: Diagrama de páginas que componen el sitio web


En concreto, tienes que modificar o crear las páginas que se indican con un color de relleno claro u oscuro en la Figura 1.

Importante: la práctica no puede fallar porque el usuario introduzca datos erróneos, se tienen que realizar todas las validaciones de los datos de entrada para asegurar el correcto funcionamiento de la práctica, aunque no estén indicadas explícitamente.

4. ¿Cómo lo hago?

4.1. Funciones de filtrado

Los datos que se reciben a través de Internet siempre se tienen que validar y filtrar en el servidor, ya que pueden provenir de una fuente no segura. Aunque los datos provengan de un formulario donde se han verificado previamente con JavaScript, no hay que confiar en ello: la ejecución de JavaScript se puede desactivar en el navegador, el código JavaScript puede fallar, el navegador web puede ser que no ejecute el código JavaScript porque no sea compatible o puede que un usuario malintencionado con conocimientos envíe directamente datos incorrectos al servidor.

Por ejemplo, el complemento Tamper Data13 para Mozilla Firefox permite visualizar y modificar los encabezados HTTP/HTTPS y los datos que se envían mediante post. Y con las herramientas para el desarrollador que poseen la mayoría de los navegadores web, se pueden cambiar o desactivar todas las comprobaciones que se realicen desde HTML o JavaScript.

PHP proporciona un conjunto de funciones básicas para validar y sanear los datos de entrada que pueden ayudar a filtrar los datos de entrada. Para validaciones más complejas será necesario desarrollar un conjunto propio de funciones de filtrado para los distintos casos que se pueden dar.

Las funciones de filtrado de datos en PHP son:

Estas funciones se pueden emplear para realizar dos tipos de filtrados:

Para indicar el tipo de filtrado a realizar (validación o saneamiento), se emplean unas constantes que se utilizan al llamar a las funciones de filtrado. Las principales constantes de filtros de validación son:

Las principales constantes de filtros de saneamiento son:

Además, existen una serie de opciones y flags para especificar información adicional en el proceso de filtrado, como las opciones min_range y max_range para definir intervalos numéricos o el flag FILTER_FLAG_ALLOW_THOUSAND para permitir el empleo de la coma como separador de miles en un número real.

Por ejemplo, en el siguiente código se realizan dos validaciones, una de número entero y otra de dirección IP, además se sanea un número real y un correo electrónico eliminando los caracteres que no son válidos:

<!DOCTYPE html> 
<html lang="es"> 
<head> 
<meta charset="utf-8" /> 
<title>Prueba de funciones de filtrado y saneamiento</title> 
</head> 
<body> 
<p> 
<?php 
 // Filtra un número entero en un intervalo 
 $int_options = array("options" => array("min_range" => 0, "max_range" => 256)); 
 $var = 123; 
 if(filter_var($var, FILTER_VALIDATE_INT, $int_options)) 
   echo "$var: Correcto<br />"; 
 else 
   echo "$var: Incorrecto<br />"; 
 $var = 333; 
 if(filter_var($var, FILTER_VALIDATE_INT, $int_options)) 
   echo "$var: Correcto<br />"; 
 else 
   echo "$var: Incorrecto<br />"; 
 
 // Filtra dirección IP 
 $var = "125.125.125.125"; 
 if(filter_var($var, FILTER_VALIDATE_IP)) 
   echo "$var: Correcto<br />"; 
 else 
   echo "$var: Incorrecto"; 
 $var = "265.125.125.125"; 
 if(filter_var($var, FILTER_VALIDATE_IP)) 
   echo "$var: Correcto<br />"; 
 else 
   echo "$var: Incorrecto<br />"; 
 
 // Sanea un número real 
 $var = "1,345kg"; 
 echo "$var: " . filter_var($var, FILTER_SANITIZE_NUMBER_FLOAT) . "<br />"; 
 echo "$var: " . filter_var($var, FILTER_SANITIZE_NUMBER_FLOAT, 
                     FILTER_FLAG_ALLOW_THOUSAND) . "<br />"; 
 
 // Sanea un correo electrónico 
 $var = "sergio\.\lujan( @ )ua\.\es"; 
 echo "$var: " . filter_var($var, FILTER_SANITIZE_EMAIL) . "<br />"; 
?> 
</p> 
</body> 
</html>

El ejemplo anterior produce la siguiente salida:

123: Correcto 
333: Incorrecto 
125.125.125.125: Correcto 
265.125.125.125: Incorrecto 
1,345kg: 1345 
1,345kg: 1,345 
sergio\.\lujan( @ )ua\.\es: sergio.lujan@ua.es

4.2. Expresiones regulares

Una expresión regular es un patrón que define un conjunto de cadenas sin enumerar todos sus elementos. Una expresión regular está formada de caracteres (letras, números y signos) y metacaracteres que tienen una función definida (representar otros caracteres o indicar la forma de combinar los caracteres). Los metacaracteres reciben este nombre porque no se representan a ellos mismos, sino que son interpretados de una manera especial.

Los principales metacaracteres que se emplean en las expresiones regulares son:

En PHP existen dos conjuntos distintos de funciones que permiten utilizar las expresiones regulares, las funciones POSIX y las funciones PCRE. En algunos aspectos ambas funciones comparten la misma sintaxis, aunque por otro lado tienen sus particularidades propias.

Las funciones POSIX son ereg(), eregi(), ereg_replace(), eregi_replace(), split() y spliti().

Las funciones PCRE (Perl Compatible Regular Expressions) implementan las expresiones regulares con la misma sintaxis y semántica que Perl 5. Estas funciones son más potentes (y por tanto, más complejas) y más rápidas que las funciones POSIX. Las funciones PCRE en PHP son: preg_match(), preg_match_all(), preg_replace(), preg_split() y preg_grep(). En las funciones PCRE la expresión regular debe empezar y acabar con una barra inclinada “/”.

El siguiente ejemplo genera la página web que se muestra en la Figura 2; este ejemplo permite introducir una expresión regular, un texto y la función que se desea emplear (POSIX o Perl) y utiliza la función seleccionada para verificar si el texto introducido cumple la expresión regular:

<!DOCTYPE html> 
<html lang="es"> 
<head> 
<meta charset="utf-8" /> 
<title>Prueba de expresiones regulares</title> 
</head> 
<body> 
<form action="expreg.php" method="post"> 
<p> 
Expresión regular: <input type="text" name="expreg" size="20" /><br /> 
Ejemplo: ^[0-9]{1,4}$ o ^[^@ ]+@[^@ ]+\.[^@ .]+$<br /> 
Texto: <input type="text" name="texto" size="20" /><br /> 
Función: 
<input type="radio" name="func" value="perl" /> Perl 
<input type="radio" name="func" value="posix" /> Posix<br /> 
<input type="submit" value="Enviar" /> 
<input type="reset" value="Borrar" /> 
</p> 
</form> 
<p> 
<?php 
 if(isset($_POST['expreg'])) 
 { 
   $expreg = stripslashes($_POST['expreg']); 
   $texto = stripslashes($_POST['texto']); 
   echo 'Expresión regular: ' . $expreg . '<br />'; 
   echo 'Texto: ' . $texto . '<br />'; 
   if($_POST['func'] == 'perl') 
   { 
    if(preg_match('/' . $expreg . '/', $texto) == 0) 
      echo 'No'; 
    else 
      echo 'Ok'; 
   } 
   else if($_POST['func'] == 'posix') 
   { 
    if(ereg($expreg, $texto) == false) 
      echo 'No'; 
    else 
      echo 'Ok'; 
   } 
   else 
    echo 'Debe seleccionar una función correcta'; 
 } 
?> 
</p> 
</body> 
</html>


PIC


Figura 2: Ejemplo de uso de expresiones regulares en PHP


4.3. Inserción, modificación y borrado en una base de datos

MUY IMPORTANTE: no se debe usar la antigua extensión mysql para nuevos desarrollos, ya que se considera obsoleta y no se desarrollan nuevas funcionalidades para ella, sólo se mantiene para corregir los fallos que aparecen.

La inserción (INSERT), la modificación (UPDATE) y el borrado (DELETE) de datos mediante SQL en una base de datos creada en MySQL se realiza de la misma forma que se ejecuta una consulta (SELECT), excepto que mientras una consulta devuelve un resultado, la inserción, la modificación y el borrado no devuelven un resultado. Si se quiere saber cuántas filas (tuplas o registros) se han visto afectadas por una inserción, una modificación o un borrado se puede emplear la función mysqli_affected_rows().

Cuando se construya la sentencia SQL de inserción o modificación hay que recordar que los valores de tipo cadena y de tipo fecha deben estar encerrados entre comillas y que el orden del tipo fecha puede ser año-mes-día (depende del SGBD y de su configuración).

A continuación se muestra un ejemplo compuesto por dos páginas. La primera página muestra un formulario web que permite al usuario insertar datos en una tabla de una base de datos; la segunda página, programada en PHP, realiza la inserción de los datos recibidos desde el formulario en la base de datos.

El código de la página con el formulario es:

<!DOCTYPE html> 
<html lang="es"> 
<head> 
<meta charset="utf-8" /> 
<title>Prueba de INSERT y MySQL</title> 
</head> 
<body> 
<h1>Insertar un libro nuevo</h1> 
<form action="insert.php" method="post"> 
<p> 
Título: <input type="text" name="titulo" /><br /> 
Resumen: <textarea name="resumen" rows="5" cols="80"></textarea><br /> 
Autor: <select name="autor"> 
<option value="3">Bob Clancy</option> 
<option value="2">Fran London</option> 
<option value="1">Luis Verne</option> 
</select><br /> 
Categoría: <select name="categoria"> 
<option value="3">Aventuras</option> 
<option value="1">Ciencia ficción</option> 
<option value="2">Terror</option> 
</select><br /> 
Editorial: <select name="editorial"> 
<option value="1">Escribe y publica</option> 
<option value="2">Libros a GoGo</option> 
<option value="3">Todo libros</option> 
</select><br /> 
Año: <input type="text" name="anyo" size="4" maxlength="4" /><br /> 
<input type="submit" value="Enviar" /> 
<input type="reset" value="Borrar" /> 
</p> 
</form> 
</body> 
</html>

El código de la página PHP (insert.php) que realiza la inserción en la base de datos es:

<!DOCTYPE html> 
<html lang="es"> 
<head> 
<meta charset="utf-8" /> 
<title>Prueba de INSERT y MySQL</title> 
</head> 
<body> 
<p> 
<?php 
 // Recoge los datos del formulario 
 $titulo = $_POST['titulo'];    // text 
 $resumen = $_POST['resumen'];  // text 
 $autor = $_POST['autor'];     // int 
 $categoria = $_POST['categoria']; // int 
 $editorial = $_POST['editorial']; // int 
 $anyo = $_POST['anyo'];       // int 
 
 // Se conecta al SGBD 
 if(!($iden = mysqli_connect("localhost", "wwwdata", ""))) 
   die("Error: No se pudo conectar"); 
 
 // Selecciona la base de datos 
 if(!mysqli_select_db($iden, "biblioteca")) 
   die("Error: No existe la base de datos"); 
 
 // Sentencia SQL: inserta un nuevo libro 
 $sentencia = "INSERT INTO libros VALUES (null, '$titulo', '$resumen', $autor, "; 
 $sentencia .= "$categoria, $editorial, $anyo)"; 
 // Ejecuta la sentencia SQL 
 if(!mysqli_query($iden, $sentencia)) 
   die("Error: no se pudo realizar la inserción"); 
 
 echo 'Se ha insertado un nuevo libro en la base de datos. '; 
 echo '¿Filas insertadas? ' . mysqli_affected_rows($sentencia); 
 
 // Cierra la conexión con la base de datos 
 mysqli_close($iden); 
?> 
</p> 
</body> 
</html>

Nota: el ejemplo anterior no está mal, pero no es la mejor forma de combinar en una página web el código HTML y el código PHP de acceso a una base de datos. Lo correcto es emplear el patrón de arquitectura de software modelo-vista-controlador14 (MVC), pero su estudio y su uso no corresponden a esta asignatura.

Después de cada operación con la base de datos hay que comprobar si se ha producido un error. Para ello, se puede comprobar el valor devuelto por las funciones de acceso a MySQL, ya que devuelven false si la operación ha fallado. Ante una situación de error, al usuario se le puede mostrar un mensaje de error propio o se puede emplear la función mysqli_error() que devuelve una descripción del último error producido.

El ejemplo anterior no está mal, pero presenta un agujero de seguridad, ya que no comprueba que en los datos introducidos pueda haber valores incorrectos que produzcan que la sentencia SQL sea errónea. Para evitarlo, se tendrían que escapar los caracteres de entrada, lo cual puede ser tedioso.

Una forma más cómoda y mucho más segura es emplear las sentencias preparadas. Una sentencia preparada (prepared statement), también llamada a veces sentencia parametrizada, se usa para ejecutar la misma sentencia repetidamente con gran eficiencia. La ejecución de una sentencia preparada consta de dos partes:

mysqli emplea marcadores de posición anónimos (?) para la sustitución de los valores. Otras tecnologías como PDO ofrecen los marcadores de posición con nombre, pero por ahora en mysqli no existen.

Los marcadores de posición se sustituyen por su valor real mediante el método mysqli_bind_param(). El segundo parámetro de esta función es una cadena que contiene uno o más caracteres que especifican los tipos para el correspondiente enlazado de las variables:

Por supuesto, el número de variables y la longitud de la cadena de tipos debe coincidir con los parámetros en la sentencia.

El siguiente ejemplo muestra cómo se utilizan las sentencias preparadas con marcadores anónimos:

<!DOCTYPE html> 
<html lang="es"> 
<head> 
<meta charset="utf-8" /> 
<title>Prueba de INSERT y MySQL</title> 
</head> 
<body> 
<p> 
<?php 
// Recoge los datos del formulario 
$titulo = $_POST['titulo'];    // text 
$resumen = $_POST['resumen'];  // text 
$autor = $_POST['autor'];     // int 
$categoria = $_POST['categoria']; // int 
$editorial = $_POST['editorial']; // int 
$anyo = $_POST['anyo'];       // int 
 
// Se conecta al SGBD 
if(!($iden = mysqli_connect("localhost", "wwwdata", ""))) 
die("Error: No se pudo conectar"); 
 
// Selecciona la base de datos 
if(!mysqli_select_db($iden, "biblioteca")) 
die("Error: No existe la base de datos"); 
 
// Sentencia SQL: inserta un nuevo libro 
$sentencia = mysqli_prepare($iden, "INSERT INTO libros VALUES (null, ?, ?, ?, ?, ?, ?)"); 
 
// Asocia los parámetros 
mysqli_stmt_bind_param($sentencia, 'ssiiii', $titulo, $resumen, $autor, $categoria, $editorial, $anyo); 
 
// Ejecuta la sentencia SQL 
if(!mysqli_stmt_execute($sentencia)) 
die("Error: no se pudo realizar la inserción"); 
 
echo 'Se ha insertado un nuevo libro en la base de datos. '; 
echo '¿Filas insertadas? ' . mysqli_stmt_affected_rows($sentencia); 
 
// Cierra la sentencia preparada 
mysqli_stmt_close($sentencia); 
 
// Cierra la conexión con la base de datos 
mysqli_close($iden); 
?> 
</p> 
</body> 
</html>

El uso de las sentencias preparadas ofrece dos ventajas muy importantes:

  1. Ofrece un aumento de velocidad en la ejecución de una sentencia cuando se ejecuta varias veces: la sentencia no se tiene que analizar e interpretar cada vez, sino que está lista para ejecutarse tantas veces como se quiera.
  2. Ofrece protección frente a la inyección SQL.

Nota: en los ejemplos anteriores se ha empleado la interfaz procedural, pero también es posible utilizar la interfaz orientada a objetos.

5. Recomendaciones

En una aplicación web se deben aplicar muchas medidas de seguridad porque los usuarios suelen ser “despistados” y se pueden dejar su sesión abierta en ordenadores ajenos. Por ello, en aquellas operaciones donde un mal uso pueda implicar que el usuario legítimo pierda su derecho de acceso (eliminar la cuenta, cambiar la contraseña, cambiar el correo electrónico de recuperación y otros datos personales) se debe incluir algún sistema adicional de seguridad. Por ejemplo, para cambiar la contraseña se suele pedir la contraseña antigua al usuario para impedir que sea modificada por otra persona.

Recuerda que las páginas que contengan código PHP tienen que tener la extensión .php. Si modificas alguna página web que ya tengas hecha de prácticas anteriores para añadirle código PHP, tendrás que cambiarle la extensión y corregir todos los enlaces que apunten a esa página.

Recuerda que para que el código PHP se ejecute, la página web tiene que pasar por el servidor web para que éste se la pase al intérprete de PHP. Para ello, tienes que cargar la página a través del servidor web: la URL de conexión tiene que tener la forma http://localhost. Si en la barra de direcciones del navegador ves escrito algo del estilo file:///D:/..., el código PHP no se ejecutará.

El manual de PHP te lo puedes descargar en diferentes formatos de su sitio web15 para tenerlo siempre a mano y poder hacer las búsquedas de información rápidamente. También puedes acceder a través de Internet a la ayuda de cualquier función de PHP escribiendo el nombre de la función a continuación de la URL http://php.net/. Por ejemplo, http://php.net/header muestra la ayuda de la función header().

El manual de MySQL te lo puedes descargar en diferentes formatos de su sitio web16 para tenerlo siempre a mano y poder hacer las búsquedas de información rápidamente.

No filtrar correctamente los datos de entrada es uno de los principales problemas de seguridad de las aplicaciones. Todos los datos que provengan de una fuente externa tienen que ser validados. En una aplicación web los datos pueden provenir principalmente de:

Las expresiones regulares son un mecanismo de programación muy potente, pero esa potencia origina que sean complejas de utilizar. Comienza a trabajar con expresiones regulares sencillas y poco a poco intenta escribir expresiones más complejas.

Es recomendable solicitar siempre una confirmación del usuario en aquellas operaciones que puedan modificar o eliminar los datos ya existentes en la base de datos. De este modo se evitará la pérdida accidental de información o que otra persona modifique o elimine la cuenta de otra persona porque se lo ha dejado abierto en un ordenador.

Recuerda que algunas funciones de PHP muestran directamente mensajes de advertencia o de error cuando se produce alguna situación errónea. Para evitar que suceda esto puedes emplear el operador de control de errores “@”.

1http://www.php.net/manual/es/book.mysql.php

2http://www.php.net/manual/es/book.mysqli.php

3http://www.w3schools.com/php/php_mysql_intro.asp

4http://devzone.zend.com/239/ext-mysqli-part-i_overview-and-prepared-statements/

5http://devzone.zend.com/255/using-ext-mysqli-part-ii_extending-mysqli/

6http://es.php.net/manual/es/book.filter.php

7http://www.w3schools.com/PHP/php_filter.asp

8http://www.w3schools.com/PHP/php_ref_filter.asp

9http://es.php.net/manual/es/book.pcre.php

10http://www.pcre.org/

11http://es.php.net/manual/es/book.regex.php

12La validación de los datos de entrada también se puede realizar en el cliente mediante JavaScript para mejorar la usabilidad de una aplicación web, pero siempre se debe realizar en el servidor como medida de seguridad.

13https://addons.mozilla.org/es-ES/firefox/addon/966

14https://es.wikipedia.org/wiki/Modelo%E2%80%93vista%E2%80%93controlador

15http://www.php.net/download-docs.php

16http://dev.mysql.com/doc/