En esta lección se comenta una estructura de recogida y comprobación de datos que se recomienda utilizar en todos los ejercicios propuestos en estos apuntes.
Si pudiéramos confiar en los datos recibidos en la matriz $_REQUEST, un programa PHP que recibe datos podría ponerse a utilizarlos directamente, pero para evitar ataques de inyección, debemos realizar una serie de pasos previos que nos permitan confiar en los datos recibidos.
Para conseguirlo, en estos apuntes se recomienda que todos los programas que reciban datos realicen los siguientes pasos:
Para poder utilizar la función recoge() en el paso siguiente, debemos incoporarla al programa copiándola y pegándola de la lección Función recoge(). En esa lección se proponen dos funciones recoge(), una más sencilla y otra más avanzada. Cualquiera de las dos permite realizar los ejercicios.
La idea es utilizar una variable para cada control de formulario que reciba el programa. El nombre de la variable puede coincidir con el nombre del control. Las variables se obtienen llamando a la función recoge(), dando como argumento el nombre del control en el formulario (que coincide con el nombre del índice en la matriz $_REQUEST).
<?php
$nombreControl1 = recoge("nombreControl1");
$nombreControl2 = recoge("nombreControl2");
...
?>
La idea es definir una variable booleana auxiliar para cada variable que recoge un dato y que indique si el dato que hemos recibido es aceptable para el programa o no. Si es aceptable, el valor de esta variable auxiliar será true. Si no es aceptable, el valor de esta variable auxiliar será false. El nombre de esas variables pueden coincidir con los nombres de las variables que recoges los datos añadiéndoles las letras Ok. El valor inicial de esas variables será false ya que en principio debemos desconfiar siempre de los datos recibidos.
En estos apuntes se añaden las letras Ok en vez de las letras OK (en mayúsculas) para respetar la notación camelcase en los nombres de las variables. Recuerde que si cambia de minúsculas o mayúsculas alguna letra del nombre de una variable, para PHP se tratará de una variable distinta.
<?php
$nombreControl1Ok = false;
$nombreControl2Ok = false;
...
?>
Para cada una de las variables que recogen los datos, escribiremos un bloque de instrucciones if ... elseif ... else ... en el que cada condición nos permitirá detectar un tipo de error en el dato recibido. Si se detecta un error, el programa escribirá un mensaje de error indicando el motivo del error. Si no se detecta ningún error, a la variable auxiliar de comprobación se le dará el valor true para indicar que ese dato en concreto es fiable.
El número de comprobaciones dependerá del tipo de control que estemos comprobando y del rango de valores que admita nuestro programa para esa variable en concreto. El orden de las comprobaciones también es importante, de los errores más generales a los más específicos.
<?php
if (condicion_1) {
print "mensaje de error 1 (indicando porque el dato no es correcto)";
} elseif (condicion_2) {
print "mensaje de error 2 (indicando porque el dato no es correcto)";
} ... {
} else {
$nombreControlOk = true;
}
?>
El último paso consiste en comprobar que todas las variables auxiliares tiene el valor true (es decir, que no se ha detectado ningún problema en los datos recibidos) y si es así, ejecutar el programa (es decir, procesar los datos recibidos y mostrar el resultado al usuario).
<?php
if ($nombreControl1Ok && $nombreControl2Ok && ...) {
... // instrucciones del programa
}
?>
El caso más simple podría ser la recogida de una caja de texto en la que queremos que el usuario escriba su nombre.
Nota: Una forma de forzar al usuario a escribir algo de contenido sería utilizar el atributo required en el formulario, pero el usuario podría escribir simplemente espacios en blanco y de todas formas debemos pensar que siempre podemos sufrir un ataque de inyección y no recibir siquiera el control.
<form action="form-recogida-input-text-2.php" method="get">
<p>Escriba su nombre: <input type="text" name="nombre"></p>
<p><input type="submit" value="Enviar"></p>
</form>
En este caso, el único aviso que vamos a dar al usuario es si deja el campo en blanco, lo que podemos hacer con la siguiente comprobación:
// Validación de datos y generación de avisos
if ($nombre == "") {
print " <p class=\"aviso\">No ha escrito su nombre.</p>\n";
print "\n";
} ...
El proceso de recogida de datos completo podría ser el siguiente:
<?php
// Función de recogida de datos
function recoge($key, $type = "")
{
if (!is_string($key) && !is_int($key) || $key == "") {
trigger_error("Function recoge(): Argument #1 (\$key) must be a non-empty string or an integer", E_USER_ERROR);
} elseif ($type !== "" && $type !== []) {
trigger_error("Function recoge(): Argument #2 (\$type) is optional, but if provided, it must be an empty array or an empty string", E_USER_ERROR);
}
$tmp = $type;
if (isset($_REQUEST[$key])) {
if (!is_array($_REQUEST[$key]) && !is_array($type)) {
$tmp = trim(htmlspecialchars($_REQUEST[$key]));
} elseif (is_array($_REQUEST[$key]) && is_array($type)) {
$tmp = $_REQUEST[$key];
array_walk_recursive($tmp, function (&$value) {
$value = trim(htmlspecialchars($value));
});
}
}
return $tmp;
}
// Variables que recogen los datos
$nombre = recoge("nombre");
// Variables auxiliares de comprobación
$nombreOk = false;
// Validación de datos y generación de avisos
if ($nombre == "") {
print " <p class=\"aviso\">No ha escrito su nombre.</p>\n";
print "\n";
} else {
$nombreOk = true;
}
// Si todo es correcto, ejecución del programa
if ($nombreOk) {
print " <p>Su nombre es <strong>$nombre</strong>.</p>\n";
print "\n";
}
?>
Otro caso podría ser la recogida de un valor numérico en la que queremos que el usuario escriba un valor que puede ser decimal dentro de un rango.
Nota: Una forma de forzar al usuario a escribir un valor decimal en un rango sería utilizar en el formulario un control de tipo number con los atributos step, min y max, pero debemos pensar que siempre podemos sufrir un ataque de inyección y no recibir el control, o recibir una cadena de texto. Para facilitar la prueba de valores incorrectos sin necesidad de editar la URL, el ejemplo siguiente utiliza un control de tipo text en vez de number.
<form action="form-recogida-input-number-1-2.php" method="get">
<p>Escriba su peso: <input type="text" name="peso"> kg</p>
<p><input type="submit" value="Enviar"></p>
</form>
En este caso, podemos dar varios avisos distintos:
// Validación de datos y generación de avisos
if ($peso == "") {
print " <p class=\"aviso\">No ha escrito su peso.</p>\n";
print "\n";
} ...
...
} elseif (!is_numeric($peso)) {
print " <p class=\"aviso\">No ha escrito su peso como número.</p>\n";
print "\n";
} ...
...
} elseif ($peso < 40 || $peso > 150) {
print " <p class=\"aviso\">¿Está seguro de que su peso es $peso kg?</p>\n";
print "\n";
} ...
El orden en que hacemos las comprobaciones es importante:
El proceso de recogida de datos completo podría ser el siguiente:
<?php
// Función de recogida de datos
function recoge($key, $type = "")
{
if (!is_string($key) && !is_int($key) || $key == "") {
trigger_error("Function recoge(): Argument #1 (\$key) must be a non-empty string or an integer", E_USER_ERROR);
} elseif ($type !== "" && $type !== []) {
trigger_error("Function recoge(): Argument #2 (\$type) is optional, but if provided, it must be an empty array or an empty string", E_USER_ERROR);
}
$tmp = $type;
if (isset($_REQUEST[$key])) {
if (!is_array($_REQUEST[$key]) && !is_array($type)) {
$tmp = trim(htmlspecialchars($_REQUEST[$key]));
} elseif (is_array($_REQUEST[$key]) && is_array($type)) {
$tmp = $_REQUEST[$key];
array_walk_recursive($tmp, function (&$value) {
$value = trim(htmlspecialchars($value));
});
}
}
return $tmp;
}
// Variables que recogen los datos
$peso = recoge("peso");
// Variables auxiliares de comprobación
$pesoOk = false;
// Validación de datos y generación de avisos
if ($peso == "") {
print " <p class=\"aviso\">No ha escrito su peso.</p>\n";
print "\n";
} elseif (!is_numeric($peso)) {
print " <p class=\"aviso\">No ha escrito su peso como número.</p>\n";
print "\n";
} elseif ($peso < 40 || $peso > 150) {
print " <p class=\"aviso\">¿Está seguro de que su peso es $peso kg?</p>\n";
print "\n";
} else {
$pesoOk = true;
}
// Si todo es correcto, ejecución del programa
if ($pesoOk) {
print " <p>Su peso es <strong>$peso</strong> kg.</p>\n";
print "\n";
}
?>
Otro caso podría ser la recogida de un valor numérico en la que queremos que el usuario escriba un valor entero (no decimal) positivo.
Nota: Una forma de forzar al usuario a escribir un valor entero en un rango sería utilizar en el formulario un control de tipo number con los atributos min y max, pero debemos pensar que siempre podemos sufrir un ataque de inyección y no recibir el control, o recibir un valor decimal o una cadena de texto. Para facilitar la prueba de valores incorrectos sin necesidad de editar la URL, el ejemplo siguiente utiliza un control de tipo text en vez de number.
<form action="form-recogida-input-number-2-2.php" method="get">
<p>Escriba su edad: <input type="text" name="edad"> años</p>
<p><input type="submit" value="Enviar"></p>
</form>
Este caso es similar al caso anterior de números decimales, pero añadiendo la comprobación de los números decimales (esta comprobación la tendríamos que hacer después de haber comprobado que se trata de un número y antes de comprobar si está fuera del rango deseado):
// Validación de datos y generación de avisos
} elseif (!ctype_digit($edad)) {
print " <p class=\"aviso\">No ha escrito su edad como número entero positivo.</p>\n";
print "\n";
} ...
El proceso de recogida de datos completo podría ser el siguiente:
<?php
// Función de recogida de datos
function recoge($key, $type = "")
{
if (!is_string($key) && !is_int($key) || $key == "") {
trigger_error("Function recoge(): Argument #1 (\$key) must be a non-empty string or an integer", E_USER_ERROR);
} elseif ($type !== "" && $type !== []) {
trigger_error("Function recoge(): Argument #2 (\$type) is optional, but if provided, it must be an empty array or an empty string", E_USER_ERROR);
}
$tmp = $type;
if (isset($_REQUEST[$key])) {
if (!is_array($_REQUEST[$key]) && !is_array($type)) {
$tmp = trim(htmlspecialchars($_REQUEST[$key]));
} elseif (is_array($_REQUEST[$key]) && is_array($type)) {
$tmp = $_REQUEST[$key];
array_walk_recursive($tmp, function (&$value) {
$value = trim(htmlspecialchars($value));
});
}
}
return $tmp;
}
// Variables que recogen los datos
$edad = recoge("edad");
// Variables auxiliares de comprobación
$edadOk = false;
// Validación de datos y generación de avisos
if ($edad == "") {
print " <p class=\"aviso\">No ha escrito su edad.</p>\n";
print "\n";
} elseif (!is_numeric($edad)) {
print " <p class=\"aviso\">No ha escrito su edad como número.</p>\n";
print "\n";
} elseif (!ctype_digit($edad)) {
print " <p class=\"aviso\">No ha escrito su edad como número entero positivo.</p>\n";
print "\n";
} elseif ($edad < 5 || $edad > 120) {
print " <p class=\"aviso\">¿Está seguro de que su edad es $edad?</p>\n";
print "\n";
} else {
$edadOk = true;
}
// Si todo es correcto, ejecución del programa
if ($edadOk) {
print " <p>Su edad es <strong>$edad</strong> años.</p>\n";
print "\n";
}
?>
Otro caso podría ser la recogida de un botón radio en la que queremos que el usuario haya elegido alguna de las opciones propuestas.
Nota: Una forma de forzar al usuario a escribir un valor entero en un rango sería utilizar el atributo required, pero debemos pensar que siempre podemos sufrir un ataque de inyección y no recibir el control o recibir un valor distinto a los esperados. En el ejemplo siguiente, para poder forzar el envío de valores distintos a los que ofrece el formulario debemos manipular la URL y para ello debemos abrir el ejemplo en una nueva pestaña.
<form action="form-recogida-input-radio-2.php" method="get">
<p>
¿Le gusta este formulario?
<input type="radio" name="gusta" value="Sí"> Sí
<input type="radio" name="gusta" value="No"> No
</p>
<p>
<input type="submit" value="Enviar">
<input type="reset" value="Borrar">
</p>
</form>
En este caso mostraremos únicamente dos avisos: si no se ha marcado ninguna opción o si se ha recibido un valor que no corresponde a ninguna de las opciones del control en el formulario.
// Validación de datos y generación de avisos
if ($gusta == "") {
print " <p class=\"aviso\">No ha indicado si le gusta el formulario.</p>\n";
print "\n";
} ...
// Validación de datos y generación de avisos
} elseif ($gusta != "Sí" && $gusta != "No") {
print " <p class=\"aviso\">No ha elegido ninguna de las opciones disponibles.</p>\n";
print "\n";
} ...
Nota: Realmente sería suficiente con la segunda condición, puesto que si el botón se deja en blanco, es un valor distinto de "Sí" y de "No" y el mensaje de aviso es similar. El interés de separar esas dos comprobaciones es que en el primer caso (si no se ha marcado una opción) es un olvido del usuario, pero en el segundo caso (si llega un valor no vacío distinto a los del formulario) se trataría de un ataque de inyección. En una página disponible en la web, en el primer caso sería conveniente informar al usuario, pero en el segundo sería mejor redirigir al formulario o a la página principal del sitio, sin mediar explicaciones (en el ejemplo simplemente mostramos un aviso en ambos casos).
El proceso de recogida de datos completo podría ser el siguiente:
<?php
// Función de recogida de datos
function recoge($key, $type = "")
{
if (!is_string($key) && !is_int($key) || $key == "") {
trigger_error("Function recoge(): Argument #1 (\$key) must be a non-empty string or an integer", E_USER_ERROR);
} elseif ($type !== "" && $type !== []) {
trigger_error("Function recoge(): Argument #2 (\$type) is optional, but if provided, it must be an empty array or an empty string", E_USER_ERROR);
}
$tmp = $type;
if (isset($_REQUEST[$key])) {
if (!is_array($_REQUEST[$key]) && !is_array($type)) {
$tmp = trim(htmlspecialchars($_REQUEST[$key]));
} elseif (is_array($_REQUEST[$key]) && is_array($type)) {
$tmp = $_REQUEST[$key];
array_walk_recursive($tmp, function (&$value) {
$value = trim(htmlspecialchars($value));
});
}
}
return $tmp;
}
// Variables que recogen los datos
$gusta = recoge("gusta");
// Variables auxiliares de comprobación
$gustaOk = false;
// Validación de datos y generación de avisos
if ($gusta == "") {
print " <p class=\"aviso\">No ha indicado si le gusta el formulario.</p>\n";
print "\n";
} elseif ($gusta != "Sí" && $gusta != "No") {
print " <p class=\"aviso\">No ha elegido ninguna de las opciones disponibles.</p>\n";
print "\n";
} else {
$gustaOk = true;
}
// Si todo es correcto, ejecución del programa
if ($gustaOk) {
print " <p><strong>$gusta</strong> le gusta este formulario.</p>\n";
print "\n";
}
?>
Si estamos recogiendo datos provenientes de varios controles, debemos realizar la validación de cada uno de ellos en bloques if ... else ... independientes. En el ejemplo siguiente se muestra la recogida de dos casillas de verificación. Las casillas de verificación son controles independientes unos de otros.
Nota: En el caso de las casillas de verificación, un ataque de inyección lo que haría sería cambiar el valor recibido. En el ejemplo siguiente, para poder forzar el envío de valores distintos a los que ofrece el formulario debemos manipular la URL y para ello debemos abrir el ejemplo en una nueva pestaña.
<form action="form-recogida-input-checkbox-2.php" method="get">
<p>
¿Sabe programar en estos lenguajes?
<input type="checkbox" name="python" value="Py"> Python
<input type="checkbox" name="php" value="PHP"> PHP
</p>
<p><input type="submit" value="Enviar"></p>
</form>
En este caso la validación consistirá únicamente en comprobar si se ha recibido un valor no vacío que no corresponde al valor del control en el formulario ("PHP" o "Py", en este caso). Ambos bloques if ... else ... son muy parecidos.
// Validación de datos y generación de avisos
if ($python != "Py" && $python != "") {
print " <p class=\"aviso\">Por favor, indique si sabe programar o no en Python.</p>\n";
print "\n";
} else {
$pythonOk = true;
}
if ($php != "PHP" && $php != "") {
print " <p class=\"aviso\">Por favor, indique si sabe programar o no en PHP.</p>\n";
print "\n";
} else {
$phpOk = true;
}
El proceso de recogida de datos completo podría ser el siguiente:
<?php
// Función de recogida de datos
function recoge($key, $type = "")
{
if (!is_string($key) && !is_int($key) || $key == "") {
trigger_error("Function recoge(): Argument #1 (\$key) must be a non-empty string or an integer", E_USER_ERROR);
} elseif ($type !== "" && $type !== []) {
trigger_error("Function recoge(): Argument #2 (\$type) is optional, but if provided, it must be an empty array or an empty string", E_USER_ERROR);
}
$tmp = $type;
if (isset($_REQUEST[$key])) {
if (!is_array($_REQUEST[$key]) && !is_array($type)) {
$tmp = trim(htmlspecialchars($_REQUEST[$key]));
} elseif (is_array($_REQUEST[$key]) && is_array($type)) {
$tmp = $_REQUEST[$key];
array_walk_recursive($tmp, function (&$value) {
$value = trim(htmlspecialchars($value));
});
}
}
return $tmp;
}
// Variables que recogen los datos
$python = recoge("python");
$php = recoge("php");
// Variables auxiliares de comprobación
$pythonOk = false;
$phpOk = false;
// Validación de datos y generación de avisos
if ($python != "Py" && $python != "") {
print " <p class=\"aviso\">Por favor, indique si sabe programar o no en Python.</p>\n";
print "\n";
} else {
$pythonOk = true;
}
if ($php != "PHP" && $php != "") {
print " <p class=\"aviso\">Por favor, indique si sabe programar o no en PHP.</p>\n";
print "\n";
} else {
$phpOk = true;
}
// Si todo es correcto, ejecución del programa
if ($pythonOk && $phpOk) {
if ($python && $php) {
print " <p>Sabe programar en Python y en PHP.</p>\n";
print "\n";
} elseif ($python && !$php) {
print " <p>Sabe programar en Python, pero no en PHP.</p>\n";
print "\n";
} elseif (!$python && $php) {
print " <p>Sabe programar en PHP, pero no en Python.</p>\n";
print "\n";
} else {
print " <p>No sabe programar ni en Python ni en PHP.</p>\n";
print "\n";
}
}
?>
En la mayoría de los casos las comprobaciones de cada uno de los controles son independientes, es decir, que el valor recibido de un control es correcto o incorrecto independientemente de los valores recibidos en otros controles. Pero en algunos casos no es así. En esos casos, se aconseja considerar los casos particulares en un bloque if ... else ... adicional posterior a la validación de cada control.
En el ejemplo siguiente se muestra la recogida de tres controles, un botón radio y dos cajas de texto. El programa simula una especie de calculadora básica y además de validar las entradas de forma similar a ejemplos anteriores debemos tener en cuenta que no se puede dividir por cero.
Nota: En el ejemplo siguiente, para poder forzar el envío de valores distintos a los que ofrece el formulario en el botón radio debemos manipular la URL y para ello debemos abrir el ejemplo en una nueva pestaña.
<form action="form-recogida-input-relacionados-2.php" method="get">
<p>
Operación:
<input type="radio" name="operacion" value="suma"> Suma
<input type="radio" name="operacion" value="resta"> Resta
<input type="radio" name="operacion" value="multiplicacion"> Multiplicación
<input type="radio" name="operacion" value="division"> División
</p>
<p>
Números:
<input type="text" name="x">
<input type="text" name="y">
</p>
<p>
<input type="submit" value="Enviar">
<input type="reset" value="Borrar">
</p>
</form>
En este caso las validación de la operación es la correspondiente a un botón radio y las validaciones de los números son las correspondientes a un número decimal.
// Validación de datos y generación de avisos
if ($operacion == "") {
print " <p class=\"aviso\">No ha indicado la operación a realizar.</p>\n";
print "\n";
} elseif ($operacion != "suma" && $operacion != "resta" && $operacion != "multiplicacion" && $operacion != "division") {
print " <p class=\"aviso\">No ha elegido ninguna de las operaciones disponibles.</p>\n";
print "\n";
} else {
$operacionOk = true;
}
if ($x == "") {
print " <p class=\"aviso\">No ha escrito el primer número.</p>\n";
print "\n";
} elseif (!is_numeric($x)) {
print " <p class=\"aviso\">No ha escrito el primer número como número.</p>\n";
print "\n";
} else {
$xOk = true;
}
if ($y == "") {
print " <p class=\"aviso\">No ha escrito el segundo número.</p>\n";
print "\n";
} elseif (!is_numeric($y)) {
print " <p class=\"aviso\">No ha escrito el segundo número como número.</p>\n";
print "\n";
} else {
$yOk = true;
}
if ($operacion == "division" && $y == 0) {
print " <p class=\"aviso\">No se puede dividir por cero.</p>\n";
print "\n";
$operacionOk = false;
}
El proceso de recogida de datos completo podría ser el siguiente:
<?php
// Función de recogida de datos
function recoge($key, $type = "")
{
if (!is_string($key) && !is_int($key) || $key == "") {
trigger_error("Function recoge(): Argument #1 (\$key) must be a non-empty string or an integer", E_USER_ERROR);
} elseif ($type !== "" && $type !== []) {
trigger_error("Function recoge(): Argument #2 (\$type) is optional, but if provided, it must be an empty array or an empty string", E_USER_ERROR);
}
$tmp = $type;
if (isset($_REQUEST[$key])) {
if (!is_array($_REQUEST[$key]) && !is_array($type)) {
$tmp = trim(htmlspecialchars($_REQUEST[$key]));
} elseif (is_array($_REQUEST[$key]) && is_array($type)) {
$tmp = $_REQUEST[$key];
array_walk_recursive($tmp, function (&$value) {
$value = trim(htmlspecialchars($value));
});
}
}
return $tmp;
}
// Variables que recogen los datos
$operacion = recoge("operacion");
$x = recoge("x");
$y = recoge("y");
// Variables auxiliares de comprobación
$operacionOk = false;
$xOk = false;
$yOk = false;
// Validación de datos y generación de avisos
if ($operacion == "") {
print " <p class=\"aviso\">No ha indicado la operación a realizar.</p>\n";
print "\n";
} elseif ($operacion != "suma" && $operacion != "resta" && $operacion != "multiplicacion" && $operacion != "division") {
print " <p class=\"aviso\">No ha elegido ninguna de las operaciones disponibles.</p>\n";
print "\n";
} else {
$operacionOk = true;
}
if ($x == "") {
print " <p class=\"aviso\">No ha escrito el primer número.</p>\n";
print "\n";
} elseif (!is_numeric($x)) {
print " <p class=\"aviso\">No ha escrito el primer número como número.</p>\n";
print "\n";
} else {
$xOk = true;
}
if ($y == "") {
print " <p class=\"aviso\">No ha escrito el segundo número.</p>\n";
print "\n";
} elseif (!is_numeric($y)) {
print " <p class=\"aviso\">No ha escrito el segundo número como número.</p>\n";
print "\n";
} else {
$yOk = true;
}
if ($operacion == "division" && $y == 0) {
print " <p class=\"aviso\">No se puede dividir por cero.</p>\n";
print "\n";
$operacionOk = false;
}
// Si todo es correcto, ejecución del programa
if ($operacionOk && $xOk && $yOk) {
print ($operacion == "suma") ? "<p>$x + $y = " . $x + $y . "</p>" : "";
print ($operacion == "resta") ? "<p>$x - $y = " . $x - $y . "</p>" : "";
print ($operacion == "multiplicacion") ? "<p>$x * $y = " . $x * $y . "</p>" : "";
print ($operacion == "division") ? "<p>$x / $y = " . $x / $y . "</p>" : "";
}
?>