<?php
/**
 * tegalog-auth.php — てがろぐ拡張スクリプト共通・ログイン連携ライブラリ(単一ファイル配布用)
 *
 * てがろぐ本体(index.cgi / tegalog.cgi)は一切改造せず、psif.cgi・tegalog.iniを
 * 読み取り専用で参照して「このリクエストはてがろぐにログイン中か」「権限レベルはいくつか」を
 * 判定するための共通部品。複数の拡張スクリプトから使い回すことを想定している。
 *
 * ---- 配置場所 ----
 * てがろぐ本体(tegalog.cgi)と同じディレクトリに置く。
 *   root/
 *     tegalog.cgi
 *     tegalog-auth.php   ← このファイル
 *     ある拡張フォルダ/
 *       スクリプト.php   ← ここから require_once __DIR__ . '/../tegalog-auth.php'; する
 *
 * ---- 使い方の例 ----
 * (二重読み込み対策として自己防衛はしているが、require_once を使うのが基本)
 *   require_once __DIR__ . '/../tegalog-auth.php';
 *   $user = tgauth_currentUser();
 *   if ($user === null) {
 *       http_response_code(403);
 *       exit('ログインしてください。');
 *   }
 *   $lv = tgauth_userLevel($user);
 *
 * CSRF対策が必要な管理画面等では、拡張スクリプト側が持つ秘密文字列(salt等)と
 * 組み合わせてトークンを発行・検証できる:
 *   $token = tgauth_csrfToken($mySecret, $user);
 *   ...
 *   if (!tgauth_verifyCsrfToken($mySecret, $user, $_POST['csrf'] ?? null)) { ... }
 */

declare(strict_types=1);

// 直接アクセスされても実害はない(関数定義のみで何も出力しない)が、念のため空応答で終わらせる。
if (basename($_SERVER['SCRIPT_FILENAME'] ?? '') === basename(__FILE__)) {
    http_response_code(404);
    exit;
}

if (!defined('TGAUTH_ROOT')) {
    // このファイル自身の場所 = てがろぐ本体の設置ディレクトリ、という前提。
    define('TGAUTH_ROOT', __DIR__);
}

// 二重require対策: PHPは無条件のトップレベル関数定義をコンパイル時に確定させるため、
// 単なる実行時チェック(function_exists → return)だけでは「require を2回呼ぶと
// Fatal Error」を防げない。関数定義そのものをこの条件ブロックで包むことで、
// 実行時に「まだ定義されていなければ定義する」という本来の意味でのガードにしている。
// (呼び出し側が require_once ではなく require を使っていても、複数の経路から
// 読み込まれても安全に動作する。ただし基本は require_once を使うこと。)
if (!function_exists('tgauth_currentUser')) {

    // key=value 形式のiniファイルを読む(てがろぐの tegalog.ini と同形式)。
    // 見つからない/読めない場合は空配列を返す(呼び出し側で例外処理を書かずに済むように)。
    function tgauth_loadIni(string $path): array
    {
        $cfg = [];
        $lines = @file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        if ($lines === false) {
            return $cfg;
        }
        foreach ($lines as $line) {
            if ($line === '' || $line[0] === '#') {
                continue;
            }
            $pos = strpos($line, '=');
            if ($pos === false) {
                continue;
            }
            $cfg[substr($line, 0, $pos)] = substr($line, $pos + 1);
        }
        return $cfg;
    }

    // tegalog.ini の内容をリクエスト内で使い回すための共通キャッシュ。
    // tgauth_cookieName/tgauth_userDetail/tgauth_allUsers/tgauth_siteConfig が
    // それぞれ個別にファイルを読み直さずに済むよう、ここに集約する。
    function tgauth_tegalogIni(): array
    {
        static $cached = null;
        if ($cached === null) {
            $cached = tgauth_loadIni(TGAUTH_ROOT . '/tegalog.ini');
        }
        return $cached;
    }

    // てがろぐのログインセッションCookie名を判定する(coexistflag/coexistsuffix対応)。
    function tgauth_cookieName(): string
    {
        static $cached = null;
        if ($cached !== null) {
            return $cached;
        }
        $ini = tgauth_tegalogIni();
        $name = 'fomlid';
        if (($ini['coexistflag'] ?? '') === '1') {
            $name .= $ini['coexistsuffix'] ?? '';
        }
        return $cached = $name;
    }

    // 現在のリクエストがてがろぐにログイン中かどうかを判定し、ログイン中ならユーザーIDを返す。
    // 未ログイン、またはpsif.cgi/tegalog.iniが読めない場合はnullを返す。
    // 読み取り専用の照合のみ行い、psif.cgi側のセッション有効期限延長(スライディング)などの
    // 書き込みは行わない(そこはてがろぐ本体だけの役目とする)。
    function tgauth_currentUser(): ?string
    {
        $psifPath = TGAUTH_ROOT . '/psif.cgi';
        if (!is_file($psifPath)) {
            return null;
        }

        $sessionId = $_COOKIE[tgauth_cookieName()] ?? '';
        if ($sessionId === '') {
            return null;
        }

        $lines = @file($psifPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
        if ($lines === false) {
            return null;
        }
        foreach ($lines as $line) {
            if (strpos($line, 'SESSION=') !== 0) {
                continue;
            }
            $parts = explode(',', substr($line, strlen('SESSION=')), 3);
            [$limit, $sid, $userid] = [$parts[0] ?? '', $parts[1] ?? '', $parts[2] ?? ''];
            if ($sid !== '' && hash_equals($sid, $sessionId) && (int)$limit >= time()) {
                return $userid;
            }
        }
        return null;
    }

    function tgauth_isLoggedIn(): bool
    {
        return tgauth_currentUser() !== null;
    }

    // tegalog.ini の userids (例: "admin<>9<>名無し<>プロフィール<>アイコン<,>user2<>3<>...") から
    // 指定ユーザーの詳細を取得する。見つからなければnull。
    // 戻り値: ['id'=>string, 'level'=>int, 'name'=>string, 'intro'=>string, 'icon'=>string]
    function tgauth_userDetail(string $userid): ?array
    {
        if ($userid === '') {
            return null;
        }
        foreach (tgauth_allUsers() as $user) {
            if ($user['id'] === $userid) {
                return $user;
            }
        }
        return null;
    }

    // 指定ユーザーの権限レベル(userlv、1〜9)を取得する。見つからなければ0。
    function tgauth_userLevel(string $userid): int
    {
        return tgauth_userDetail($userid)['level'] ?? 0;
    }

    // 現在ログイン中のユーザーの詳細を取得する(tgauth_currentUser + tgauth_userDetail のショートカット)。
    // 未ログインならnull。
    function tgauth_currentUserDetail(): ?array
    {
        $user = tgauth_currentUser();
        return $user !== null ? tgauth_userDetail($user) : null;
    }

    // 現在ログイン中のユーザーの権限レベルが指定値以上かどうか(未ログインなら常にfalse)。
    // 管理者権限が必要な処理のガードに使う想定。
    function tgauth_isAdmin(int $level = 9): bool
    {
        $user = tgauth_currentUser();
        return $user !== null && tgauth_userLevel($user) >= $level;
    }

    // tegalog.ini の userids に登録されている全ユーザーの一覧を返す。
    // 戻り値: [ ['id'=>string,'level'=>int,'name'=>string,'intro'=>string,'icon'=>string], ... ]
    function tgauth_allUsers(): array
    {
        $raw = tgauth_tegalogIni()['userids'] ?? '';
        if ($raw === '') {
            return [];
        }
        $users = [];
        foreach (explode('<,>', $raw) as $record) {
            $fields = explode('<>', $record);
            if (($fields[0] ?? '') === '') {
                continue;
            }
            $users[] = [
                'id'    => $fields[0] ?? '',
                'level' => (int)($fields[1] ?? 0),
                'name'  => $fields[2] ?? '',
                'intro' => $fields[3] ?? '',
                'icon'  => $fields[4] ?? '',
            ];
        }
        return $users;
    }

    // tegalog.ini の任意のキーを取得する汎用アクセサ(サイトタイトルや文字コード設定等、
    // ログイン連携に限らずてがろぐ本体の設定を参照したい拡張スクリプト向け)。
    // 見つからなければ $default を返す。
    function tgauth_siteConfig(string $key, ?string $default = null): ?string
    {
        return tgauth_tegalogIni()[$key] ?? $default;
    }

    // ログイン必須(かつ任意で最低権限レベル)を満たしていなければ、
    // 呼び出し側が用意したコールバックを実行してそこで終了させる。
    // 満たしていればユーザーIDを返す。
    // $onFail は関数(string $reason)を受け取り、内部で exit すること。
    // (JSON APIかHTML画面かで応答形式が異なるため、応答自体は呼び出し側に委ねる設計)
    function tgauth_requireLogin(callable $onFail, int $minLevel = 0): string
    {
        $user = tgauth_currentUser();
        if ($user === null) {
            $onFail('not_logged_in');
            exit; // $onFail が exit しなかった場合の保険
        }
        if ($minLevel > 0 && tgauth_userLevel($user) < $minLevel) {
            $onFail('insufficient_level');
            exit;
        }
        return $user;
    }

    // 拡張スクリプト側の秘密文字列(salt等)とログインユーザーIDから決定的にCSRFトークンを導出する。
    // セッションストレージを持たない単一ファイル配布物として、外部サイトからの偽造フォーム送信を
    // 防ぐための最低限の対策。$secret は各拡張スクリプトが自分の設定ファイル等から渡すこと。
    function tgauth_csrfToken(string $secret, string $loginUser): string
    {
        return hash('sha256', 'tgauth-csrf|' . $loginUser . '|' . $secret);
    }

    function tgauth_verifyCsrfToken(string $secret, string $loginUser, ?string $token): bool
    {
        if ($token === null || $token === '') {
            return false;
        }
        return hash_equals(tgauth_csrfToken($secret, $loginUser), $token);
    }

} // if (!function_exists('tgauth_currentUser'))
