Pastas de imagens por usuário em PHP

A pasta uploads no servidor de exemplo em PHP, com pastas compartilhadas e uma pasta users contendo alice e bob, cada um com as próprias imagens

Em um site onde cada pessoa escreve o próprio conteúdo, a galeria deve mostrar a cada pessoa as próprias imagens, e não as de todo mundo. Com o exemplo de PHP, isso é uma mudança em uma única função: a raiz de armazenamento é escolhida a cada requisição, a partir do usuário autenticado em $_SESSION. Esta página mostra essa função, como ativá-la e o que ela protege e o que não protege.

Uma função decide para onde vão os arquivos

Toda rota do exemplo (listagem, criação de pasta, upload) obtém a raiz de armazenamento de rte_root_for_request(). Por padrão, ela retorna a pasta compartilhada uploads/. Com RTE_PER_USER=1 no ambiente, ela retorna uploads/users/<id>/ para o usuário autenticado:

PHP · rte/files-lib.php
// The one place that decides WHERE files live.
// Returns ['dir' => folder on disk, 'url' => public URL of that folder].
// Default: one shared folder. With RTE_PER_USER=1: uploads/users/<id>/ per signed-in user.
function rte_root_for_request(): array
{
    if (getenv('RTE_PER_USER') !== '1') {
        return ['dir' => __DIR__ . '/../uploads', 'url' => '/uploads'];
    }
    $id = rte_current_user_id();
    return ['dir' => __DIR__ . '/../uploads/users/' . $id, 'url' => '/uploads/users/' . $id];
}

// In shared mode, uploads/users/ holds other people's folders: never list or open it.
function rte_is_reserved(array $segments): bool
{
    return getenv('RTE_PER_USER') !== '1' && isset($segments[0]) && strcasecmp($segments[0], 'users') === 0;
}

// The signed-in user's id, from YOUR login (here: the PHP session). Never from the request.
function rte_current_user_id(): string
{
    if (session_status() !== PHP_SESSION_ACTIVE) {
        session_start();
    }
    $id = (string)($_SESSION['user_id'] ?? '');
    // PHP locks the session file until the request ends; release it so the
    // gallery's requests do not wait for each other.
    session_write_close();
    // The id becomes a folder name, so it must be a plain token.
    if (!preg_match('/\A[A-Za-z0-9_-]{1,64}\z/', $id)) {
        throw new RteHttpError(401, 'Sign in to use the image gallery.');
    }
    return $id;
}

Como as verificações de pasta, a listagem e move_uploaded_file() trabalham todos dentro dessa raiz, nada mais muda. A pasta drafts de um usuário é uploads/users/alice/drafts no disco, mas a galeria a chama de drafts, e a URL que ela recebe de volta é /uploads/users/alice/drafts/....

De onde vem o id do usuário

O id precisa vir do seu próprio login, nunca da requisição. No exemplo, ele é lido de $_SESSION['user_id']. Também se verifica que ele é um token simples antes de virar nome de pasta. Se os seus ids forem endereços de e-mail, use no lugar o id numérico do seu banco de dados.

A chamada a session_write_close() está ali por causa do funcionamento do PHP. Com as sessões em arquivo padrão, o PHP mantém a sessão bloqueada até o fim da requisição, então as requisições da galeria vindas do mesmo navegador esperariam umas pelas outras. A raiz só precisa do id, então a sessão é liberada logo depois de lê-lo.

Sem uma sessão válida, toda rota responde 401 {"error":"Sign in to use the image gallery."}. A caixa de diálogo então mostra "The image gallery is unavailable right now (http-401)." ao abrir.

Testar sem um sistema de login

O projeto de exemplo inclui demo-login.php para você testar dois usuários. Ele define o mesmo valor de sessão que o seu código de login definiria:

PHP · demo-login.php
session_start();
session_regenerate_id(true);           // new session id at every sign-in
$user = $_GET['user'] ?? '';
if (in_array($user, ['alice', 'bob'], true)) {
    $_SESSION['user_id'] = $user;      // what rte_current_user_id() reads
} else {
    unset($_SESSION['user_id']);
}

Ele só funciona no servidor embutido do PHP (verifica PHP_SAPI), e as rotas do guia não dependem dele. Exclua-o e defina $_SESSION['user_id'] no seu próprio código de login, depois da verificação de senha, junto com session_regenerate_id(true).

Modo compartilhado e modo por usuário no mesmo disco

As pastas por usuário ficam sob uploads/users/. Se você algum dia rodar o modo compartilhado na mesma pasta, isso tornaria as pastas de todos visíveis na galeria compartilhada. Por isso, no modo compartilhado, o exemplo esconde users da listagem da raiz, responde 404 para ?folder=users/alice e se recusa a criar uma pasta chamada users. A imagem no topo mostra os dois no mesmo disco: as pastas compartilhadas blog, products e team, e users/alice e users/bob do modo por usuário.

O que isso não protege

As imagens continuam sendo servidas como arquivos estáticos a partir de /uploads/. Qualquer pessoa que tenha a URL da imagem do Bob pode abri-la, autenticada ou não. A parte aleatória do nome armazenado torna as URLs difíceis de adivinhar, mas não as torna secretas: elas acabam no HTML de todo documento que mostra a imagem. Se as imagens precisarem ser privadas, sirva-as por um script PHP que verifique a sessão e mantenha uploads/ fora da raiz de documentos.

Teste

Inicie o projeto de exemplo deste guia no modo por usuário. No PowerShell, defina antes $env:RTE_PER_USER=1.

Shell
RTE_PER_USER=1 php -d upload_max_filesize=5M -d display_errors=0 -d log_errors=1 -S localhost:5201 router.php

Depois faça login como dois usuários, com arquivos de cookie separados, e envie uma imagem com cada um:

Shell
curl "http://localhost:5201/rte/gallery?folder="
{"error":"Sign in to use the image gallery."}

curl -c alice.txt -b alice.txt "http://localhost:5201/demo-login?user=alice"
curl -c bob.txt -b bob.txt "http://localhost:5201/demo-login?user=bob"

curl -b alice.txt -F "file=@harbor.jpg" http://localhost:5201/rte/upload
{"url":"/uploads/users/alice/harbor-a936755b.jpg"}
curl -b bob.txt -F "file=@meadow.jpg" http://localhost:5201/rte/upload
{"url":"/uploads/users/bob/meadow-94064337.jpg"}

curl -b bob.txt "http://localhost:5201/rte/gallery?folder=..%2Falice"
{"error":"Invalid folder."}

A listagem do Bob contém apenas meadow-94064337.jpg. Uma pasta da Alice que o Bob pede pelo nome (?folder=drafts) é um 404 para ele, e um cookie de sessão que ele mesmo inventa recebe o 401. No navegador, abra /demo-login?user=alice e depois Insert Gallery para ver a pasta da Alice.

O mesmo guia para outras plataformas

JavaScript · ASP.NET Core · MVC 5 · Web Forms

Leia a seguir