Dossiers d’images par utilisateur en PHP

Le dossier uploads sur le serveur de l’exemple PHP, avec des dossiers partagés et un dossier users contenant alice et bob, chacun avec ses propres images

Sur un site où chaque personne rédige son propre contenu, la galerie doit montrer à chacune ses propres images, et non celles de tout le monde. Avec l’exemple PHP, il suffit de modifier une seule fonction : la racine de stockage est choisie à chaque requête, d’après l’utilisateur connecté enregistré dans $_SESSION. Cette page présente cette fonction, la façon de l’activer, et ce qu’elle protège ou ne protège pas.

Une seule fonction décide où vont les fichiers

Chaque route de l’exemple (liste, création de dossier, envoi) obtient sa racine de stockage de rte_root_for_request(). Par défaut, elle renvoie le dossier partagé uploads/. Avec RTE_PER_USER=1 dans l’environnement, elle renvoie uploads/users/<id>/ pour l’utilisateur connecté :

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;
}

Comme les vérifications de dossier, la liste et move_uploaded_file() travaillent toutes à l’intérieur de cette racine, rien d’autre ne change. Le dossier drafts d’un utilisateur est uploads/users/alice/drafts sur le disque, mais la galerie l’appelle drafts, et l’URL qu’elle reçoit en retour est /uploads/users/alice/drafts/....

D’où vient l’identifiant de l’utilisateur

L’identifiant doit provenir de votre propre système de connexion, jamais de la requête. Dans l’exemple, il est lu dans $_SESSION['user_id']. On vérifie aussi qu’il s’agit d’un simple jeton avant qu’il ne devienne un nom de dossier. Si vos identifiants sont des adresses e-mail, utilisez plutôt votre identifiant numérique en base de données.

L’appel à session_write_close() s’explique par le fonctionnement de PHP. Avec les sessions par fichiers par défaut, PHP garde la session verrouillée jusqu’à la fin de la requête : les requêtes de la galerie venant du même navigateur s’attendraient donc les unes les autres. La racine n’a besoin que de l’identifiant : la session est donc libérée juste après sa lecture.

Sans session valide, chaque route répond 401 {"error":"Sign in to use the image gallery."}. La boîte de dialogue affiche alors « The image gallery is unavailable right now (http-401). » à son ouverture.

Essayer sans système de connexion

Le projet d’exemple inclut demo-login.php pour vous permettre d’essayer deux utilisateurs. Il définit la même valeur de session que votre code de connexion définirait :

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']);
}

Il ne fonctionne qu’avec le serveur intégré de PHP (il vérifie PHP_SAPI), et les routes du guide n’en dépendent pas. Supprimez-le et définissez $_SESSION['user_id'] dans votre propre code de connexion, après la vérification du mot de passe, en appelant aussi session_regenerate_id(true).

Mode partagé et mode par utilisateur sur un même disque

Les dossiers par utilisateur se trouvent sous uploads/users/. Si vous utilisiez un jour le mode partagé sur ce même dossier, les dossiers de tout le monde deviendraient visibles dans la galerie partagée. En mode partagé, l’exemple masque donc users dans la liste de la racine, répond 404 pour ?folder=users/alice et refuse de créer un dossier nommé users. L’image en haut de la page montre les deux sur un même disque : les dossiers partagés blog, products et team, et users/alice et users/bob issus du mode par utilisateur.

Ce que cela ne protège pas

Les images restent servies en fichiers statiques depuis /uploads/. Toute personne qui a l’URL de l’image de Bob peut l’ouvrir, connectée ou non. La partie aléatoire du nom stocké rend les URL difficiles à deviner, mais ne les rend pas secrètes : elles se retrouvent dans le HTML de chaque document qui affiche l’image. Si les images doivent être privées, servez-les via un script PHP qui vérifie la session, et placez uploads/ en dehors de la racine des documents.

Tester

Démarrez le projet d’exemple de ce guide en mode par utilisateur. Dans PowerShell, définissez plutôt $env:RTE_PER_USER=1 au préalable.

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

Connectez-vous ensuite en tant que deux utilisateurs, avec des fichiers de cookies distincts, et envoyez une image chacun :

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."}

La liste de Bob ne contient que meadow-94064337.jpg. Un dossier d’Alice que Bob demande par son nom (?folder=drafts) lui renvoie un 404, et un cookie de session qu’il fabrique lui-même reçoit le 401. Dans le navigateur, ouvrez /demo-login?user=alice puis Insert Gallery pour voir le dossier d’Alice.

Le même guide pour d’autres plateformes

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

À lire ensuite