Carpetas de imágenes por usuario en PHP

En un sitio donde cada persona escribe su propio contenido, la galería debería mostrar a cada persona sus propias imágenes, no las de todos. Con el ejemplo de PHP, eso es un cambio en una sola función: la raíz de almacenamiento se elige en cada solicitud a partir del usuario con sesión iniciada en $_SESSION. Esta página muestra esa función, cómo activarla y qué protege y qué no.
Una función decide dónde van los archivos
Todas las rutas del ejemplo (listado, creación de carpetas, carga) obtienen su raíz de almacenamiento de rte_root_for_request(). De forma predeterminada, devuelve la carpeta compartida uploads/. Con RTE_PER_USER=1 en el entorno, devuelve uploads/users/<id>/ para el usuario con sesión iniciada:
// 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 las comprobaciones de carpetas, el listado y move_uploaded_file() trabajan dentro de esa raíz, no cambia nada más. La carpeta drafts de un usuario es uploads/users/alice/drafts en el disco, pero la galería la llama drafts, y la URL que recibe es /uploads/users/alice/drafts/....
De dónde sale el id del usuario
El id debe venir de tu propio inicio de sesión, nunca de la solicitud. En el ejemplo se lee de $_SESSION['user_id']. Además, se comprueba que sea un token simple antes de convertirlo en nombre de carpeta. Si tus ids son direcciones de correo electrónico, usa en su lugar el id numérico de tu base de datos.
La llamada a session_write_close() está ahí por cómo funciona PHP. Con las sesiones en archivos predeterminadas, PHP mantiene la sesión bloqueada hasta que termina la solicitud, así que las solicitudes de la galería desde el mismo navegador se esperarían unas a otras. La raíz solo necesita el id, así que la sesión se libera justo después de leerlo.
Sin una sesión válida, todas las rutas responden 401 {"error":"Sign in to use the image gallery."}. El cuadro de diálogo muestra entonces "The image gallery is unavailable right now (http-401)." al abrirse.
Probarlo sin un sistema de inicio de sesión
El proyecto de ejemplo incluye demo-login.php para que puedas probar con dos usuarios. Establece el mismo valor de sesión que establecería tu código de inicio de sesión:
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']);
}Solo funciona con el servidor integrado de PHP (comprueba PHP_SAPI), y las rutas de la guía no dependen de él. Bórralo y establece $_SESSION['user_id'] en tu propio código de inicio de sesión, después de comprobar la contraseña, junto con session_regenerate_id(true).
Modo compartido y modo por usuario en un mismo disco
Las carpetas por usuario están dentro de uploads/users/. Si alguna vez ejecutas el modo compartido sobre la misma carpeta, las carpetas de todos serían visibles en la galería compartida. Por eso, en modo compartido, el ejemplo oculta users en el listado de la raíz, responde 404 a ?folder=users/alice y se niega a crear una carpeta llamada users. La imagen del principio muestra ambos en un mismo disco: las carpetas compartidas blog, products y team, y users/alice y users/bob del modo por usuario.
Qué no protege
Las imágenes se siguen sirviendo como archivos estáticos desde /uploads/. Cualquiera que tenga la URL de la imagen de Bob puede abrirla, con o sin sesión iniciada. La parte aleatoria del nombre guardado hace que las URL sean difíciles de adivinar, pero no las vuelve secretas: acaban en el HTML de cada documento que muestra la imagen. Si las imágenes deben ser privadas, sírvelas a través de un script PHP que compruebe la sesión y deja uploads/ fuera de la raíz de documentos.
Pruébalo
Inicia el proyecto de ejemplo de esta guía en modo por usuario. En PowerShell, establece primero $env:RTE_PER_USER=1.
RTE_PER_USER=1 php -d upload_max_filesize=5M -d display_errors=0 -d log_errors=1 -S localhost:5201 router.phpDespués inicia sesión como dos usuarios con archivos de cookies separados y sube una imagen con cada uno:
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."}El listado de Bob solo contiene meadow-94064337.jpg. Una carpeta de Alice que Bob pide por su nombre (?folder=drafts) es un 404 para él, y una cookie de sesión que se invente recibe el 401. En el navegador, abre /demo-login?user=alice y luego Insert Gallery para ver la carpeta de Alice.
La misma guía para otras plataformas
JavaScript · ASP.NET Core · MVC 5 · Web Forms