PHP Paso de datos con formularios
Sumario
- 1 Introdución
- 2 Método GET
- 3 Método POST
- 4 Comprobando se hai datos
- 5 Validación, normalización e escapado dos campos
- 6 Seguridade no uso dos formularios
- 7 Obtendo variables de formularios (POST/GET) e filtrando ao mesmo tempo
- 8 Redireccionamento de páxinas
- 9 Gardando imaxes no servidor
- 10 Verificando os datos dun formulario
- 11 Creando arrays cos datos dun formulario
- 12 Exercicios propostos
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
formtemos, 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
actionbaleiro:
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_sizedebe ser maior queupload_max_filesize.
- A forma de modificar estas directivas depende de como estea instalado PHP. O arquivo
.htaccesssó permite empregar directivasphp_valuecando 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
diefinalizan 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
inputconteña o texto25, o valor recibido será unha cadea.
- Non obstante,
$_POSTtamén pode conter arrays se empregamos nomes do tipocampo[]ou estruturas comousuario[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()ouhttp_build_query(). - Números, correos, IP, etc. → validación específica.
- HTML →
No cliente
- HTML permite realizar validacións básicas mediante atributos como:
required: indica que o campo é obrigatorio.maxlength,minlength,minemax: limitan lonxitudes ou rangos.type="email",type="number",type="date", etc.acceptnun campo de tipofile: 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: constanteFILTER_VALIDATE_*ouFILTER_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_EMAILFILTER_SANITIZE_ENCODEDFILTER_SANITIZE_NUMBER_FLOATFILTER_SANITIZE_NUMBER_INTFILTER_SANITIZE_SPECIAL_CHARSFILTER_SANITIZE_FULL_SPECIAL_CHARSFILTER_SANITIZE_URL
- Importante en PHP 8:
FILTER_SANITIZE_MAGIC_QUOTESfoi eliminado en PHP 8.0 e non debe empregarse.FILTER_SANITIZE_STRINGeFILTER_SANITIZE_STRIPPEDestán obsoletos dende PHP 8.1. Para escapar texto que vai ser mostrado en HTML empregaremos directamentehtmlspecialchars().
- Un filtro de saneamento non substitúe unha validación. Por exemplo, se esperamos un enteiro é preferible rexeitar
12abccomo inválido en lugar de transformalo silenciosamente en12.
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()ehtmlspecialchars()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
3se 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 epassword_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.
- Os parámetros empregados teñen o seguinte significado:
- $texto: cadea que queremos converter para poder amosala de forma segura dentro de HTML.
- ENT_QUOTES: indica que, ademais dos caracteres especiais de HTML, se deben converter tanto as comiñas dobres (") como as comiñas simples (').
- ENT_SUBSTITUTE: emprégase para que en caso de enviar caracteres non válidos UTF-8 os sustitúa polo carácter �.
- Isto é especialmente importante cando o dato vai ser empregado dentro dun atributo HTML.
- Visualmente non notaredes nada no navegador, pero se mirades o código fonte de páxina podedes comprobar como cambia as comillas e outros caracteres por códigos.
- 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)
- Os ataques CSRF trataranse cando vexamos as variables de sesión na UD4.
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()epassword_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_SERVERouINPUT_ENV. - var_name: nome da variable.
- filter: filtro a aplicar.
- options: opcións ou flags.
- type:
- Importante:
FILTER_DEFAULTé un alias deFILTER_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$_GETou$_POSTdentro do programa e queremos filtrar ese novo valor, empregaremosfilter_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.
- Podemos facelo coa función header():
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,printfnin espazos enviados antes da chamada aheader(). - Despois dun redireccionamento é recomendable executar
exit;para impedir que continúe o resto do script.
- A cabeceira debe enviarse antes de producir saída cara ao cliente. Non debe haber HTML,
- 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
$_GETe 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
acceptaxuda 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 afinfo_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 superaupload_max_filesize.UPLOAD_ERR_FORM_SIZE (2): superaMAX_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_sizedebe ser maior queupload_max_filesizese 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_valueen.htaccesspode 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 porempty()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
namepara 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.phpgardaranse 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.phpun array de usuarios. Cada usuario terá: id, nome, teléfono e correo electrónico. Nese mesmo arquivo define unha funciónobterUsuarioPorID()que reciba un enteiro e devolva o usuario correspondente ounullse non existe. Crea unha páxinalistaUsuarios.phpque amose os nomes como enlaces adetalleUsuario.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ámetroidda 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).