PHP Paso de datos con formularios

De MediaWiki
Ir a la navegación Ir a la búsqueda

Introdución

  • A través da etiqueta <form> podemos enviar datos dunha páxina a outra.
Os principais elementos HTML que poden enviar información dentro dun formulario son os elementos asociados aos formularios HTML.
  • Dentro da etiqueta form temos, entre outros:
  • O atributo method, que normalmente toma os valores get ou post.
  • O atributo action, que indica a URL que vai procesar os datos enviados polo formulario.
  • Se queremos que o formulario se procese na mesma páxina podemos deixar o atributo action baleiro:
1 <form name="form1" action="" method="post">
2 
3 </form>
Outra posibilidade é empregar $_SERVER['PHP_SELF']. Neste caso debemos escapar o seu valor antes de introducilo no HTML:
1 <form name="form1"
2       action="<?php echo htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>"
3       method="post">
4 
5 </form>
  • Importante: os datos recibidos do cliente non deben considerarse fiables. Isto é válido tanto para GET como para POST. A validación HTML ou JavaScript mellora a experiencia de usuario, pero a validación de seguridade debe facerse tamén no servidor.
  • Nota: Para pasar datos dunha páxina a outra tamén podemos empregar campos ocultos (hidden). Un campo oculto non é secreto: o usuario pode velo e modificalo, polo que tamén debe validarse no servidor.


Método GET

  • Co método GET os datos envíanse como parámetros na URL. Por exemplo:
https://exemplo.test/buscar.php?q=php&orde=asc
Os datos situados despois do signo ? forman a cadea de consulta ou query string. Os diferentes parámetros sepáranse normalmente mediante &.
  • Características do método GET:
  • Os parámetros son visibles na URL.
  • A URL pode gardarse nos marcadores, copiarse ou compartirse conservando os parámetros.
  • Os parámetros poden quedar rexistrados no historial do navegador, nos logs do servidor e noutros sistemas intermedios.
  • O usuario pode modificar libremente os valores enviados. Polo tanto, os datos GET sempre deben validarse no servidor.
  • Non existe un límite único de tamaño establecido por PHP para unha URL completa; os límites prácticos dependen do navegador, do servidor web, dos proxies e da infraestrutura empregada.
  • GET é apropiado principalmente para operacións de consulta que non deberían modificar o estado da aplicación.
  • As variables enviadas deste xeito están dispoñibles en PHP mediante o array superglobal $_GET.
  • Vexamos un exemplo utilizando un formulario.

Arquivo: UD2_Form_Ex1_eleccion_pelicula.php

 1 <!DOCTYPE html>
 2 <html lang="gl">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>Formulario</title>
 6 </head>
 7 <body>
 8     <form id="frmPelicula" method="get" action="UD2_Form_Ex1_datos_pelicula.php">
 9         <div>
10             <label for="lstPelicula">Película preferida:</label>
11             <select id="lstPelicula" name="lstPelicula">
12                 <option value="ET">E.T. O extraterrestre</option>
13                 <option value="Superman">Supermán</option>
14             </select>
15         </div>
16 
17         <div>
18             <label for="lstXenero">Xénero preferido:</label>
19             <select id="lstXenero" name="lstXenero">
20                 <option value="Acción">Acción</option>
21                 <option value="Ciencia ficción">Ciencia ficción</option>
22             </select>
23         </div>
24 
25         <button type="submit">ENVIAR</button>
26     </form>
27 </body>
28 </html>
  • Fixarse en que enviamos os datos utilizando o método GET.

Arquivo: UD2_Form_Ex1_datos_pelicula.php

 1 <!DOCTYPE html>
 2 <html lang="gl">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>Datos da película</title>
 6 </head>
 7 <body>
 8 <?php
 9 $pelicula = $_GET['lstPelicula'] ?? '';
10 $xenero = $_GET['lstXenero'] ?? '';
11 
12 $peliculaHtml = htmlspecialchars($pelicula, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
13 $xeneroHtml = htmlspecialchars($xenero, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
14 
15 echo 'Os datos que escolliches son:<br><br>';
16 printf('PELÍCULA: %s<br>XÉNERO: %s', $peliculaHtml, $xeneroHtml);
17 ?>
18 </body>
19 </html>
  • Nota: O operador ?? permite indicar un valor alternativo cando unha clave non existe. Deste xeito evitamos acceder directamente a unha clave inexistente de $_GET.
  • Nota: Para visualizar durante o desenvolvemento o conxunto de valores recibidos pódese empregar var_dump():
var_dump($_GET);


Método POST

  • Co método POST os datos do formulario envíanse no corpo da petición HTTP e non forman parte da URL.
  • Isto non significa que POST sexa secreto nin que os seus datos sexan fiables. Un usuario pode inspeccionar e modificar unha petición POST empregando as ferramentas de desenvolvemento do navegador, curl, Postman ou outras ferramentas. Para protexer os datos durante o transporte debe empregarse HTTPS.
  • O tamaño máximo dos datos recibidos mediante POST está condicionado, entre outras cousas, pola directiva post_max_size de PHP. O seu valor por defecto é 8M, pero pode ser modificado pola configuración do servidor.
No caso da subida de arquivos tamén intervén upload_max_filesize, cuxo valor por defecto é 2M. Para permitir arquivos de maior tamaño, post_max_size debe ser maior que upload_max_filesize.
  • A forma de modificar estas directivas depende de como estea instalado PHP. O arquivo .htaccess só permite empregar directivas php_value cando PHP funciona como módulo de Apache e a configuración do servidor o permite. Con CGI/FastCGI ou PHP-FPM poden existir outros mecanismos, por exemplo .user.ini, segundo a configuración do servidor.
  • As variables enviadas mediante POST están dispoñibles no array superglobal $_POST.
  • Modificamos o exemplo anterior para que empregue POST.

Arquivo: UD2_Form_Ex2_eleccion_pelicula.php

 1 <!DOCTYPE html>
 2 <html lang="gl">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>Formulario</title>
 6 </head>
 7 <body>
 8     <form id="frmPelicula" method="post" action="UD2_Form_Ex2_datos_pelicula.php">
 9         <div>
10             <label for="lstPelicula">Película preferida:</label>
11             <select id="lstPelicula" name="lstPelicula">
12                 <option value="ET">E.T. O extraterrestre</option>
13                 <option value="Superman">Supermán</option>
14             </select>
15         </div>
16 
17         <div>
18             <label for="lstXenero">Xénero preferido:</label>
19             <select id="lstXenero" name="lstXenero">
20                 <option value="Acción">Acción</option>
21                 <option value="Ciencia ficción">Ciencia ficción</option>
22             </select>
23         </div>
24 
25         <button type="submit">ENVIAR</button>
26     </form>
27 </body>
28 </html>

Arquivo: UD2_Form_Ex2_datos_pelicula.php

 1 <!DOCTYPE html>
 2 <html lang="gl">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>Datos da película</title>
 6 </head>
 7 <body>
 8 <?php
 9 $pelicula = $_POST['lstPelicula'] ?? '';
10 $xenero = $_POST['lstXenero'] ?? '';
11 
12 $peliculaHtml = htmlspecialchars($pelicula, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
13 $xeneroHtml = htmlspecialchars($xenero, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
14 
15 echo 'Os datos que escolliches son:<br><br>';
16 printf('PELÍCULA: %s<br>XÉNERO: %s', $peliculaHtml, $xeneroHtml);
17 ?>
18 </body>
19 </html>
  • Nota: Durante o desenvolvemento podemos comprobar os datos recibidos con:
var_dump($_POST);


Comprobando se hai datos

  • A páxina que recibe un formulario normalmente vai validar os datos e realizar algunha operación con eles. Por exemplo, pode facer unha consulta, gardar información ou modificar datos existentes.
  • A validación no cliente mediante HTML ou JavaScript é útil, pero non substitúe a validación no servidor.

Comprobando o método da petición

  • Unha forma segura de saber se unha páxina foi enviada mediante POST é consultar $_SERVER['REQUEST_METHOD']:
1 if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
2     exit('Non se enviou o formulario mediante POST');
3 }
  • Se queremos saber se chegou un campo concreto podemos utilizar isset(), array_key_exists() ou o operador ??, segundo o caso:
1 $nome = $_POST['txtNome'] ?? '';
2 
3 if ($nome === '') {
4     echo 'O campo nome non ten datos';
5 }
  • Nota: exit e die finalizan a execución do script.

Tipo dos datos recibidos

  • Os valores simples procedentes dos campos dun formulario chegan normalmente como cadeas de caracteres. Por exemplo, aínda que un input conteña o texto 25, o valor recibido será unha cadea.
  • Non obstante, $_POST tamén pode conter arrays se empregamos nomes do tipo campo[] ou estruturas como usuario[nome]. Os arquivos subidos non aparecen en $_POST, senón en $_FILES.
  • Por este motivo non debemos facer un cast antes de validar o dato. Primeiro comprobamos que a cadea ten o formato esperado e despois facemos a conversión.
Exemplo para unha idade formada só por díxitos:
1 $idadeTexto = $_POST['txtIdade'] ?? '';
2 
3 if (is_string($idadeTexto) && ctype_digit($idadeTexto)) {
4     $idade = (int) $idadeTexto;
5 } else {
6     echo 'A idade non é un número enteiro válido';
7 }
  • Tamén podemos empregar filtros de validación, como veremos máis adiante.

Comprobando se chegan datos de varios formularios diferentes á mesma páxina

  • Se varios formularios envían datos á mesma páxina é preferible identificar explicitamente a orixe cun campo oculto, en lugar de depender do botón de envío. O botón pode non aparecer na petición nalgúns modos de envío, por exemplo ao premer Intro nun campo.

Formulario da primeira páxina:

<form method="post" action="segunda.php">
    <input type="hidden" name="formulario" value="primeira">
    ...
    <button type="submit">Enviar</button>
</form>

Formulario da segunda páxina:

<form method="post" action="">
    <input type="hidden" name="formulario" value="segunda">
    ...
    <button type="submit">Enviar</button>
</form>

Código de segunda.php:

$orixe = $_POST['formulario'] ?? null;

if ($orixe === 'primeira') {
    // Datos enviados dende primeira.php
} elseif ($orixe === 'segunda') {
    // Datos enviados dende o formulario de segunda.php
} else {
    // A páxina cargouse directamente ou chegou unha orixe non válida
}
  • Importante: un campo oculto tamén pode ser modificado polo usuario. Só serve para identificar a intención do formulario; o seu valor debe comprobarse no servidor.

Validación, normalización e escapado dos campos

  • Ao traballar con datos externos convén diferenciar tres operacións:
  • Validación: comprobar se un dato cumpre as regras esperadas. Por exemplo, comprobar se unha idade é un enteiro entre 0 e 130.
  • Normalización ou saneamento: transformar un dato cando realmente o necesitemos, por exemplo aplicar trim() para eliminar espazos ao principio e ao final.
  • Escapado da saída: adaptar un dato ao contexto no que se vai amosar. Para escribir texto dentro de HTML empregaremos normalmente htmlspecialchars().
  • Non debemos asumir que un dato queda seguro para todos os contextos por aplicarlle un filtro xenérico. A protección depende do uso:
  • HTML → htmlspecialchars().
  • SQL → consultas preparadas con parámetros.
  • URL → rawurlencode() ou http_build_query().
  • Números, correos, IP, etc. → validación específica.

No cliente

  • HTML permite realizar validacións básicas mediante atributos como:
  • required: indica que o campo é obrigatorio.
  • maxlength, minlength, min e max: limitan lonxitudes ou rangos.
  • type="email", type="number", type="date", etc.
  • accept nun campo de tipo file: axuda a indicar ao navegador os tipos de ficheiro que se queren seleccionar.
  • Estas comprobacións poden ser evitadas polo usuario. Por iso deben repetirse no servidor as regras que afecten á integridade ou á seguridade da aplicación.

No servidor: filter_var()

  • PHP dispón da función filter_var(), que permite validar ou sanear unha variable segundo o filtro indicado.
  • Sintaxe simplificada:
filter_var($valor, $filtro, $opcion);
  • Parámetros:
  • $valor: dato que queremos comprobar.
  • $filtro: constante FILTER_VALIDATE_* ou FILTER_SANITIZE_*.
  • $opcion: opcións ou flags adicionais do filtro.

Filtros para a validación

  • FILTER_VALIDATE_BOOLEAN: valida un valor booleano.
  • FILTER_VALIDATE_EMAIL: valida unha dirección de correo electrónico.
  • FILTER_VALIDATE_FLOAT: valida un número de punto flotante.
  • FILTER_VALIDATE_INT: valida un número enteiro.
  • FILTER_VALIDATE_IP: valida unha dirección IP.
  • FILTER_VALIDATE_REGEXP: valida mediante unha expresión regular.
  • FILTER_VALIDATE_URL: valida unha URL.

Filtros de saneamento

  • Algúns filtros de saneamento que continúan dispoñibles son:
  • FILTER_SANITIZE_EMAIL
  • FILTER_SANITIZE_ENCODED
  • FILTER_SANITIZE_NUMBER_FLOAT
  • FILTER_SANITIZE_NUMBER_INT
  • FILTER_SANITIZE_SPECIAL_CHARS
  • FILTER_SANITIZE_FULL_SPECIAL_CHARS
  • FILTER_SANITIZE_URL
  • Importante en PHP 8:
  • FILTER_SANITIZE_MAGIC_QUOTES foi eliminado en PHP 8.0 e non debe empregarse.
  • FILTER_SANITIZE_STRING e FILTER_SANITIZE_STRIPPED están obsoletos dende PHP 8.1. Para escapar texto que vai ser mostrado en HTML empregaremos directamente htmlspecialchars().
  • Un filtro de saneamento non substitúe unha validación. Por exemplo, se esperamos un enteiro é preferible rexeitar 12abc como inválido en lugar de transformalo silenciosamente en 12.

Exemplos

Validando

  • Validando un número enteiro:
1 $valor = '123';
2 
3 $enteiro = filter_var($valor, FILTER_VALIDATE_INT);
4 
5 if ($enteiro === false) {
6     echo 'Valor incorrecto';
7 } else {
8     echo "Valor correcto: $enteiro";
9 }
  • Validando un número flotante:
1 $valor = '-1.45';
2 
3 $decimal = filter_var($valor, FILTER_VALIDATE_FLOAT);
4 
5 if ($decimal === false) {
6     echo 'Valor incorrecto';
7 } else {
8     echo "Valor correcto: $decimal";
9 }
  • Validando un correo electrónico:
1 $correo = 'usuario@example.com';
2 
3 if (filter_var($correo, FILTER_VALIDATE_EMAIL) === false) {
4     echo 'Correo incorrecto';
5 } else {
6     echo 'Correo correcto';
7 }

Escapando texto para HTML

  • Se queremos mostrar nunha páxina un texto recibido do usuario, escapámolo no momento da saída:
1 $texto = '<script>alert("ola")</script>';
2 
3 echo htmlspecialchars(
4     $texto,
5     ENT_QUOTES | ENT_SUBSTITUTE,
6     'UTF-8'
7 );
O navegador mostrará o texto recibido, pero non interpretará as etiquetas como código HTML.
  • Importante: strip_tags() e htmlspecialchars() non fan o mesmo. strip_tags() elimina etiquetas; htmlspecialchars() codifica os caracteres especiais para que se mostren como texto.

Saneando un número flotante

1 $numero = '53.3pp';
2 
3 $resultado = filter_var(
4     $numero,
5     FILTER_SANITIZE_NUMBER_FLOAT,
6     FILTER_FLAG_ALLOW_FRACTION
7 );
8 
9 var_dump($resultado);
Neste caso o resultado será a cadea 53.3. Esta operación transforma o dato; non é equivalente a comprobar que o dato orixinal fose un decimal válido.

Establecendo criterios

  • Podemos validar un enteiro dentro dun rango:
 1 $valor = 25;
 2 
 3 $opcion = [
 4     'options' => [
 5         'min_range' => 10,
 6         'max_range' => 100,
 7     ],
 8 ];
 9 
10 $resultado = filter_var($valor, FILTER_VALIDATE_INT, $opcion);
11 
12 if ($resultado === false) {
13     echo 'Valor incorrecto';
14 } else {
15     echo "Valor correcto: $resultado";
16 }
  • Tamén podemos indicar un valor por defecto:
 1 $valor = -200;
 2 
 3 $opcion = [
 4     'options' => [
 5         'default' => 3,
 6         'min_range' => 10,
 7         'max_range' => 100,
 8     ],
 9 ];
10 
11 $resultado = filter_var($valor, FILTER_VALIDATE_INT, $opcion);
12 echo $resultado;
Neste caso obteremos 3 se o valor non supera a validación.
  • Para un número decimal podemos indicar outro separador decimal mediante a opción decimal:
 1 $valor = '1,23';
 2 
 3 $opcion = [
 4     'options' => [
 5         'decimal' => ',',
 6     ],
 7 ];
 8 
 9 $resultado = filter_var($valor, FILTER_VALIDATE_FLOAT, $opcion);
10 
11 if ($resultado === false) {
12     echo 'Valor incorrecto';
13 } else {
14     echo "Valor correcto: $resultado";
15 }


Seguridade no uso dos formularios

  • Os datos dun formulario están controlados polo cliente e poden ser modificados. A aplicación debe validar a entrada e aplicar a protección apropiada no contexto no que vai empregar cada dato.

Inxección SQL

  • Unha inxección SQL pode producirse cando unha aplicación constrúe unha consulta concatenando directamente datos controlados polo usuario.
Exemplo incorrecto:
1 $nome = $_POST['usuario'] ?? '';
2 $password = $_POST['password'] ?? '';
3 
4 $sql = "SELECT count(*) FROM usuarios
5         WHERE nome='$nome' AND password='$password'";
  • O problema non se resolve eliminando comiñas mediante filtros HTML nin empregando htmlspecialchars(). HTML e SQL son contextos diferentes.

Solución á inxección SQL

  • A solución habitual é empregar consultas preparadas con parámetros. Os datos e o código SQL envíanse ao xestor de base de datos de forma separada.
Exemplo con MySQLi:
 1 $stmt = $pdo->prepare(
 2     'SELECT id, nome, password_hash
 3      FROM usuarios
 4      WHERE nome = :nome'
 5 );
 6 
 7 $stmt->execute([
 8     'nome' => $nome
 9 ]);
10 
11 $usuario = $stmt->fetch(PDO::FETCH_ASSOC);
  • Cando traballemos con contrasinais reais non debemos gardalos nin comparalos en texto claro. Empregaremos password_hash() para gardar o hash e password_verify() para comprobar o contrasinal.
  • mysqli_real_escape_string() pode ser útil nalgúns contextos específicos, pero non é a técnica principal que debemos ensinar para construír consultas con datos externos. A opción preferente son as consultas preparadas.
  • Un procedemento almacenado tampouco evita por si só unha inxección SQL: dependerá de como estea implementado e de se constrúe SQL dinámico internamente.

Cross-Site Scripting (XSS)

  • Un ataque XSS pode producirse cando datos controlados por un usuario se incorporan a unha páxina e o navegador os interpreta como HTML ou JavaScript.
Por exemplo, imaxinemos que recibimos:
1 <script>alert('Exemplo XSS')</script>
Se facemos directamente:
echo $_GET['texto'];
o navegador podería interpretar o contido como marcado ou código.

Solucións ao ataque Cross-Site Scripting (XSS)

  • Para mostrar texto dentro de HTML empregaremos normalmente:
$texto = $_GET['texto'] ?? '';

echo htmlspecialchars(
    $texto,
    ENT_QUOTES | ENT_SUBSTITUTE,
    'UTF-8'
);
  • htmlspecialchars() non elimina o texto: codifica os caracteres con significado especial en HTML para que se mostren como texto.
  • O escapado debe realizarse no momento da saída e segundo o contexto. Un valor preparado para HTML non queda automaticamente preparado para SQL, JavaScript, CSS ou unha URL.
  • strip_tags() pode eliminar etiquetas HTML, pero non debe considerarse unha alternativa xeral ao escapado contextual.
  • Se queremos utilizar $_SERVER['PHP_SELF'] nun atributo HTML, debemos escapar o valor:
<form method="post"
      action="<?php echo htmlspecialchars($_SERVER['PHP_SELF'], ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8'); ?>">
</form>
Para un formulario que se procesa na mesma páxina tamén podemos empregar simplemente:
<form method="post" action="">
</form>

Ataques CSRF (Cross-Site Request Forgery)

Normas xerais

  • Como normas xerais:
  • Validaremos no servidor todos os datos procedentes do cliente.
  • Nos formularios que modifican o estado da aplicación empregaremos protección contra CSRF.
  • Para mostrar texto dentro de HTML empregaremos htmlspecialchars(..., ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8').
  • Para SQL empregaremos consultas preparadas con parámetros.
  • Para contrasinais empregaremos password_hash() e password_verify().
  • Non empregaremos filtros HTML para intentar evitar inxección SQL.
  • Sanearemos ou normalizaremos datos só cando as regras da aplicación o requiran; non substituiremos a validación por unha transformación silenciosa.


Obtendo variables de formularios (POST/GET) e filtrando ao mesmo tempo

  • PHP dispón da función filter_input(), que permite obter unha variable externa e aplicarlle opcionalmente un filtro.
  • Sintaxe actual:
filter_input(
    int $type,
    string $var_name,
    int $filter = FILTER_DEFAULT,
    array|int $options = 0
): mixed
  • Parámetros:
  • type: INPUT_GET, INPUT_POST, INPUT_COOKIE, INPUT_SERVER ou INPUT_ENV.
  • var_name: nome da variable.
  • filter: filtro a aplicar.
  • options: opcións ou flags.
  • Importante: FILTER_DEFAULT é un alias de FILTER_UNSAFE_RAW; é dicir, por defecto non se aplica unha limpeza automática.
  • filter_input() traballa sobre o valor orixinal proporcionado pola SAPI. Se modificamos previamente $_GET ou $_POST dentro do programa e queremos filtrar ese novo valor, empregaremos filter_var().
  • Exemplo validando un identificador enteiro recibido por GET:
 1 $id = filter_input(
 2     INPUT_GET,
 3     'id',
 4     FILTER_VALIDATE_INT,
 5     [
 6         'options' => [
 7             'min_range' => 1,
 8         ],
 9     ]
10 );
11 
12 if ($id === null) {
13     echo 'Non se enviou o parámetro id.';
14 } elseif ($id === false) {
15     echo 'O identificador non é válido.';
16 } else {
17     echo 'Identificador válido: ' . $id;
18 }
  • Exemplo para obter texto e mostralo en HTML:
 1 $buscar = filter_input(INPUT_GET, 'buscar', FILTER_UNSAFE_RAW);
 2 
 3 if (!is_string($buscar)) {
 4     $buscar = '';
 5 }
 6 
 7 $buscarHtml = htmlspecialchars(
 8     $buscar,
 9     ENT_QUOTES | ENT_SUBSTITUTE,
10     'UTF-8'
11 );
12 
13 echo "Buscaches: $buscarHtml";


Redireccionamento de páxinas

  • Despois de procesar un formulario pode ser necesario redireccionar ao usuario a outra páxina.
1 header('Location: /nova_paxina.php');
2 exit;
Importante:
  • A cabeceira debe enviarse antes de producir saída cara ao cliente. Non debe haber HTML, echo, printf nin espazos enviados antes da chamada a header().
  • Despois dun redireccionamento é recomendable executar exit; para impedir que continúe o resto do script.
  • Se queremos enviar parámetros GET nunha redirección é preferible construír a cadea mediante http_build_query():
1 $parametros = http_build_query([
2     'parametro1' => 'valor 1',
3     'parametro2' => 'valor 2',
4 ]);
5 
6 header('Location: /nova_paxina.php?' . $parametros);
7 exit;
  • Na páxina de destino poderemos obter os datos mediante $_GET e validalos.
  • Para inspeccionar as cabeceiras HTTP xa non é necesario instalar complementos específicos. Os navegadores actuais dispoñen das ferramentas de desenvolvemento: normalmente podemos abrir a lapela Rede/Network, seleccionar unha petición e revisar as súas cabeceiras.


Gardando imaxes no servidor

  • Os formularios permiten subir arquivos ao servidor. Un caso típico é asociar unha imaxe a un produto.
  • A subida de arquivos require especial atención á seguridade:
  • Non debemos confiar no nome orixinal enviado polo cliente.
  • Non debemos confiar en $_FILES['...']['type'], xa que esa información é proporcionada polo cliente e PHP non a verifica.
  • Debemos comprobar o código de erro, o tamaño e o tipo real do arquivo.
  • É recomendable xerar un nome novo no servidor.
  • Para arquivos que non necesiten ser servidos directamente polo servidor web é preferible almacenalos fóra do DocumentRoot.
  • Nunca debemos permitir a execución de código nun cartafol de subidas.

Cartafol de destino

  • Nun exemplo sinxelo no que Apache serve directamente as imaxes e o DocumentRoot é /var/www/html, poderiamos crear un cartafol específico:
1 cd /var/www/html
2 mkdir IMAXES
3 chown www-data:www-data IMAXES
4 chmod 750 IMAXES
  • O usuario e grupo reais dependen da instalación. En Debian/Ubuntu é habitual www-data; noutras instalacións pode ser diferente.
  • Evitaremos permisos 777. O proceso que executa PHP só necesita os permisos necesarios para escribir no directorio.

Páxina do formulario

  • Para subir arquivos é obrigatorio utilizar enctype="multipart/form-data".
 1 <!DOCTYPE html>
 2 <html lang="gl">
 3 <head>
 4     <meta charset="UTF-8">
 5     <title>Subida de imaxes</title>
 6 </head>
 7 <body>
 8 
 9 <form action="GardaCampos.php" method="post" enctype="multipart/form-data">
10     <label for="fileToUpload">Selecciona unha imaxe:</label>
11     <input
12         type="file"
13         name="fileToUpload"
14         id="fileToUpload"
15         accept=".jpg,.jpeg,.png,.gif"
16         required
17     >
18 
19     <button type="submit">Subir imaxe</button>
20 </form>
21 
22 </body>
23 </html>
  • O atributo accept axuda ao usuario a seleccionar determinados ficheiros, pero non é unha medida de seguridade. O servidor debe comprobar igualmente o arquivo recibido.

Páxina que procesa os datos

  • O seguinte exemplo:
  • comproba o método da petición;
  • comproba o código de erro;
  • limita o tamaño;
  • verifica que o arquivo procede dunha subida HTTP;
  • detecta o MIME real mediante finfo;
  • só permite determinados tipos de imaxe;
  • xera un nome aleatorio;
  • move o arquivo ao directorio definitivo.
 1 <?php
 2 
 3 if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
 4     exit('Método non permitido.');
 5 }
 6 
 7 if (!isset($_FILES['fileToUpload'])) {
 8     exit('Non se recibiu ningún arquivo.');
 9 }
10 
11 $arquivo = $_FILES['fileToUpload'];
12 
13 if ($arquivo['error'] !== UPLOAD_ERR_OK) {
14     exit('Erro na subida. Código: ' . $arquivo['error']);
15 }
16 
17 // Límite propio da aplicación: 2 MiB.
18 $tamanoMaximo = 2 * 1024 * 1024;
19 
20 if ($arquivo['size'] > $tamanoMaximo) {
21     exit('O arquivo supera o tamaño máximo permitido.');
22 }
23 
24 if (!is_uploaded_file($arquivo['tmp_name'])) {
25     exit('O arquivo non procede dunha subida HTTP válida.');
26 }
27 
28 // Detectamos o MIME a partir do contido real do arquivo.
29 $finfo = new finfo(FILEINFO_MIME_TYPE);
30 $mime = $finfo->file($arquivo['tmp_name']);
31 
32 $tiposPermitidos = [
33     'image/jpeg' => 'jpg',
34     'image/png'  => 'png',
35     'image/gif'  => 'gif',
36 ];
37 
38 if ($mime === false || !isset($tiposPermitidos[$mime])) {
39     exit('Tipo de arquivo non permitido.');
40 }
41 
42 $extension = $tiposPermitidos[$mime];
43 
44 // Non empregamos o nome enviado polo cliente como nome definitivo.
45 $nomeSeguro = bin2hex(random_bytes(16)) . '.' . $extension;
46 
47 $directorioDestino = __DIR__ . '/IMAXES';
48 
49 if (!is_dir($directorioDestino) || !is_writable($directorioDestino)) {
50     exit('O directorio de destino non existe ou non permite escritura.');
51 }
52 
53 $destino = $directorioDestino . DIRECTORY_SEPARATOR . $nomeSeguro;
54 
55 if (!move_uploaded_file($arquivo['tmp_name'], $destino)) {
56     exit('Non foi posible gardar o arquivo.');
57 }
58 
59 echo 'Imaxe gardada correctamente co nome: '
60     . htmlspecialchars($nomeSeguro, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');
  • Neste exemplo empregamos a clase finfo coa constante FILEINFO_MIME_TYPE. Non é necesario chamar a finfo_close(); en PHP 8.5 esa función está obsoleta porque o obxecto se libera automaticamente.

Códigos de erro de subida

  • O campo $_FILES['fileToUpload']['error'] pode ter, entre outros, os seguintes valores:
  • UPLOAD_ERR_OK (0): subida correcta.
  • UPLOAD_ERR_INI_SIZE (1): o arquivo supera upload_max_filesize.
  • UPLOAD_ERR_FORM_SIZE (2): supera MAX_FILE_SIZE, se o formulario enviou ese campo.
  • UPLOAD_ERR_PARTIAL (3): subida parcial.
  • UPLOAD_ERR_NO_FILE (4): non se seleccionou ningún arquivo.
  • UPLOAD_ERR_NO_TMP_DIR (6): non existe o directorio temporal.
  • UPLOAD_ERR_CANT_WRITE (7): PHP non puido escribir no disco.
  • UPLOAD_ERR_EXTENSION (8): unha extensión de PHP detivo a subida.

Funcións útiles

  • basename(): devolve a última parte dunha ruta. Pode ser útil para mostrar o nome orixinal, pero non debemos confiar nel como única medida de seguridade nin utilizalo necesariamente como nome final.
  • pathinfo(): permite obter partes dun nome de ficheiro. A extensión enviada polo usuario non demostra o tipo real do arquivo.
  • getimagesize(): pode obter información dunha imaxe e pode empregarse como comprobación adicional cando a aplicación espera imaxes.
  • is_uploaded_file(): comproba que o ficheiro temporal procede dunha subida HTTP.
  • move_uploaded_file(): move de forma segura un ficheiro subido ao seu destino.

Limitar o tamaño

  • Ademais dos límites de PHP, a aplicación debería establecer o seu propio límite:
$tamanoMaximo = 2 * 1024 * 1024;

if ($_FILES['fileToUpload']['size'] > $tamanoMaximo) {
    exit('O arquivo é demasiado grande.');
}
  • HTML permite enviar un campo oculto MAX_FILE_SIZE:
<form action="GardaCampos.php" method="post" enctype="multipart/form-data">
    <input type="hidden" name="MAX_FILE_SIZE" value="2097152">
    <input type="file" name="fileToUpload">
    <button type="submit">Subir</button>
</form>
  • Importante: este campo está controlado polo cliente e pode ser modificado. Non substitúe os límites configurados en PHP nin a comprobación do tamaño no servidor.

Configuración en php.ini

  • As directivas principais son:
upload_max_filesize = 2M
post_max_size = 8M
  • Estes son os valores por defecto de PHP 8.5, pero un servidor real pode ter outros valores.
  • post_max_size debe ser maior que upload_max_filesize se queremos permitir a subida dun ficheiro dese tamaño.
  • Para saber que arquivo de configuración está cargando PHP en liña de comandos podemos executar:
php --ini
  • Nunha instalación Debian/Ubuntu poden existir arquivos diferentes segundo a SAPI empregada, por exemplo:
/etc/php/8.5/apache2/php.ini
/etc/php/8.5/fpm/php.ini
/etc/php/8.5/cli/php.ini
  • Nota: a ruta exacta depende da instalación e da versión instalada.
  • Se PHP funciona como módulo de Apache e o servidor o permite, algunhas directivas poden configurarse mediante .htaccess:
php_value upload_max_filesize 64M
php_value post_max_size 70M
php_value max_execution_time 300
php_value max_input_time 300
  • Se se utiliza CGI/FastCGI ou PHP-FPM, php_value en .htaccess pode non funcionar. Dependendo do servidor poden empregarse .user.ini, a configuración do pool de PHP-FPM ou a configuración proporcionada polo servizo de aloxamento.


Verificando os datos dun formulario

  • Podemos comprobar varios campos obrigatorios percorrendo un array cos seus nomes.
 1 $camposObrigatorios = ['txtNome', 'txtEmail'];
 2 
 3 $erros = [];
 4 
 5 foreach ($camposObrigatorios as $campo) {
 6     if (!isset($_POST[$campo]) || !is_string($_POST[$campo]) || trim($_POST[$campo]) === '') {
 7         $erros[] = "O campo $campo é obrigatorio.";
 8     }
 9 }
10 
11 if ($erros !== []) {
12     foreach ($erros as $erro) {
13         echo htmlspecialchars($erro, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8') . '<br>';
14     }
15 }
  • Empregamos trim() porque unha cadea formada só por espazos tampouco debería considerarse un valor válido para estes campos.
  • Nota: Non sempre debemos empregar empty() para validar campos obrigatorios, xa que a cadea "0" considérase baleira por empty() e nalgúns formularios pode ser un valor válido.


Creando arrays cos datos dun formulario

  • Nos formularios xerados dinamicamente pode haber un número variable de campos. HTML permite empregar corchetes no atributo name para que PHP reciba os valores nun array.
  • Imaxinemos que queremos introducir o estado de varios exemplares dun libro e que o número de exemplares non se coñece de antemán.
En lugar de crear nomes diferentes:
<input type="text" name="txtEstado1">
<input type="text" name="txtEstado2">
<input type="text" name="txtEstado3">
podemos empregar:
<?php for ($cont = 0; $cont < $numExemplares; $cont++): ?>
    <input type="text" name="txtEstado[]">
<?php endfor; ?>
  • Ao recibir o formulario, $_POST['txtEstado'] será un array.
  • Debemos comprobar que realmente recibimos un array antes de percorrelo:
$estados = $_POST['txtEstado'] ?? [];

if (!is_array($estados)) {
    $estados = [];
}

foreach ($estados as $estado) {
    if (!is_string($estado)) {
        continue;
    }

    $estado = trim($estado);

    // Validar $estado segundo as regras da aplicación.
}


Exercicios propostos

  • Exercicio 1: Crea unha páxina cun formulario e dous campos, nome e idade. O método de envío será POST e o destino será a mesma páxina, sen escribir manualmente o seu nome na URL. O campo nome terá un máximo de vinte caracteres e o campo idade deberá conter exactamente dous díxitos. O formulario terá un botón "Enviar datos". Se algún dato non é válido, amosarase unha mensaxe de erro e volverase amosar o formulario conservando os valores correctos introducidos. Se todos os datos son válidos, amosaranse nunha lista e non se volverá amosar o formulario. Debes diferenciar entre validación da entrada e escapado da saída HTML.
  • Exercicio 2: Fai o necesario para xestionar a alta, baixa e baleirado dunha lista de froitas. O nome dunha froita debe validarse no servidor e todos os valores deben escaparse ao seren mostrados en HTML. Se o campo está baleiro ou non cumpre o formato permitido, amosarase un erro. A lista amosarase nunha táboa e ao carón de cada froita haberá un botón para eliminala. Se non hai elementos amosarase "Cesta baleira". Nun arquivo separado /utilidades/operacionsFroitas.php gardaranse as funcións necesarias. O arquivo é imprescindible e só debe cargarse unha vez, polo que se empregará require_once. Para manter a lista entre peticións neste exercicio pódense empregar campos ocultos, lembrando que os seus valores poden ser modificados polo cliente e deben validarse de novo. Máis adiante substituiremos este mecanismo por sesións.
  • Exercicio 3: Crea nun arquivo separado /utilidades/usuarios.php un array de usuarios. Cada usuario terá: id, nome, teléfono e correo electrónico. Nese mesmo arquivo define unha función obterUsuarioPorID() que reciba un enteiro e devolva o usuario correspondente ou null se non existe. Crea unha páxina listaUsuarios.php que amose os nomes como enlaces a detalleUsuario.php. O identificador enviarase mediante GET. Na páxina de detalle debes validar que o ID recibido é un enteiro positivo e escapar os datos antes de amosalos. Proba a modificar manualmente o parámetro id da URL.


Solución Exercicios propostos

Solución Exercicio 1

<?php

$nome = '';
$idadeTexto = '';
$idade = null;
$erros = [];
$formularioEnviado = $_SERVER['REQUEST_METHOD'] === 'POST';

if ($formularioEnviado) {
    $nomeRecibido = $_POST['nome'] ?? '';
    $idadeRecibida = $_POST['idade'] ?? '';

    if (!is_string($nomeRecibido)) {
        $nomeRecibido = '';
    }

    if (!is_string($idadeRecibida)) {
        $idadeRecibida = '';
    }

    $nome = trim($nomeRecibido);
    $idadeTexto = trim($idadeRecibida);

    // Validación do nome.
    if ($nome === '') {
        $erros[] = 'O campo nome é obrigatorio.';
    } elseif (mb_strlen($nome) > 20) {
        $erros[] = 'O campo nome non pode ter máis de 20 caracteres.';
    } elseif (!preg_match("/^[\p{L}\p{M} .'-]+$/u", $nome)) {
        $erros[] = 'O nome contén caracteres non permitidos.';
    }

    // Validación da idade: exactamente dous díxitos.
    if ($idadeTexto === '') {
        $erros[] = 'O campo idade é obrigatorio.';
    } elseif (!preg_match('/^\d{2}$/', $idadeTexto)) {
        $erros[] = 'A idade debe estar formada exactamente por dous díxitos.';
    } else {
        $idade = (int) $idadeTexto;
    }
}

function escaparHtml(string $valor): string
{
    return htmlspecialchars(
        $valor,
        ENT_QUOTES | ENT_SUBSTITUTE,
        'UTF-8'
    );
}

$datosValidos = $formularioEnviado && $erros === [];
?>
<!DOCTYPE html>
<html lang="gl">
<head>
    <meta charset="UTF-8">
    <title>Formulario</title>
</head>
<body>

<?php if (!$datosValidos): ?>

    <h1>Formulario</h1>

    <?php if ($formularioEnviado && $erros !== []): ?>
        <h2>Erros</h2>
        <ul>
            <?php foreach ($erros as $erro): ?>
                <li><?php echo escaparHtml($erro); ?></li>
            <?php endforeach; ?>
        </ul>
    <?php endif; ?>

    <form method="post" action="">
        <div>
            <label for="nome">Nome:</label>
            <input
                type="text"
                id="nome"
                name="nome"
                maxlength="20"
                required
                value="<?php echo escaparHtml($nome); ?>"
            >
        </div>

        <div>
            <label for="idade">Idade:</label>
            <input
                type="text"
                id="idade"
                name="idade"
                inputmode="numeric"
                maxlength="2"
                size="3"
                required
                value="<?php echo escaparHtml($idadeTexto); ?>"
            >
        </div>

        <button type="submit">Enviar datos</button>
    </form>

<?php else: ?>

    <h1>Datos enviados</h1>
    <ul>
        <li>Nome: <?php echo escaparHtml($nome); ?></li>
        <li>Idade: <?php echo $idade; ?></li>
    </ul>

<?php endif; ?>

</body>
</html>
  • Fixarse en que o nome non se transforma con filtros HTML antes de validalo. Gardamos o valor lóxico e aplicamos htmlspecialchars() unicamente cando o inserimos na saída HTML.


Solución Exercicio 2

Arquivo /utilidades/operacionsFroitas.php
<?php

/**
 * Valida e normaliza o nome dunha froita.
 *
 * @param string $nome Nome recibido.
 * @return string|false Nome normalizado ou false se non é válido.
 */
function verificarNomeFroita(string $nome): string|false
{
    $nome = trim($nome);

    if ($nome === '') {
        return false;
    }

    if (mb_strlen($nome) > 40) {
        return false;
    }

    // Permitimos letras Unicode, espazos, apóstrofos e guións.
    if (!preg_match("/^[\p{L}\p{M} '-]+$/u", $nome)) {
        return false;
    }

    return $nome;
}

/**
 * Engade unha froita se aínda non existe.
 *
 * @param array $lista Lista actual.
 * @param string $froita Froita a engadir.
 * @return array Nova lista.
 */
function engadirFroita(array $lista, string $froita): array
{
    if (!in_array($froita, $lista, true)) {
        $lista[] = $froita;
    }

    return $lista;
}

/**
 * Elimina unha froita da lista.
 *
 * @param array $lista Lista actual.
 * @param string $froita Froita a eliminar.
 * @return array Nova lista.
 */
function eliminarFroita(array $lista, string $froita): array
{
    $indice = array_search($froita, $lista, true);

    if ($indice !== false) {
        unset($lista[$indice]);
        $lista = array_values($lista);
    }

    return $lista;
}
Arquivo xestionFroitas.php
<?php

require_once __DIR__ . '/utilidades/operacionsFroitas.php';

function escaparHtml(string $valor): string
{
    return htmlspecialchars(
        $valor,
        ENT_QUOTES | ENT_SUBSTITUTE,
        'UTF-8'
    );
}

$listaFroitas = [];
$erro = null;

// Recuperamos a lista dos campos ocultos.
// Estes valores veñen do cliente, polo que os validamos novamente.
$listaRecibida = $_POST['listaFroitas'] ?? [];

if (is_array($listaRecibida)) {
    foreach ($listaRecibida as $froitaRecibida) {
        if (!is_string($froitaRecibida)) {
            continue;
        }

        $froitaValida = verificarNomeFroita($froitaRecibida);

        if ($froitaValida !== false) {
            $listaFroitas = engadirFroita($listaFroitas, $froitaValida);
        }
    }
}

if ($_SERVER['REQUEST_METHOD'] === 'POST') {
    if (isset($_POST['btnBaleirar'])) {
        $listaFroitas = [];
    }

    if (isset($_POST['btnEngadir'])) {
        $novaFroita = $_POST['novaFroita'] ?? '';

        if (!is_string($novaFroita)) {
            $novaFroita = '';
        }

        $novaFroita = verificarNomeFroita($novaFroita);

        if ($novaFroita === false) {
            $erro = 'O nome da froita non é válido.';
        } else {
            $listaFroitas = engadirFroita($listaFroitas, $novaFroita);
        }
    }

    if (isset($_POST['btnBorrar']) && is_string($_POST['btnBorrar'])) {
        $froitaAEliminar = verificarNomeFroita($_POST['btnBorrar']);

        if ($froitaAEliminar !== false) {
            $listaFroitas = eliminarFroita($listaFroitas, $froitaAEliminar);
        }
    }
}
?>
<!DOCTYPE html>
<html lang="gl">
<head>
    <meta charset="UTF-8">
    <title>Xestión de froitas</title>
</head>
<body>

<h1>Xestión de froitas</h1>

<?php if ($erro !== null): ?>
    <p><?php echo escaparHtml($erro); ?></p>
<?php endif; ?>

<form method="post" action="">

    <?php foreach ($listaFroitas as $clave => $froita): ?>
        <input
            type="hidden"
            name="listaFroitas[<?php echo $clave; ?>]"
            value="<?php echo escaparHtml($froita); ?>"
        >
    <?php endforeach; ?>

    <?php if ($listaFroitas === []): ?>
        <p>Cesta baleira.</p>
    <?php else: ?>
        <table>
            <thead>
            <tr>
                <th>Froita</th>
                <th>Acción</th>
            </tr>
            </thead>
            <tbody>

            <?php foreach ($listaFroitas as $froita): ?>
                <tr>
                    <td><?php echo escaparHtml($froita); ?></td>
                    <td>
                        <button
                            type="submit"
                            name="btnBorrar"
                            value="<?php echo escaparHtml($froita); ?>"
                        >
                            Eliminar
                        </button>
                    </td>
                </tr>
            <?php endforeach; ?>

            </tbody>
        </table>
    <?php endif; ?>

    <div>
        <label for="novaFroita">Nome da froita:</label>
        <input type="text" id="novaFroita" name="novaFroita" maxlength="40">
        <button type="submit" name="btnEngadir" value="1">Engadir froita</button>
    </div>

    <?php if ($listaFroitas !== []): ?>
        <button type="submit" name="btnBaleirar" value="1">Baleirar cesta</button>
    <?php endif; ?>

</form>

</body>
</html>
  • Nota: Este exercicio conserva o estado mediante campos ocultos porque as sesións aínda non se estudaron. Os campos ocultos poden ser manipulados polo cliente; por iso a solución volve validar todas as froitas recibidas. Nunha aplicación real, para manter estado interno empregaremos normalmente sesións ou persistencia no servidor.


Solución Exercicio 3

Arquivo utilidades/usuarios.php
<?php

$usuarios = [
    [
        'id' => 1,
        'nome' => 'Usuario 1',
        'tfno' => '123456789',
        'email' => 'usuario1@example.com',
    ],
    [
        'id' => 2,
        'nome' => 'Usuario 2',
        'tfno' => '987654321',
        'email' => 'usuario2@example.com',
    ],
    [
        'id' => 3,
        'nome' => 'Usuario 3',
        'tfno' => '555555555',
        'email' => 'usuario3@example.com',
    ],
];

/**
 * Obtén os datos dun usuario a partir do seu ID.
 */
function obterUsuarioPorID(int $id): ?array
{
    global $usuarios;

    foreach ($usuarios as $usuario) {
        if ($usuario['id'] === $id) {
            return $usuario;
        }
    }

    return null;
}
Arquivo listaUsuarios.php
<?php

require_once __DIR__ . '/utilidades/usuarios.php';

function escaparHtml(string $valor): string
{
    return htmlspecialchars(
        $valor,
        ENT_QUOTES | ENT_SUBSTITUTE,
        'UTF-8'
    );
}
?>
<!DOCTYPE html>
<html lang="gl">
<head>
    <meta charset="UTF-8">
    <title>Listado de usuarios</title>
</head>
<body>

<h1>Listado de usuarios</h1>

<ul>
<?php foreach ($usuarios as $usuario): ?>
    <?php
    $url = 'detalleUsuario.php?' . http_build_query([
        'id' => $usuario['id'],
    ]);
    ?>
    <li>
        <a href="<?php echo escaparHtml($url); ?>">
            <?php echo escaparHtml($usuario['nome']); ?>
        </a>
    </li>
<?php endforeach; ?>
</ul>

</body>
</html>
Arquivo detalleUsuario.php
<?php

require_once __DIR__ . '/utilidades/usuarios.php';

function escaparHtml(string $valor): string
{
    return htmlspecialchars(
        $valor,
        ENT_QUOTES | ENT_SUBSTITUTE,
        'UTF-8'
    );
}

$idRecibido = $_GET['id'] ?? null;

$usuarioSeleccionado = null;
$erro = null;

if ($idRecibido === null) {
    $erro = 'Non se indicou ningún identificador.';
} elseif (!is_string($idRecibido)) {
    $erro = 'O identificador non é válido.';
} else {
    $idUsuario = filter_var(
        $idRecibido,
        FILTER_VALIDATE_INT,
        [
            'options' => [
                'min_range' => 1,
            ],
        ]
    );

    if ($idUsuario === false) {
        $erro = 'O identificador non é válido.';
    } else {
        $usuarioSeleccionado = obterUsuarioPorID($idUsuario);

        if ($usuarioSeleccionado === null) {
            $erro = 'Non existe ningún usuario con ese identificador.';
        }
    }
}
?>
<!DOCTYPE html>
<html lang="gl">
<head>
    <meta charset="UTF-8">
    <title>Detalles do usuario</title>
</head>
<body>

<h1>Detalles do usuario</h1>

<?php if ($erro !== null): ?>

    <p><?php echo escaparHtml($erro); ?></p>

<?php else: ?>

    <dl>
        <dt>Nome</dt>
        <dd><?php echo escaparHtml($usuarioSeleccionado['nome']); ?></dd>

        <dt>Teléfono</dt>
        <dd><?php echo escaparHtml($usuarioSeleccionado['tfno']); ?></dd>

        <dt>Correo</dt>
        <dd><?php echo escaparHtml($usuarioSeleccionado['email']); ?></dd>
    </dl>

<?php endif; ?>

<p><a href="listaUsuarios.php">Volver á lista de usuarios</a></p>

</body>
</html>


-- Ángel D. Fernández González -- (2026).