Adicionar uma galeria de imagens a um editor de texto rico em PHP

A caixa de diálogo Insert Gallery do editor pode mostrar as imagens que já estão no seu servidor, para que as pessoas escolham uma imagem em vez de enviar a mesma de novo. Basta uma URL que responda com JSON. Esta página traz esse endpoint em PHP 8 puro: um script que lê uma pasta sob uploads/ com scandir() e lista as subpastas e as imagens dela.
Ative a galeria
Defina galleryEndpoint ao criar o editor. A caixa de diálogo então pede uma listagem a essa URL toda vez que abre ou muda de pasta.
<link rel="stylesheet" href="/richtexteditor/rte_theme_default.css">
<script src="/richtexteditor/rte.js"></script>
<script src="/richtexteditor/plugins/all_plugins.js"></script>
<script src="/rte-files.js"></script>
<script>
new RichTextEditor("#editor", {
toolbar: "full",
galleryEndpoint: "/rte/gallery",
maxUploadFileSize: 4000000 // same limit as rte/upload.php, checked before sending
});
</script>rte-files.js é o pequeno arquivo de navegador que envia os uploads da caixa de diálogo para /rte/upload. O guia de upload de imagens mostra o lado do servidor.
A rota
GET /rte/gallery?folder=blog retorna a listagem de uma pasta. O script é curto, porque o trabalho está em duas funções auxiliares:
if ($method === 'GET') {
return rte_listing(rte_resolve_folder($_GET['folder'] ?? ''));
}rte_resolve_folder() transforma a pasta virtual da query string em um diretório real dentro da raiz de armazenamento, ou responde 400 ou 404. O guia de pastas explica como. rte_listing() monta o JSON.
O que a galeria recebe
A caixa de diálogo precisa da pasta atual, da pasta pai (null na raiz), das subpastas e das imagens com suas URLs e tamanhos:

Este é o código que a produz:
// The listing for one folder, in the shape the gallery dialog expects.
function rte_listing(array $folder): array
{
$folders = [];
$images = [];
$names = scandir($folder['dir']);
if ($names === false) {
throw new RteHttpError(500, 'The folder cannot be read.');
}
foreach ($names as $name) {
if ($name === '' || $name[0] === '.') {
continue; // ".", "..", and hidden files such as .htaccess
}
$path = $folder['dir'] . DIRECTORY_SEPARATOR . $name;
// Skip symlinks and Windows junctions. is_link() alone is not enough: PHP 8.2 on
// Windows returns false for a junction, so also require realpath() to be unchanged.
$real = realpath($path);
$same = DIRECTORY_SEPARATOR === '\\' ? strcasecmp($real ?: '', $path) === 0 : $real === $path;
if (is_link($path) || !$same) {
continue;
}
$segments = [...$folder['segments'], $name];
if (is_dir($path)) {
if (rte_is_valid_name($name) && !rte_is_reserved($segments)) { // only folders the routes accept back
$folders[] = ['folder' => implode('/', $segments), 'name' => $name];
}
} elseif (is_file($path) && in_array(rte_extension($name), RTE_IMAGE_EXTENSIONS, true)) {
$images[] = [
'url' => rte_url($folder['root'], $segments),
'name' => $name,
'folder' => $folder['virtual'],
'size' => (int)filesize($path),
];
}
}
$byName = fn(array $a, array $b): int => strcasecmp($a['name'], $b['name']) ?: strcmp($a['name'], $b['name']);
usort($folders, $byName);
usort($images, $byName);
$virtual = $folder['virtual'];
$parent = null;
if ($virtual !== '') {
$cut = strrpos($virtual, '/');
$parent = $cut === false ? '' : substr($virtual, 0, $cut);
}
return [
'currentFolder' => $virtual,
'currentFolderDisplay' => '/' . $virtual,
'parentFolder' => $parent,
'folders' => $folders,
'images' => $images,
];
}Vale conhecer algumas escolhas feitas nele:
- Nomes que começam com ponto são ignorados, então
uploads/.htaccessnunca aparece. - Apenas arquivos
.jpg .jpeg .png .gif .webpsão listados. Qualquer outra coisa na pasta fica invisível. - Links simbólicos e junctions do Windows são ignorados. No Windows, o
is_link()do PHP 8.2 retornoufalsepara uma junction no nosso teste, então o código também verifica serealpath()deixa o caminho inalterado. - Cada parte da URL de uma imagem passa por
rawurlencode(), entãomy photo.jpgé listado como/uploads/products/my%20photo.jpg. - Pastas e imagens são ordenadas por nome sem diferenciar maiúsculas e minúsculas, com
strcasecmp().
Formatos que a caixa de diálogo oferece e o servidor aceita
A caixa de diálogo da galeria lançada lista "JPG, JPEG, JFIF, PNG, GIF, WEBP, BMP, AVIF" como formatos aceitos, e o seletor de arquivos dela os oferece. Este servidor aceita cinco deles: .jpg .jpeg .png .gif .webp. Se alguém escolher um arquivo .bmp, a caixa de diálogo o envia, o servidor responde 400 {"error":"Only .jpg, .jpeg, .png, .gif and .webp images can be uploaded."} e a caixa de diálogo mostra "Upload failed for photo.bmp.". O motivo dado pelo servidor está no corpo da resposta, que você pode ver nas ferramentas de desenvolvedor do navegador.
Com o tamanho é diferente: com maxUploadFileSize: 4000000, a caixa de diálogo recusa um arquivo grande antes de enviá-lo. Um arquivo de 4.5 MB resultou em "big.jpg is 4.3 MB. The maximum allowed size is 3.8 MB." (a caixa de diálogo conta em megabytes binários).
Servir as imagens
A listagem aponta para /uploads/..., então essas URLs precisam retornar os arquivos. No projeto de exemplo, router.php os serve para o servidor embutido do PHP. Ele envia apenas extensões de imagem, com um Content-Type fixo e X-Content-Type-Options: nosniff, e nunca executa nada dessa pasta. No Apache ou no nginx, o próprio servidor web os serve diretamente. Mantenha a regra que impede a execução, como mostra o guia de upload seguro.
Teste
Inicie o projeto de exemplo deste guia com php -d upload_max_filesize=5M -d display_errors=0 -d log_errors=1 -S localhost:5201 router.php e peça uma listagem:
curl "http://localhost:5201/rte/gallery?folder=team"
{"currentFolder":"team","currentFolderDisplay":"/team","parentFolder":"","folders":[],"images":[]}
curl "http://localhost:5201/rte/gallery?folder=blog/2027"
{"error":"Folder not found."}A segunda requisição recebe HTTP 404. Abra http://localhost:5201/ e clique em Insert Gallery: a caixa de diálogo mostra as pastas e imagens da raiz, e clicar em uma pasta a abre.
O mesmo guia para outras plataformas
JavaScript · Next.js · ASP.NET Core · Razor Pages · Blazor · MVC 5 · Web Forms