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

A caixa de diálogo Insert Gallery mostrando as pastas blog, products e team e quatro imagens da pasta uploads do exemplo em PHP, uma delas selecionada

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.

HTML · index.php
<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:

PHP · rte/gallery.php
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:

A resposta JSON de GET /rte/gallery?folder=blog no exemplo de PHP: currentFolder blog, parentFolder vazio, nenhuma subpasta e imagens com url, name, folder e size
A resposta real do exemplo, reduzida a duas imagens.

Este é o código que a produz:

PHP · rte/files-lib.php
// 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/.htaccess nunca aparece.
  • Apenas arquivos .jpg .jpeg .png .gif .webp sã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 retornou false para uma junction no nosso teste, então o código também verifica se realpath() deixa o caminho inalterado.
  • Cada parte da URL de uma imagem passa por rawurlencode(), então my 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:

Shell
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

Leia a seguir