Programação Por Equipe DevBras

Capa do artigo: Galeria de fotos em PHP que lê uma tabela, não a pasta inteira

Galeria de fotos em PHP que lê uma tabela, não a pasta inteira

Troque a galeria que lista o diretório por uma que lê uma tabela: arquivos fora da pasta pública, nomes gerados, ordem controlada e exclusão que apaga registro e arquivo.

A galeria do site do buffet era "super simples": um arquivo PHP abria a pasta fotos/ e mostrava tudo que encontrava. Funcionou por três anos. Um dia alguém subiu ali backup-site.zip para baixar em casa, e o zip apareceu como item da galeria, com link clicável, para qualquer visitante. Junto, uma foto de um evento que o cliente tinha pedido para tirar do ar.

A resposta curta: a galeria mostra o que você cadastrou numa tabela, não o que está no diretório. Cada foto tem um registro com álbum, legenda, ordem e se está publicada. Os arquivos ficam fora da pasta pública, com nome gerado pelo sistema, e são entregues por um script que consulta a tabela. Apagar da galeria apaga o registro e o arquivo, e a listagem automática de diretório fica desligada no servidor.

Por que listar a pasta dá errado

Funções como scandir ou glob devolvem tudo que está no disco, sem saber o que é vitrine e o que é sobra. Arquivo de teste, cópia antiga, foto que deveria estar oculta, miniatura gerada por outro plugin, arquivo que o editor de imagens deixou para trás: tudo vira item. E, como a pasta está dentro da raiz do site, qualquer arquivo nela pode ser baixado diretamente por quem adivinhar o nome.

Há também problemas menos dramáticos. A ordem é a alfabética do disco, não a que o cliente quer. Não há onde guardar legenda, texto alternativo ou data do evento. Esconder uma foto exige renomear ou mover arquivo pelo FTP. Se você ainda precisa listar arquivos de uma pasta para outro fim, os cuidados estão em listar arquivos por data em PHP; para a vitrine, a resposta é outra.

Como montar a galeria a partir de uma tabela

  1. Crie a tabela com álbum, dono, nome do arquivo gerado, legenda, ordem e indicador de publicação.
  2. Mova os arquivos para fora da raiz pública, por exemplo /srv/galeria/original e /srv/galeria/mini.
  3. No upload, gere o nome com random_bytes, regrave a imagem e crie a miniatura. O nome original vira, no máximo, legenda inicial.
  4. Escreva a página da galeria lendo só registros publicados, na ordem da coluna.
  5. Escreva o script que entrega a imagem, consultando a tabela pelo id.
  6. Desligue a listagem de diretórios no servidor e apague a pasta antiga depois de migrar.
CREATE TABLE fotos (
  id         INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
  album_id   INT UNSIGNED NOT NULL,
  dono_id    INT UNSIGNED NOT NULL,
  arquivo    CHAR(32)     NOT NULL,
  legenda    VARCHAR(200) NOT NULL DEFAULT '',
  ordem      INT          NOT NULL DEFAULT 0,
  publicada  TINYINT(1)   NOT NULL DEFAULT 0,
  criada_em  DATETIME     NOT NULL DEFAULT CURRENT_TIMESTAMP,
  KEY ix_album (album_id, publicada, ordem)
) DEFAULT CHARSET=utf8mb4;

O campo arquivo guarda 32 caracteres hexadecimais, resultado de bin2hex(random_bytes(16)). Como a imagem é regravada como JPEG no upload, a extensão é sempre a mesma e não depende do que a pessoa enviou. Os detalhes de receber e regravar estão em upload de arquivo em PHP e arquivo enviado vira imagem de novo.

A página da galeria

<?php
// conexao.php devolve um PDO configurado com ERRMODE_EXCEPTION e utf8mb4
$pdo = require __DIR__ . '/conexao.php';

$albumId = (int) ($_GET['album'] ?? 0);
$stmt = $pdo->prepare(
    'SELECT id, legenda FROM fotos
     WHERE album_id = ? AND publicada = 1
     ORDER BY ordem, id'
);
$stmt->execute([$albumId]);
?>
<ul>
<?php foreach ($stmt as $foto): ?>
  <li>
    <a href="/foto.php?id=<?= (int) $foto['id'] ?>">
      <img src="/foto.php?mini=<?= (int) $foto['id'] ?>"
           alt="<?= htmlspecialchars($foto['legenda'], ENT_QUOTES, 'UTF-8') ?>">
    </a>
  </li>
<?php endforeach; ?>
</ul>

O script que entrega a imagem

<?php
$pdo = require __DIR__ . '/conexao.php';

$mini = isset($_GET['mini']);
$id = (int) ($mini ? $_GET['mini'] : ($_GET['id'] ?? 0));

$stmt = $pdo->prepare('SELECT arquivo FROM fotos WHERE id = ? AND publicada = 1');
$stmt->execute([$id]);
$arquivo = $stmt->fetchColumn();

if ($arquivo === false) {
    http_response_code(404);
    exit;
}

$caminho = '/srv/galeria/' . ($mini ? 'mini/' : 'original/') . $arquivo . '.jpg';
if (!is_file($caminho)) {
    http_response_code(404);
    exit;
}

header('Content-Type: image/jpeg');
header('Content-Length: ' . filesize($caminho));
header('Cache-Control: public, max-age=86400');
readfile($caminho);

O caminho no disco é montado só com dados da tabela, nunca com texto vindo da URL. Por isso não há como pedir ../../config.php: o id vira inteiro, e o nome do arquivo sai do banco. Uma foto despublicada devolve 404 na hora, mesmo que alguém tenha guardado o link. Em álbuns privados, acrescente a verificação de quem está logado antes do readfile.

Em sites com muito tráfego, entregar imagem pelo PHP consome processos. Servidores como o Nginx permitem que o PHP só autorize e o próprio servidor envie o arquivo, com o cabeçalho X-Accel-Redirect. Comece com readfile e troque quando a medição mostrar que vale.

Desligar a listagem de diretórios

Mesmo depois da migração, garanta que nenhuma pasta do site mostre o índice de arquivos quando não há index.php. No Apache, isso é Options -Indexes na configuração do site ou no .htaccess, quando permitido. No Nginx, a listagem só aparece se alguém ativou autoindex on. O comportamento do Apache ao negar acesso está em erro 403 no Apache.

<Directory /var/www/buffet>
    Options -Indexes
</Directory>

Apagar é apagar os dois

Excluir uma foto da galeria tem duas partes: remover o registro e apagar os arquivos original e miniatura. Faça a exclusão do registro primeiro, para que a foto saia da vitrine imediatamente, e depois apague os arquivos com unlink. Se o unlink falhar, registre o nome num log para limpeza posterior. Só esconder a linha deixa o arquivo acessível a quem guardou o link e ocupa disco para sempre. Se o cliente pediu remoção por privacidade, o arquivo precisa sumir de verdade, incluindo dos backups no ciclo de retenção.

Erros comuns

  • Galeria que lista a pasta: qualquer arquivo largado ali, de zip de backup a foto removida, vira item público.
  • Arquivos dentro da raiz do site: quem adivinha ou guarda o nome baixa direto, sem passar por nenhuma regra.
  • Nome original do arquivo no disco: nomes com acento, espaço ou repetidos quebram links e sobrescrevem fotos de outros eventos.
  • Caminho montado com texto da URL: abre espaço para pedir arquivos fora da pasta com ../.
  • Esconder em vez de apagar: a foto continua acessível pelo link antigo e o pedido de remoção não foi cumprido.
  • Listagem de diretório ativa: o servidor mostra o conteúdo inteiro de qualquer pasta sem índice.

Checklist

  • A página da galeria consulta uma tabela, não o diretório.
  • Os arquivos de imagem ficam fora da raiz pública do site.
  • O nome no disco é gerado pelo sistema e não vem do upload.
  • O script de entrega recebe só um id inteiro e devolve 404 para foto despublicada.
  • A ordem das fotos é uma coluna editável.
  • A listagem automática de diretórios está desligada.
  • Excluir uma foto remove registro, original e miniatura.

Pegue o nome de uma foto que você removeu da galeria no mês passado e tente abri-la direto no navegador. Ela ainda carrega? Se carrega, a pasta continua sendo a galeria de verdade.

Avalie este artigo

Referência: http://www.toprated.com.br/galeria-de-fotos-super-simples-usando-apenas-1-arquivo-php