PHP Upload de Arquivos
Foto de perfil, currículo em PDF, comprovante de pagamento: muitos sites recebem arquivos. Em PHP, isso usa o mesmo formulário que você já conhece, com dois detalhes a mais, e uma superglobal nova, $_FILES. Receber é fácil; o cuidado está em conferir o que chegou, porque um arquivo enviado por um desconhecido é a coisa mais perigosa que um site pode guardar.
- O formulário de upload:
method="post",enctype="multipart/form-data"e<input type="file">. - A estrutura de
$_FILES, vista comvar_dump:name,type,tmp_name,error,size. - Como guardar o arquivo com
move_uploaded_file. - Por que não confiar na extensão nem no
type, e como descobrir o tipo de verdade comfinfo. - Os limites de tamanho:
upload_max_filesize,post_max_sizeeMAX_FILE_SIZE. - Os códigos de erro
UPLOAD_ERR_*. - Como dar ao arquivo um nome seguro.
A ideia: a encomenda que chega na portaria
Quando chega um pacote no prédio da Ana, o porteiro não leva direto para o apartamento. Ele guarda numa sala de espera e anota numa ficha: nome escrito na etiqueta, tamanho, se chegou inteiro. A Ana então decide: abre a caixa, confere se é mesmo o que encomendou e leva para casa. Se não buscar, o pacote é descartado.
O upload no PHP é igualzinho:
- O PHP recebe o arquivo e o guarda numa pasta temporária (a sala de espera), com um nome aleatório.
- Ele preenche uma ficha em
$_FILES: nome original, tamanho, tipo informado, se houve erro. - O seu programa confere e, se estiver tudo certo, move o arquivo para uma pasta definitiva. Se não mover, o PHP apaga o temporário assim que a página termina.
E, como na portaria, a etiqueta do pacote pode mentir: o nome e o tipo informados foram escritos por quem enviou.
O formulário de upload
<form action="upload.php" method="post" enctype="multipart/form-data"> <p>Foto: <input type="file" name="foto" accept="image/*"></p> <p>Legenda: <input name="legenda"></p> <button>Enviar</button> </form>
- method="post"
- Obrigatório. Arquivos não cabem no endereço (GET).
- enctype="multipart/form-data"
- Encoding type, "tipo de codificação". O formato normal (
nome=valor&…, de Formulários HTML) só leva texto. Omultipart/form-datadivide o pedido em "partes" separadas, e cada parte pode ser um campo de texto ou um arquivo inteiro. Sem isso, o arquivo não vai. - <input type="file" name="foto">
- O botão "Escolher arquivo". O
nameé a chave em$_FILES. - accept="image/*"
- Sugere ao navegador mostrar só imagens na janela de escolha. É conforto, não proteção: a pessoa pode trocar para "todos os arquivos".
- name="legenda"
- Campos de texto continuam funcionando normalmente, e chegam em
$_POST.
$_FILES por dentro
Vamos ver a "ficha" que o PHP preenche. Este programa só mostra as duas superglobais:
<?php var_dump($_FILES); var_dump($_POST);
No laboratório, o envio é feito com o curl -F (form), que monta um pedido multipart/form-data como o navegador. -F "foto=@gato.png" quer dizer "o campo foto é o arquivo gato.png" (o @ indica arquivo); -F "legenda=Meu gato" é um campo de texto:
$ curl -s localhost:8000/dump.php -F "foto=@gato.png" -F "legenda=Meu gato"
array(1) {
["foto"]=>
array(6) {
["name"]=>
string(8) "gato.png"
["full_path"]=>
string(8) "gato.png"
["type"]=>
string(9) "image/png"
["tmp_name"]=>
string(27) "/tmp/phpv324i422nmvt0CCFjoC"
["error"]=>
int(0)
["size"]=>
int(124)
}
}
array(1) {
["legenda"]=>
string(8) "Meu gato"
}
| Chave | Exemplo | O que é | Confiável? |
|---|---|---|---|
name | gato.png | Nome do arquivo no computador de quem enviou | Não: escrito por quem enviou |
full_path | gato.png | O caminho completo, quando se envia uma pasta inteira (PHP 8.1+) | Não |
type | image/png | O tipo que o navegador disse que é (o "MIME type") | Não: é só a etiqueta do pacote |
tmp_name | /tmp/phpv324… | Onde o PHP guardou o arquivo de verdade (a sala de espera) | Sim: quem criou foi o PHP |
error | 0 | Código do resultado: 0 = deu certo (veja a tabela de erros) | Sim |
size | 124 | Tamanho em bytes | Sim: o PHP mediu |
O tmp_name é diferente a cada envio, e o arquivo nele some quando o PHP termina de responder. Por isso o próximo passo é obrigatório: mover.
Vários arquivos: name="fotos[]" e o formato "virado"
Com <input type="file" name="fotos[]" multiple>, a pessoa escolhe vários arquivos de uma vez. Os colchetes funcionam como em $_GET e $_POST, mas o array sai num formato curioso: em vez de uma ficha por arquivo, vem uma lista por chave:
$ curl -s localhost:8000/dump.php -F "fotos[]=@gato.png" -F "fotos[]=@bolo.jpg"
array(1) {
["fotos"]=>
array(6) {
["name"]=>
array(2) {
[0]=>
string(8) "gato.png"
[1]=>
string(8) "bolo.jpg"
}
…
["error"]=>
array(2) {
[0]=>
int(0)
[1]=>
int(0)
}
["size"]=>
array(2) {
[0]=>
int(124)
[1]=>
int(716)
}
}
}
O primeiro arquivo é $_FILES['fotos']['name'][0], $_FILES['fotos']['tmp_name'][0], e assim por diante. Para percorrer:
foreach ($_FILES['fotos']['name'] as $i => $nome) {
$erro = $_FILES['fotos']['error'][$i];
$tmp = $_FILES['fotos']['tmp_name'][$i];
// confira e mova cada um, como no exemplo completo abaixo
}
Um formulário de upload de verdade. Rode, escolha um arquivo qualquer do seu computador na aba Resultado e envie: o var_dump mostra a ficha que o PHP preencheu. Tente também enviar sem escolher arquivo (repare no error), e depois apague o enctype e envie de novo. No editor do guia, o arquivo vai para o PHP que roda dentro do seu próprio navegador: nada é enviado para a internet. Se o seu navegador não deixar escolher arquivos aqui dentro, use a caixa logo abaixo, em que um campo de texto faz o papel do arquivo.
<form method="post" enctype="multipart/form-data"> <p>Arquivo: <input type="file" name="foto"></p> <p>Legenda: <input name="legenda"></p> <button>Enviar</button> </form> <h3>$_FILES</h3> <pre><?php var_dump($_FILES); ?></pre> <h3>$_POST</h3> <pre><?php var_dump($_POST); ?></pre>
Guardar: move_uploaded_file
Para tirar o arquivo da sala de espera, use move_uploaded_file ("mover arquivo enviado"):
<?php
$arq = $_FILES['foto'];
$destino = 'uploads/' . $arq['name'];
if (move_uploaded_file($arq['tmp_name'], $destino)) {
echo "Salvo em $destino\n";
} else {
echo "Não foi possível salvar.\n";
}
- move_uploaded_file(
- Move o arquivo e devolve
true(deu certo) oufalse. - $arq['tmp_name'],
- De onde: o arquivo temporário. A função só aceita arquivos que chegaram por upload neste pedido; um caminho qualquer, como
/etc/passwd, é recusado. Por isso use ela, e nãorenameoucopy. - $destino)
- Para onde: o caminho final, com o nome do arquivo. A pasta precisa existir e o PHP precisa ter permissão de escrita nela.
A primeira vez que a Ana testou, a pasta uploads ainda não existia:
$ curl -s localhost:8000/mover.php -F "foto=@gato.png" <br /> <b>Warning</b>: move_uploaded_file(uploads/gato.png): Failed to open stream: No such file or directory in <b>/home/ana/site/mover.php</b> on line <b>4</b><br /> <br /> <b>Warning</b>: move_uploaded_file(): Unable to move "/tmp/php5942e2shniftfKdlkkp" to "uploads/gato.png" in <b>/home/ana/site/mover.php</b> on line <b>4</b><br /> Não foi possível salvar. $ mkdir uploads $ curl -s localhost:8000/mover.php -F "foto=@gato.png" Salvo em uploads/gato.png $ ls -l uploads total 4 -rw-rw-r-- 1 ana ana 124 out 16 20:58 gato.png
Funcionou, mas este mover.php tem três falhas graves: não confere se houve erro, aceita qualquer tipo de arquivo e usa o nome escolhido por quem enviou. As próximas seções fecham cada uma.
Os códigos de erro (UPLOAD_ERR_*)
A chave error diz como foi o envio. Cada número tem uma constante com nome, que deixa o código mais legível:
| Valor | Constante | Significa | O que dizer ao visitante |
|---|---|---|---|
0 | UPLOAD_ERR_OK | Deu tudo certo | — |
1 | UPLOAD_ERR_INI_SIZE | Maior que o upload_max_filesize do php.ini | "Arquivo grande demais" |
2 | UPLOAD_ERR_FORM_SIZE | Maior que o campo MAX_FILE_SIZE do formulário | "Arquivo grande demais" |
3 | UPLOAD_ERR_PARTIAL | Chegou só um pedaço (conexão caiu) | "Tente de novo" |
4 | UPLOAD_ERR_NO_FILE | Nenhum arquivo foi escolhido | "Escolha um arquivo" |
6 | UPLOAD_ERR_NO_TMP_DIR | Falta a pasta temporária no servidor | Problema do servidor: avise o administrador |
7 | UPLOAD_ERR_CANT_WRITE | Não conseguiu gravar no disco (cheio, sem permissão) | Problema do servidor |
8 | UPLOAD_ERR_EXTENSION | Uma extensão do PHP interrompeu o envio | Problema do servidor |
(Não existe o 5.) Enviar o formulário sem escolher arquivo dá uma ficha "vazia", com error 4:
$ curl -s localhost:8000/dump.php -F "foto=@/dev/null;filename="
array(1) {
["foto"]=>
array(6) {
["name"]=>
string(0) ""
["full_path"]=>
string(0) ""
["type"]=>
string(0) ""
["tmp_name"]=>
string(0) ""
["error"]=>
int(4)
["size"]=>
int(0)
}
}
…
(O filename= vazio é como o navegador avisa "nenhum arquivo escolhido".) Repare: $_FILES['foto'] existe mesmo sem arquivo. Por isso não basta isset($_FILES['foto']): confira o error.
Limites de tamanho
Três configurações controlam o tamanho, e é bom conhecer as três porque cada uma falha de um jeito:
| Limite | Onde | Padrão | Se passar |
|---|---|---|---|
upload_max_filesize | php.ini | 2M | Aquele arquivo vem com error 1; os outros campos chegam normalmente |
post_max_size | php.ini | 8M | O pedido inteiro é descartado: $_POST e $_FILES chegam vazios |
MAX_FILE_SIZE | Campo escondido no formulário | — | error 2. Só ajuda; quem envia pode mudar o valor |
Os padrões do PHP da Ana (sem php.ini):
$ php -i | grep -E "upload_max_filesize|post_max_size|max_file_uploads" max_file_uploads => 20 => 20 post_max_size => 8M => 8M upload_max_filesize => 2M => 2M
Um arquivo de 3 MB (passa dos 2M) e um de 9 MB (passa até dos 8M do pedido inteiro), enviados para este programa:
<?php var_dump($_FILES['foto']['error'] ?? 'nada em $_FILES'); var_dump($_POST);
$ curl -s localhost:8000/erro.php -F "foto=@grande.bin" -F "legenda=x"
int(1) « UPLOAD_ERR_INI_SIZE
array(1) {
["legenda"]=>
string(1) "x" « o resto do formulário chegou
}
$ curl -s localhost:8000/erro.php -F "foto=@enorme.bin" -F "legenda=x"
<br />
<b>Warning</b>: PHP Request Startup: POST Content-Length of 9000317 bytes exceeds the limit of 8388608 bytes in <b>Unknown</b> on line <b>0</b><br />
string(15) "nada em $_FILES"
array(0) { « nem a legenda chegou
}
O segundo caso é traiçoeiro: o PHP descarta tudo antes do seu código rodar ("Request Startup" = início do pedido; "Unknown on line 0" porque nenhum arquivo seu estava rodando ainda). Para o seu programa, parece que o formulário veio vazio. Se REQUEST_METHOD é POST mas $_POST e $_FILES estão vazios, desconfie do post_max_size.
O campo MAX_FILE_SIZE vai antes do <input type="file">:
<input type="hidden" name="MAX_FILE_SIZE" value="1000000"> <input type="file" name="foto">
$ curl -s localhost:8000/erro.php -F "MAX_FILE_SIZE=500" -F "foto=@bolo.jpg"
int(2) « bolo.jpg tem 716 bytes: UPLOAD_ERR_FORM_SIZE
array(1) {
["MAX_FILE_SIZE"]=>
string(3) "500"
}
Para aceitar arquivos maiores, aumente os dois no php.ini (com post_max_size um pouco maior que upload_max_filesize, porque o pedido também leva os outros campos), ou, no servidor de testes, na linha de comando:
$ php -d upload_max_filesize=10M -d post_max_size=12M -S localhost:8000
E, no seu código, tenha o seu limite ($arq['size'] > 1_000_000), que não depende da configuração do servidor.
Validar o tipo: não confie na extensão
Uma foto chamada gato.jpg é uma foto? Não necessariamente. O nome (e a extensão .jpg) e o type (image/jpeg) foram escritos por quem enviou. A Ana copiou um arquivo de texto com o nome falsa.jpg e mandou com o curl dizendo que era JPEG:
<?php $arq = $_FILES['foto']; echo "O navegador disse: ", $arq['type'], "\n"; $finfo = new finfo(FILEINFO_MIME_TYPE); echo "O finfo viu: ", $finfo->file($arq['tmp_name']), "\n"; echo "mime_content_type: ", mime_content_type($arq['tmp_name']), "\n";
$ curl -s localhost:8000/tipo.php -F "foto=@falsa.jpg;type=image/jpeg" O navegador disse: image/jpeg « a etiqueta mentiu O finfo viu: text/plain « o conteúdo é texto mime_content_type: text/plain $ curl -s localhost:8000/tipo.php -F "foto=@bolo.jpg" O navegador disse: image/jpeg O finfo viu: image/jpeg mime_content_type: image/jpeg
- new finfo(FILEINFO_MIME_TYPE)
- Cria um "inspetor de arquivos" (file info) da extensão
fileinfo. Ele abre o arquivo e olha os primeiros bytes, a "assinatura" que todo formato tem (um PNG sempre começa com os bytes‰PNG, um JPEG comFF D8 FF).FILEINFO_MIME_TYPEpede a resposta no formatotipo/subtipo. - $finfo->file($caminho)
- Devolve o tipo descoberto pelo conteúdo. (A seta
->chama um método de um objeto, assunto de Classes e Objetos; aqui, basta copiar a forma.) - mime_content_type($caminho)
- Faz o mesmo numa função só. Use a que achar mais simples.
Por que isso importa? O pior caso é alguém enviar um programa PHP com nome de foto. Se ele for salvo numa pasta acessível pelo navegador com a extensão .php, o atacante abre o endereço e o servidor executa o programa dele. O finfo não se engana com o nome:
$ php tipos.php falsa.jpg: text/plain foto.png: image/png script.jpg: text/x-php « um programa PHP disfarçado de .jpg
A defesa tem duas partes, e as duas são necessárias:
- Aceitar só os tipos que você espera, descobertos pelo
finfo, numa lista (['image/jpeg' => 'jpg', 'image/png' => 'png']). - Escolher a extensão do arquivo salvo a partir dessa lista, nunca a partir do nome original. Assim, mesmo um arquivo malicioso nunca é salvo como
.php.
Nomes seguros
Usar $arq['name'] como nome do arquivo salvo traz vários problemas:
- Sobrescrever: duas pessoas enviam
foto.jpg, e a segunda apaga a primeira. - Extensões perigosas:
foto.php,.htaccess. - Caracteres problemáticos: espaços, acentos, parênteses (
minha foto (1).PNG), nomes enormes. - Caminhos: um nome como
../../index.phptentaria sair da pasta. O PHP já corta o caminho doname(fica sóindex.php), mas ofull_pathmantém tudo:
$ curl -s localhost:8000/dump.php -F "foto=@receita.txt;filename=../../index.php"
array(1) {
["foto"]=>
array(6) {
["name"]=>
string(9) "index.php"
["full_path"]=>
string(15) "../../index.php"
…
A solução é inventar o nome: letras aleatórias + a extensão da sua lista. random_bytes(16) gera 16 bytes aleatórios, e bin2hex os escreve como 32 letras e números (0-9, a-f):
$novoNome = bin2hex(random_bytes(16)) . '.' . $tipos[$tipo]; // por exemplo: 1a99be15d05c6e4708f3bc45fd5c9c9b.png
Se precisar mostrar o nome original depois ("gato.png"), guarde-o num banco de dados junto do nome novo, e mostre-o com htmlspecialchars (Escapar a Saída).
Tudo junto: o upload seguro
<?php
$mensagem = '';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$arq = $_FILES['foto'] ?? null;
$tipos = ['image/jpeg' => 'jpg', 'image/png' => 'png', 'image/webp' => 'webp'];
if ($arq === null || !is_int($arq['error'])) {
$mensagem = 'Nenhum arquivo recebido.';
} elseif ($arq['error'] === UPLOAD_ERR_NO_FILE) {
$mensagem = 'Escolha uma foto.';
} elseif ($arq['error'] === UPLOAD_ERR_INI_SIZE || $arq['error'] === UPLOAD_ERR_FORM_SIZE) {
$mensagem = 'A foto é grande demais.';
} elseif ($arq['error'] !== UPLOAD_ERR_OK) {
$mensagem = 'O envio falhou (código ' . $arq['error'] . ').';
} elseif ($arq['size'] > 1_000_000) {
$mensagem = 'A foto pode ter no máximo 1 MB.';
} else {
$finfo = new finfo(FILEINFO_MIME_TYPE);
$tipo = $finfo->file($arq['tmp_name']);
if (!isset($tipos[$tipo])) {
$mensagem = 'Envie uma imagem JPG, PNG ou WebP.';
} else {
$novoNome = bin2hex(random_bytes(16)) . '.' . $tipos[$tipo];
if (move_uploaded_file($arq['tmp_name'], __DIR__ . '/uploads/' . $novoNome)) {
$mensagem = 'Foto salva como ' . $novoNome;
} else {
$mensagem = 'Não foi possível salvar a foto.';
}
}
}
}
?>
<p><?= htmlspecialchars($mensagem) ?></p>
<form method="post" enctype="multipart/form-data">
<input type="hidden" name="MAX_FILE_SIZE" value="1000000">
<input type="file" name="foto" accept="image/*">
<button>Enviar</button>
</form>
| Trecho | Por quê |
|---|---|
$_FILES['foto'] ?? null | O pedido pode nem ter o campo (ou ter passado do post_max_size). |
!is_int($arq['error']) | Se alguém mandou foto[], o error vira array. Recuse. |
a sequência de elseif com UPLOAD_ERR_* | Uma mensagem clara para cada problema; só segue com UPLOAD_ERR_OK. |
$arq['size'] > 1_000_000 | O seu limite (1 MB), independente do servidor. O _ é só para ler melhor o número. |
$finfo->file(…) + isset($tipos[$tipo]) | Tipo pelo conteúdo, conferido numa lista de permitidos. |
bin2hex(random_bytes(16)) . '.' . $tipos[$tipo] | Nome inventado, extensão vinda da lista. |
__DIR__ . '/uploads/' | __DIR__ é a pasta deste arquivo PHP: o destino não depende de onde o servidor foi aberto. |
Testando cada situação (só a primeira linha da resposta, com a mensagem):
$ curl -s localhost:8000/upload.php -F "foto=@gato.png" | head -1 <p>Foto salva como 1a99be15d05c6e4708f3bc45fd5c9c9b.png</p> $ curl -s localhost:8000/upload.php -F "foto=@falsa.jpg;type=image/jpeg" | head -1 <p>Envie uma imagem JPG, PNG ou WebP.</p> $ curl -s localhost:8000/upload.php -F "foto=@grande.bin" | head -1 <p>A foto é grande demais.</p> $ curl -s localhost:8000/upload.php -F "foto=@/dev/null;filename=" | head -1 <p>Escolha uma foto.</p> $ ls -l uploads total 8 -rw-rw-r-- 1 ana ana 124 out 16 20:59 1a99be15d05c6e4708f3bc45fd5c9c9b.png -rw-rw-r-- 1 ana ana 124 out 16 20:58 gato.png
Sem arquivo de verdade: aqui um campo de texto faz o papel do arquivo. O PHP grava o que você digitar num arquivo temporário e pergunta ao finfo o que ele é, ignorando o nome. Experimente o nome foto.jpg com um texto qualquer, depois com <?php echo 1;, depois com {"nome": "Ana"}.
<?php
$nome = $_POST['nome'] ?? 'foto.jpg';
$conteudo = $_POST['conteudo'] ?? 'Lista: farinha, ovos, leite';
if ($_SERVER['REQUEST_METHOD'] === 'POST') {
$tmp = tempnam(sys_get_temp_dir(), 'up');
file_put_contents($tmp, $conteudo);
$finfo = new finfo(FILEINFO_MIME_TYPE);
echo "<p>O nome diz: <b>" . htmlspecialchars($nome) . "</b><br>";
echo "O finfo viu: <b>" . htmlspecialchars($finfo->file($tmp)) . "</b></p>";
unlink($tmp);
}
?>
<form method="post">
<p>Nome do "arquivo": <input name="nome" value="<?= htmlspecialchars($nome) ?>"></p>
<p>Conteúdo:<br><textarea name="conteudo" rows="4" cols="40"><?= htmlspecialchars($conteudo) ?></textarea></p>
<button>"Enviar"</button>
</form>
Quando dá errado
$_FILES vazio, e o nome do arquivo apareceu em $_POST
$ curl -s localhost:8000/dump.php -d "foto=gato.png"
array(0) {
}
array(1) {
["foto"]=>
string(8) "gato.png"
}
O que aconteceu: o formulário não tem enctype="multipart/form-data". Sem ele, o navegador manda só o nome do arquivo, como texto comum (é o que o curl -d simula aqui), e o conteúdo fica para trás.
Como resolver: <form method="post" enctype="multipart/form-data">. Com method="get" também não funciona.
PHP Request Startup: POST Content-Length of … bytes exceeds the limit
<b>Warning</b>: PHP Request Startup: POST Content-Length of 9000317 bytes exceeds the limit of 8388608 bytes in <b>Unknown</b> on line <b>0</b><br />
O que aconteceu: o pedido inteiro passou do post_max_size (8388608 bytes = 8 MB). O PHP jogou fora $_POST e $_FILES.
Como resolver: aumente post_max_size (e upload_max_filesize) no php.ini, ou avise o limite no formulário. No código, trate "POST com $_FILES vazio" como "arquivo grande demais".
move_uploaded_file(…): Failed to open stream: No such file or directory
O que aconteceu: a pasta de destino não existe (ou o caminho está errado). A mensagem seguinte, Unable to move "/tmp/php…" to "uploads/…", é a consequência.
Como resolver: crie a pasta (mkdir uploads) e use caminho absoluto: __DIR__ . '/uploads/'. Se a mensagem for Permission denied, o PHP não tem permissão de escrita na pasta (no Linux, confira o dono e as permissões da pasta com ls -ld uploads).
O que aconteceu: o código leu $_FILES mas não chamou move_uploaded_file (ou chamou com o destino errado e não conferiu o retorno). O arquivo temporário é apagado quando a página termina.
Como resolver: mova sempre, e confira: if (!move_uploaded_file(…)) { … }.
error 1 com um arquivo de 3 MB
O que aconteceu: o padrão de upload_max_filesize é 2M. Fotos de celular passam disso com facilidade.
Como resolver: aumente no php.ini (e o post_max_size junto), ou reduza a foto antes de enviar. Confira o valor em uso com php -i | grep upload_max_filesize ou ini_get('upload_max_filesize').
Referência
| Item | O que é |
|---|---|
enctype="multipart/form-data" | Obrigatório no <form> para enviar arquivos (com method="post") |
<input type="file" name="x"> | Campo de arquivo; multiple + name="x[]" para vários |
$_FILES['x']['name'] | Nome original (não confiável) |
$_FILES['x']['type'] | Tipo informado pelo navegador (não confiável) |
$_FILES['x']['tmp_name'] | Caminho do arquivo temporário |
$_FILES['x']['error'] | UPLOAD_ERR_OK (0) ou um código de erro |
$_FILES['x']['size'] | Tamanho em bytes |
move_uploaded_file($tmp, $destino) | Move o arquivo enviado; true/false |
(new finfo(FILEINFO_MIME_TYPE))->file($tmp) | Tipo real, pelo conteúdo |
mime_content_type($tmp) | O mesmo, numa função |
bin2hex(random_bytes(16)) | 32 caracteres aleatórios para o nome |
upload_max_filesize / post_max_size | Limites do php.ini (padrão 2M / 8M) |
MAX_FILE_SIZE | Campo escondido, antes do type="file"; gera o erro 2 |
Exercícios
1. O atributo esquecido
Complete o formulário de upload:
2. Deu certo?
Complete com a constante que significa "upload sem erro":
3. Mover
Complete com a função que guarda o arquivo enviado:
4. Qual informação de $_FILES é confiável para saber se o arquivo é mesmo uma imagem?
5. A pessoa enviou um arquivo maior que post_max_size. O que o seu código encontra?
6. Qual é o nome mais seguro para salvar uma foto enviada?
Missão: o finfo já descobriu que o arquivo é image/png. Monte em $novo um nome seguro: 32 caracteres aleatórios (bin2hex(random_bytes(16))), um ponto e a extensão que a lista $tipos dá para esse tipo. Ignore o nome original. A saída deve ser algo como 3f9a…c1.png.
<?php $tipos = ['image/jpeg' => 'jpg', 'image/png' => 'png']; $nomeOriginal = '../../minha foto (1).PHP'; $tipo = 'image/png'; // o que o finfo detectou $novo = $nomeOriginal; // troque por um nome seguro echo $novo, "\n";
<?php $tipos = ['image/jpeg' => 'jpg', 'image/png' => 'png']; $nomeOriginal = '../../minha foto (1).PHP'; $tipo = 'image/png'; // o que o finfo detectou $novo = bin2hex(random_bytes(16)) . '.' . $tipos[$tipo]; echo $novo, "\n";
Missão: abaixo está a ficha que $_FILES['foto'] recebe quando a pessoa clica em Enviar sem escolher arquivo (copiada do var_dump real). Rode: o match cai no default. Acrescente um braço para UPLOAD_ERR_NO_FILE com a mensagem Você não escolheu nenhum arquivo. (o match está em match).
<?php
$arq = ['name' => '', 'full_path' => '', 'type' => '', 'tmp_name' => '', 'error' => 4, 'size' => 0];
$mensagem = match ($arq['error']) {
UPLOAD_ERR_OK => 'Arquivo recebido!',
UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => 'Arquivo grande demais.',
default => 'Erro no envio (código ' . $arq['error'] . ').',
};
echo $mensagem, "\n";
<?php
$arq = ['name' => '', 'full_path' => '', 'type' => '', 'tmp_name' => '', 'error' => 4, 'size' => 0];
$mensagem = match ($arq['error']) {
UPLOAD_ERR_OK => 'Arquivo recebido!',
UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => 'Arquivo grande demais.',
UPLOAD_ERR_NO_FILE => 'Você não escolheu nenhum arquivo.',
default => 'Erro no envio (código ' . $arq['error'] . ').',
};
echo $mensagem, "\n";
Resumo
- Upload precisa de
method="post",enctype="multipart/form-data"e<input type="file" name="…">. $_FILES['campo']trazname,full_path,type,tmp_name,erroresize. Nome e tipo foram escritos por quem enviou.- Confira
error(UPLOAD_ERR_OK= 0) antes de tudo; 4 = nenhum arquivo, 1 e 2 = grande demais. - Limites:
upload_max_filesize(2M) por arquivo;post_max_size(8M) para o pedido inteiro, que, se estourar, esvazia$_POSTe$_FILES. - Descubra o tipo pelo conteúdo, com
finfooumime_content_type, e aceite só uma lista de tipos. - Salve com
move_uploaded_file, com um nome inventado (bin2hex(random_bytes(16))) e a extensão da sua lista.