Comprobación de datos

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.

Estructura general

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:

Ejemplo de recogida de texto

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>
Enlace a ejemplo

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";
}
?>

Ejemplo de recogida de número decimal

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>
Enlace a ejemplo

En este caso, podemos dar varios avisos distintos:

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";
}
?>

Ejemplo de recogida de número entero positivo

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>
Enlace a ejemplo

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):

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";
}
?>

Ejemplo de recogida de botón radio

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>
Enlace a ejemplo

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.

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";
}
?>

Ejemplo de recogida de varias casillas de verificació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>
Enlace a ejemplo

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.

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";
    }
}
?>

Ejemplo de recogida de varios controles relacionados entre sí

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>
Enlace a ejemplo

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.

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>" : "";
}
?>