PHP でのユーザーごとの画像フォルダー

PHP のサンプルサーバーの uploads フォルダー。共有フォルダーと、alice と bob を含む users フォルダーがあり、それぞれに専用の画像がある

各ユーザーがそれぞれ自分のコンテンツを書くサイトでは、ギャラリーには全員の画像ではなく、そのユーザー自身の画像だけを表示すべきです。PHP のサンプルでは、これは 1 つの関数の変更で実現できます。ストレージルートを、$_SESSION にあるサインイン中のユーザーをもとに、リクエストごとに決めるのです。このページでは、その関数、それを有効にする方法、そしてこれで保護できるものとできないものを紹介します。

ファイルの保存先を決める 1 つの関数

サンプルのすべてのルート(一覧の表示、フォルダーの作成、アップロード)は、rte_root_for_request() からストレージルートを取得します。既定では、共有の uploads/ フォルダーを返します。環境変数で RTE_PER_USER=1 を設定すると、サインイン中のユーザーの uploads/users/<id>/ を返します。

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

フォルダーのチェック、一覧の作成、move_uploaded_file() はすべてそのルートの中で動作するので、ほかには何も変わりません。ユーザーの drafts フォルダーはディスク上では uploads/users/alice/drafts ですが、ギャラリーでは drafts と表示され、返される URL は /uploads/users/alice/drafts/... になります。

ユーザー ID の取得元

ID は必ず自分のログイン処理から取得し、リクエストから取得してはいけません。サンプルでは $_SESSION['user_id'] から読み取ります。また、フォルダー名として使う前に、単純なトークンであることをチェックします。ID がメールアドレスの場合は、代わりにデータベースの数値の ID を使ってください。

session_write_close() を呼び出しているのは、PHP の仕組みのためです。既定のファイルセッションでは、PHP はリクエストが終わるまでセッションをロックしたままにするので、同じブラウザーからのギャラリーのリクエストが互いに待たされてしまいます。ルートを決めるには ID さえあればよいので、読み取ったらすぐにセッションを解放します。

有効なセッションがない場合、すべてのルートが 401 {"error":"Sign in to use the image gallery."} を返します。その場合、ダイアログは開いたときに「The image gallery is unavailable right now (http-401).」と表示します。

ログインの仕組みなしで試す

サンプルプロジェクトには、2 人のユーザーを試せるように demo-login.php が含まれています。これは、ログイン処理が設定するのと同じセッションの値を設定します。

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

これは PHP の組み込みサーバーでしか動作せず(PHP_SAPI をチェックしています)、ガイドのルートはこれに依存していません。このファイルは削除し、自分のサインイン処理の中で、パスワードのチェックの後に session_regenerate_id(true) とあわせて $_SESSION['user_id'] を設定してください。

共有モードとユーザーごとのモードを同じディスクで使う

ユーザーごとのフォルダーは uploads/users/ 以下にあります。同じフォルダーで共有モードを実行すると、全員のフォルダーが共有ギャラリーに表示されてしまいます。そのため、共有モードのサンプルは、ルートの一覧から users を隠し、?folder=users/alice には 404 を返し、users という名前のフォルダーの作成を拒否します。ページ上部の画像は、両方を同じディスクに置いた状態です。共有の blog、products、team フォルダーと、ユーザーごとのモードで作られた users/alice、users/bob があります。

これで保護できないもの

画像は依然として /uploads/ から静的ファイルとして配信されます。Bob の画像の URL を知っていれば、サインインしているかどうかにかかわらず、誰でも開くことができます。保存名のランダムな部分によって URL は推測しにくくなりますが、秘密にはなりません。その画像を表示するすべての文書の HTML に URL が含まれるからです。画像を非公開にする必要がある場合は、セッションをチェックする PHP スクリプトを通して配信し、uploads/ はドキュメントルートの外に置いてください。

テストする

このガイドのサンプルプロジェクトをユーザーごとのモードで起動します。PowerShell では、代わりに先に $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

次に、別々の Cookie ファイルを使って 2 人のユーザーとしてサインインし、それぞれ画像を 1 つずつアップロードします。

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

Bob の一覧には meadow-94064337.jpg だけが含まれます。Bob が Alice のフォルダーを名前で指定しても(?folder=drafts)404 になり、自分ででっち上げたセッション Cookie では 401 になります。ブラウザーで /demo-login?user=alice を開いてから「Insert Gallery」を開くと、Alice のフォルダーが表示されます。

ほかのプラットフォーム向けの同じガイド

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

次に読む