domingo, 30 de junio de 2013

Extensión MySQLi - Parte 3: Consultas preparadas y escapado

Las consultas que se realizan a la base de datos, en un sistema real, contendrán datos aportados por el usuario. Estos datos habrán sido enviados al servidor ya se mediante peticiones POST o peticiones GET.
La recuperación de estos datos es muy sencilla. Para ello solo tendremos que acceder al array superglobal asociativo correspondiente. Buscaremos en el array $_POST o $_GET dependiendo de como el servidor espere la recepción de información.

Para la comprobación de la existencia de una variable o elemento de array existe una función muy útil llamada isset()

 if (isset($_POST['userName'],$_POST['password'])) {  
   $user = $_POST['userName'];  
   $pass = $_POST['password'];  
   ...  
 }  

Ahora podríamos utilizar estos datos para crear una consulta:

  if (isset($_POST['userName'],$_POST['password'])) {   
   $user = $_POST['userName'];   
   $pass = sha1($_POST['password']);   
   //podriamos comprobar si los campos estan vacios para mostrar error al usuario  
   ...  
   $resultado = $mysqli->query("SELECT idUsuario FROM usuario WHERE nomUsuario='$user' AND  password='$pass'";  
   ...  
  }   

Una regla importante en el desarrollo de aplicaciones web es que nunca hay que fiarse de lo que va a introducir el usuario y siempre hay que adelantarse a posibles datos de carácter malicioso.

Como veremos en otro tutorial, el procesado que podemos hacer a los datos de entrada es muy variado. Y dependerá de lo que queremos almacenar, aplicaremos un método u otro de saneamiento.
Ahora vamos a ver como podemos escapar caracteres problemáticos ya que de esta manera almacenaremos cualquier tipo de dato sin tener que eliminar. Ya que es posible que en ocasiones queramos guardar caracteres que son considerados problemáticos como: ', ", /... Además es el método más habitual y sencillo de usar contra inyecciones de sql.

A) Interfaz orientada a objetos

 $mysqli = new mysqli("localhost", "mi_usuario", "mi_contraseña", "demo");  
 if($mysqli->conect_errno) die($mysqli->conect_error);   
 //podemos utililzar el alias mysqli_escape_string 
 $nombre = $mysqli->real_escape_string($_POST['nombre']);  
 $res = $mysqli->query("SELECT * FROM usuario WHERE nombre ='$nombre'");   
 if($res->num_rows == 0) { 
 }  

string real_escape_string(string $cadena) devolverá la misma cadena de entrada pero con los caracteres peligrosos escapados. De esta forma no hay peligro en que se se realizen consultas con estos datos.

B) Interfaz procedimental

  $con = mysqli_connect("localhost", "mi_usuario", "mi_contraseña", "demo");   
  if(mysqli_connect_errno()) die(mysqli_connect_error());    
  //podriamos utilizar el alias mysqli_escape_string   
  $nombre = mysqli_real_escape_string($con,$_POST['nombre']);   
  $res = mysqli_query($con,"SELECT * FROM usuario WHERE nombre ='$nombre'");   
  if (mysqli_num_rows($res) == 0) {  
   //no encontrado  
  }   


Con MySQLi tenemos una alternativa para la realización de consultas. Podemos usar las consultas preparadas. Una alternativa que nos proporcionará ventajas:
- Nos evita tener que parsear los datos externos de las consultas ya que lo hace automáticamente.  No es que parsear sea costoso o sintacticamente incómodo, pero se nos puede olvidar. Y así nos podemos despreocupar de la injección SQL.
- Debido a su forma de utilización, que veremos a continuación, nos optimiza la posible ejecución de múltiples consultas iguales. El mayor beneficio se obtendría por ejemplo en un bucle de consultas, ya que como veremos, una vez preparada la consulta, solo enviaríamos parámetros al servidor MySQL y no las consultas enteras. Por lo el servidor no tiene que perder tanto tiempo procesado consultas enteras.
Evidentemente este procesado en dos etapas puede ser mínimamente más costoso que el uso de las interfaces de MySQLi mostradas, si no vamos a utilizar numerosos consultas iguales. Pero se compensan con los beneficios en seguridad.

Consultas preparadas

1. Preparación

Hasta ahora hemos creado las las consultas la incluyendo todos los datos que necesitan ser usados, procesando los externos. En una sentencia preparada creamos esta consulta marcando los espacios donde irán los datos que necesitan ser procesados por seguridad. Se marcará por medio de signos de interrogación. Por lo tanto la consulta se enviará al servidor MySQL sin datos.

A) Interfaz orientada a objetos

Vamos crear un objeto mysqli_stmt que dispondrá de los métodos que necesitaremos para preparar la consulta. Y llamaremos a su método prepare().

Sintaxis:

 mysqli_stmt mysqli::stmt_init ( void )  

 bool mysqli_stmt::prepare(string $consulta);  

prepare() nos devolverá false o true en caso de que haya o no un error en la consulta.

 $mysqli = new mysqli('localhost','root','','demo');   
 if($mysqli->connect_errno) die ($mysqli->connect_error);   
 $stmt = $mysqli->stmt_init();  
 $sql = "insert into usuario (nombre,email) values(?,?)";   
 if($stmt->prepare($sql)){   

B) Interfaz procedimental

Como hemos explicado a lo largo de los tutoriales de MySQLi, tenemos una equivalencia para los métodos de la interfaz orientada a  objetos.

Sintaxis:

 bool mysqli_stmt_prepare(mysqli_stmt $stmt,string $consulta);  

 mysqli_stmt mysqli_stmt_init ( mysqli $link )  

prepare() nos devolverá false o true en caso de que haya o no un error en la consulta.

 $con = mysqli_connect('localhost','root','','demo');   
 if(mysqli_connect_errno()) die(mysqli_connect_error());   
 $sql = "insert into usuario (nombre,email) values(?,?)";   
 $stmt = mysqli_stmt_init($con);  
 if(mysqli_stmt_prepare($stmt,$sql)){   

2. Vincular parámetros y ejecución de la consulta

A) Interfaz orientada a objetos

Primero vamos a vincular los parámetros que en la consulta hemos representado con signos de interrogación. Estos parámetros serán escapados automáticamente.

Sintaxis:

 bool mysqli_stmt::bind_param ( string $types , mixed &$var1 [, mixed &$... ] )  

Muy importante son los tipos de las variables que vamos a vincular. Por cada variable a vincular ($nombre y $correo en nuestro ejemplo) especificaremos su tipo mediante un carácter dentro de la cadena que corresponde al segundo parámetro de la función.
Los tipos pueden ser cuatro:
  •  "i" si la variable a la que vincula es de tipo entero. 
  • "d" si la variable a la que vincula es de tipo double.
  • "s" si la variable a la que vincula es tipo cadena. 
  • "b" si la variable a la que vincula es tipo blob.
Podríamos haber asignado el valor de las variables vinculadas antes de dicha vinculación. Pero también es posible hacerlo posteriormente como se ve en el ejemplo:
    $stmt->bind_param("ss",$nombre, $correo);  
    $nombre = "Roberto";  
    $correo = "prueba@ejemplo.com";  

Una vez ya hemos vinculado los parámetros de la consulta preparada, ya podemos ejecutarla y cerrar la consulta. $stmt->execute() devolverá un boolean lo cual indicará si es false o no el posible error en la ejecución.

    $stmt->execute();   
    $nombre = 'Jose';   
    $correo = 'prueba2@ejemplo.com';   
    $stmt->execute();   
    //cerramos al consulta   
    $stmt->close();   
 }//fin if en comprobación de error en prepare()

Como se puede ver, hemos ejecutado la consulta dos veces pero sin tener que enviar de nuevo el "insert" al servidor MySQL. Simplemente asignamos nuevos valores a las variables vinculadas y ejecutamos. Esto quiere decir que no tendremos que volver a enviar la consulta completa y su correspondiente procesado. Al haber enviado la lógica con la consulta preparada, posteriormente únicamente enviaremos los datos. Por lo que cuantas más ejecuciones de consultas similares realicemos mayor será el beneficio de rendimiento.

B) Interfaz procedimental

Vinculación de los parámetros que hemos marcado con signo de interrogante en la lógica de la consulta.

Sintaxis:

 bool mysqli_stmt_bind_param ( mysqli_stmt $stmt , string $types , mixed &$var1 [, mixed &$... ] )  

  mysqli_stmt_bind_param($stmt,"ss",$nombre, $correo);   
  $nombre = "Roberto";   
  $correo = "prueba@ejemplo.com";   

Ejecución y terminación de la consulta. Como en el ejemplo orientado a objetos, realizaremos 2 ejecuciones de la misma consulta:

    mysqli_stmt_execute($stmt);   
    $nombre = 'Jose';   
    $correo = 'prueba2@ejemplo.com';   
    mysqli_stmt_execute($stmt);   
    //cerramos al consulta   
    mysqli_stmt_close($stmt);   
 }//fin if comprobación error en mysqli_stmt_prepare()  


3. Recuperación de información

Hasta ahora hemos ejecutado una consulta preparada pero es muy probable que queramos recuperar los resultados obtenidos por dicha consulta. Sobre todo si estamos ante una consulta de selección.
Para ello tendremos que vincular las variables resultado de la consulta preparada para almacenar su resultado. Recuerda que antes hemos vinculado datos externos. Ahora son los resultados lo que tenemos que vincular.
Después de esa vinculación ya los podremos obtenerlos.

A) Interfaz orientada a objetos

Veamos la sintaxis de las dos funciones necesarios para extraer los resultados de las columnas de la consulta preparada. Con la primera de ella vincularemos las columnas del resultado de la consulta con las variables que obtendrán dicho resultado. Y con la segunda función obtendremos los resultados de dichas variables. Como se ha mostrado a lo largo del tutorial, será necesario recorrer en un bucle para obtener los datos de las columnas de cada una de las posibles filas resultantes de la consulta.

 bool mysqli_stmt::bind_result ( mixed &$var1 [, mixed &$... ] )  
 bool mysqli_stmt::fetch ( void )  

 $sql = "select nombre,correo from usuarios where BETWEEN ? AND ? AND nombre LIKE 'J%'";  
 if ($stmt->prepare($sql)) {  
   $stmt->bind_param("ii", $a, $b);  
   $a = 3;  
   $b = 50;  
   $stmt->execute();  
   $nombre = "";  //inicializamos nuestras variables resultado
   $correo = "";  
   // vinculamos el resultado  
   $stmt->bind_result($nombre, $correo); 
   //recuperadmos los datos  
   while ($stmt->fetch()) {  
     echo $nombre . "<br/>";  
     echo $correo . "<br/>";  
   }  
   $stmt->close();  
 }  

B) Interfaz procedimental

Con la primera función vincularemos las variables que contendrán los resultados de cada columna y con la segunda obtenemos dichos resultados

 bool mysqli_stmt_bind_result ( mysqli_stmt $stmt , mixed &$var1 [, mixed &$... ] )  
 bool mysqli_stmt_fetch ( mysqli_stmt $stmt )  

 $sql = "select nombre,email from usuario where BETWEEN ? AND ? AND nombre LIKE 'J%'";  
 if ($stmt = mysqli_stmt_prepare($con, $sql)) {  
   mysqli_stmt_bind_param($stmt, "ii", $a, $b);  
   $a = 3;  
   $b = 50;  
   mysqli_stmt_execute($stmt);  
   $nombre = "";  
   $email = "";  
   // vinculamos el resultado  
   mysqli_stmt_bind_result($stmt, $nombre, $email);  
   //recuperadmos los datos  
   while (mysqli_stmt_fetch($stmt)) {  
     echo $nombre . "<br/>";  
     echo $email .  "<br/>";   
   }  
   mysqli_stmt_close($stmt);  
 }  

4. Recuperación de información compleja

Ahora puede surgir una duda. Hemos presentado la función para vincular variables con las columnas de la consulta. Sabiendo el número de columnas de la misma. Imaginemos que la consulta preparada es de tipo "SELECT *". ¿Que pasa si no sabemos el número exacto de columnas?¿Como vinculamos si no sabemos con cuantas variables vincular los resultados?

A) Interfaz orientada a objetos

Vamos a utilizar para ello la siguiente función:

 mysqli_result mysqli_stmt::result_metadata ( void )  

Con ella obtendremos los metadatos del conjunto de resultados de la consulta preparada. Lo que quiere decir que obtendremos un objeto que entre otras cosas tendrá el numero de columnas y de ahí podremos extraer sus nombre. Pero no los resultados.

 $meta = $stmt->result_metadata();   

En el siguiente enlace obtendrás más información sobre result_metadata() y sobre las funciones aplicables a los metadatos obtenidos.

De esta forma, podríamos obtener un array con todos los campos contenidos en el objeto obtenido con la función anterior. Con un bucle y por ejemplo con la siguiente función (una de las mencionadas en el anterior enlace), obtendremos el siguiente nombre de campo del conjunto de metadatos:

 object mysqli_result::fetch_field ( void )  

Así que vamos a obtener el array de nombres de campos.

 $campos= array(); 
 while ( $campo = $meta->fetch_field() ) {   
    $campos[] = campo->name;    
 }   

Ahora podemos pasar el array con todos los campos al método bind_result mediante la función call_user_func_array(). La cual permite llamar a función estática, pasandole un array de parámetros:

 call_user_func_array(array($stmt, 'bind_result'), makeRefArr($campos));   

Nota 1: El primer parámetro de call_user_func_array puede ser un objeto, con lo que el array de valores (segundo parámetro de la función) se lo estamos pasando al constructor de dicho objeto. O puede ser un array donde el primer elemento del mismo es un objeto y el segundo elemento es el método del mismo donde aplicaremos el array de valores (segundo parámetro de la función). Con lo que en nuestro ejemplo, pasaremos el array makeRefArr($campos) al método bin_result ($stmt->bind_result) del objeto $stmt.
Nota 2: El segundo parámetro, en este caso, tiene que ser un array de referencias y por eso hemos llamado a una función propia llamada makeRefArr(). Que crea un array de referencia a partir de los valores de otro array.

 function makeRefArr(&$arr) {  
   $refs = array();  
   foreach ($arr as $key => &$val) {  
     $refs[$key] = &$val;  
   }  
   return $refs;  
 }  


Finalmente para interactuar con los parámetros vinculados podríamos hacer algo parecido a los siguiente:

  while ($stmt->fetch()) {  
     $arr = array();  
     foreach ($campos as $key => $val) {  
       //echo $key . "=>" . $val ."<br/>";  
       $arr[$key] = $val;  
     }  
     //echo "<br/>";  
     $resultado[] = $arr;  
   }  

Al igual que anteriormente, utilizaremos la función fetch(). Esto nos permitía acceder a los datos de las variables de los resultados vinculadas. Ahora, después de la llamada a la función vinculación (call_user...), $campos se ha convertido en un array asociativo.  Donde los indices son los nombres de los campos. Recuerda que tenemos que tener fetch() dentro de un bucle para iterar con cada posible fila del resultado y que $campos cada vez sea una fila distinta del resultado.

Nota: si pensamos, en este caso fetch() hace los mismo que cuando vinculábamos un número determinado de variables. Ya que con ello asignabamos a cada una de ellas un resultado. Y ahora a cada valor (nombre de campo) del array unidimensional le asigna un valor. Lo que lo convierte en un array de pares indice (nombre de campo) y valor.

B) Interfaz procedimental

 $con = mysqli_connect('localhost', 'root', 'AquiNoSeEntra123', 'mvc');  
 $stmt = mysqli_stmt_init($con);  
 if (mysqli_stmt_prepare($stmt, "SELECT * FROM usuarios")) {  
   mysqli_stmt_execute($stmt);  
   $meta = mysqli_stmt_result_metadata($stmt);  
   $campos = array();  
   $resultado = array();  
   while ($campo = mysqli_fetch_field($meta)) {  
     $campos[] = $campo->name;  
   }  
   call_user_func_array(array($stmt, 'bind_result'), makeRefArr($campos));  
   while (mysqli_stmt_fetch($stmt)) {  
     $arr = array();  
     foreach ($campos as $key => $val) {  
       //echo $key . "=>" . $val . "<br/>";  
       $arr[$key] = $val;  
     }  
     //echo "<br/>";  
     $resultado[] = $arr;  
   }  

call_user_func_array no admite la llamada a un método no estático por lo que no vale llamar a:

   call_user_func_array(array('mysqli_stmt','bind_result'), makeRefArr($campos));  

Ya que obtendremos el siguiente error:

Warning: call_user_func_array() expects parameter 1 to be a valid callback, non-static method mysqli_stmt::bind_result() cannot be called statically in...

Otras funciones

Al igual que cuando no utilizábamos consultas preparadas, también tenemos dos propiedades/funciones que nos permiten recuperar  información de las últimas consultas realizadas:

Con la primera de ellas obtendremos el número de filas actualizadas, insertadas o borradas.

 int $mysqli_stmt->$affected_rows  //propiedad si utilizamos interfaz orientada a objetos
 int mysqli_stmt_affected_rows ( mysqli_stmt $stmt ) //función en interfaz procedimental 

Con la segunda el último id automático creado en la ultima consulta preparada de inserción.

 int $mysqli_stmt->insert_id;  
 mixed mysqli_stmt_insert_id ( mysqli_stmt $stmt )  


Entradas relacionadas

Extensión MySQLi - Parte 1: Consultas de modificación
Extensión MySQLi - Parte 2: Consultas de selección
Extensión MySQLi - Parte 4: Transacciones

No hay comentarios:

Publicar un comentario