セッションもどき

PHPで開発するとき、小さな組織のお客さんだと、
稀にセッションも使えないようなサーバで動作する
プログラムの実装を要求されることがあります。
そんなときに作った苦し紛れのセッションもどきスクリプトを以下に記します。

<?
if (file_exists("Blowfish.php")) {
    include_once "Blowfish.php";
}

////////////////////////////////////////////////////////////////////////////////
// 定数

// セッション ID を格納するクッキーのキー
define("SESS_KEY", "PHPSESSID");

// セッション ID の文字列長
define("SESS_ID_LEN", 32);

// セッションファイル格納ディレクトリパス
// 公開ディレクトリ以下に置く場合は推測されにくい名称を使うこと
define("SESS_DIR", "tC7h4DaIIOGFnOFkICeXD5Mu");

// セッションデータ暗号化用キー
define("SESS_CRYPT_KEY", "OSyzLnxLLk3iWErjaohljZELtQVIcDKT");

// セッションデータにおける文字列中の改行を示す文字列
define("SESS_STR_NL", "___NEW_LINE___");

// セッションデータ読み書き時のブロックサイズ
define("SESS_BLOCK_SIZE", 256);

// セッションタイムアウト(秒)
define("SESS_TIMEOUT", 900);

////////////////////////////////////////////////////////////////////////////////
// 変数

$___Session___errmsg = "";
$___Session___params = array();

////////////////////////////////////////////////////////////////////////////////
// 関数

/**
 * セッションにおけるエラーメッセージを取得、設定します。
 * @param string $msg エラーメッセージです。
 * @return string エラーメッセージです。
 */
function sessionError($msg = null) {
    global $___Session___errmsg;
    
    if ($msg == null) {
        return $___Session___errmsg;
    }
    else {
        $___Session___errmsg = $msg;
    }
}

/** 
 * セッションパラメタを取得、設定します。
 * 引数をひとつも渡さない場合はパラメタを配列で返します。
 * @param string $key 取得、設定対象のパラメタ名です。
 * @param mixed $value 設定する値です。
 * @return mixed 指定したパラメタ、もしくはパラメタを全て返します。
 */
function session($key = null, $value = null) {
    global $___Session___params;

    // キーが指定されなかった場合は配列ごと返す ================================
    if ($key == null) {
        return $___Session___params;
    }
    // キーが指定された場合は対応する値を扱う ==================================
    else {
        // 値が指定されなかった場合は対応する値を返す --------------------------
        if ($value === null) {
            return $___Session___params[$key];
        }
        // 値が指定された場合は設定する ----------------------------------------
        else {
            $___Session___params[$key] = $value;
        }
    }
}

/**
 * セッションを開始します。
 * @return boolean 成功した場合はtrue、失敗した場合はfalseです。
 */
function createSession() {
    $result  = true;
    $sess_id = "";

    // セッションファイルを格納するディレクトリが無ければ作成する。
    // 更にタイムアウトしたセッションのファイルを削除する。
    if (!file_exists(SESS_DIR)) {
        mkdir(SESS_DIR);
        fclose(fopen($path, "w"));
    }
    else {
        if (($dh = opendir(SESS_DIR)) !== false) {
            while (($elem = readdir($dh)) !== false) {
                if ($elem == "." || $elem == "..") {
                    continue;
                }
                $stat = stat(SESS_DIR . "/" . $elem);
                if (time() - $stat[8] > SESS_TIMEOUT) {
                    unlink(SESS_DIR . "/" . $elem);
                }
            }
        }
    }
    
    // セッション ID 取得
    do {
        // クッキーにセッション ID が設定されていない場合は生成し、
        // クッキーに設定
        if (empty($_COOKIE[SESS_KEY])) {
            $sess_id = getRandomString(SESS_ID_LEN);
            $_COOKIE[SESS_KEY] = $sess_id;
            setcookie(SESS_KEY, $sess_id);
            $result = true;
        }
        // クッキーにセッション ID が設定されている場合は取得
        // セッション ID の文字列長が不正な場合はクッキー変数から削除
        else {
            $sess_id = $_COOKIE[SESS_KEY];
            if (strlen($sess_id) != SESS_ID_LEN) {
                $_COOKIE[SESS_KEY] = "";
                $result = false;
            }
        }
    }
    while (!$result);

    // 値読込
    if ($result) {
        $path = SESS_DIR . "/" . $sess_id; // セッションファイルパス
        
        if (!file_exists($path)) {
            fclose(fopen($path, "w"));
        }
        else {
            $fh = fopen($path, "r");
            if ($fh !== false) {
                // 一定サイズずつ読みこんで復号化する。
                $buf  = "";
                if (class_exists("Crypt_Blowfish")) {
                    $bfish = new Crypt_Blowfish(SESS_CRYPT_KEY);
                    while (!feof($fh)) {
                        // パディングのために null 文字が詰められている可能
                        // 性があるため、念のため置換しておく
                        $buf .= str_replace(
                            "\0",
                            "",
                            $bfish->decrypt(fread($fh, SESS_BLOCK_SIZE))
                        );
                    }
                }
                else {
                    while (!feof($fh)) {
                        $buf .= fread($fh, SESS_BLOCK_SIZE);
                    }
                }
                
                fclose($fh);
                
                // 復号化したセッションデータをパラメタごとに分割する。
                $pos = 0;
                while (true) {
                    $nlPos = strpos($buf, "\n", $pos);
                    if ($nlPos === false) {
                        $line = substr($buf, $pos);
                        $pos  = -1;
                    }
                    else {
                        $line = substr($buf, $pos, $nlPos - $pos);
                        $pos  = $nlPos + 1;
                    }
                    
                    list($key, $value) = split("=", $line, 2);
                    session(
                        $key,
                        getSafeSessionParameter(unserialize($value), true)
                    );
                    
                    if ($pos == -1) {
                        break;
                    }
                }
                
                return true;
            }
            else {
                sessionError(
                    "セッションデータファイル[" . $sess_id . "]" .
                    "を読み込むことが出来ませんでした。"
                );
            }
        }
    }

    return false;
}

/**
 * セッションパラメタの内容をファイルに書き込みます。
 * @return boolean 書き込みに成功した場合はtrue、そうでなければfalseです。
 */
function commitSession() {
    $result = false;
    
    if (!empty($_COOKIE[SESS_KEY])) {
        $params = session();
        $buf    = "";

        // セッションパラメタをひとつの変数に突っ込む
        foreach (array_keys($params) as $key) {
            $buf .=
                "$key=" .
                serialize(getSafeSessionParameter($params[$key])) .
                "\n";
        }

        if (($fh = fopen(SESS_DIR . "/" . $_COOKIE[SESS_KEY], "w")) !== false) {
            // 既定バイト数ずつ暗号化して書き込む
            $pos = 0;
            if (class_exists("Crypt_Blowfish")) {
                $bfish = new Crypt_Blowfish(SESS_CRYPT_KEY);
                while (
                    ($block = substr($buf, $pos, SESS_BLOCK_SIZE)) !== false
                ) {
                    fwrite($fh, $bfish->encrypt($block));
                    $pos += SESS_BLOCK_SIZE;
                }
            }
            else {
                while (
                    ($block = substr($buf, $pos, SESS_BLOCK_SIZE)) !== false
                ) {
                    fwrite($fh, $block);
                    $pos += SESS_BLOCK_SIZE;
                }
            }
            
            fclose($fh);
            $result = true;
        }
    }

    return $result;
}

/**
 * セッションを破棄します。
 */
function destroySession() {
    $result = false;
    
    if (!empty($_COOKIE[SESS_KEY])) {
        $result = unlink(SESS_DIR . "/" . $_COOKIE[SESS_KEY]);
        setcookie(SESS_KEY, "", time() - 3600);
    }
    return $result;
}

/**
 * セッションファイルにデータを保存する際、文字列を安全な表現に変換します。
 * @param mixed $value 変換対象です。これが配列の場合、再帰的に処理します。
 * @param boolean $unsafe 安全な表現から元の表現に戻す場合、trueを指定します。
 * @return mixed 変換後の値です。
 */
function getSafeSessionParameter($value, $unsafe = false) {
    if (is_string($value)) {
        if ($unsafe) {
            return str_replace(SESS_STR_NL, "\n", $value);
        }
        else {
            return str_replace("\n", SESS_STR_NL, $value);
        }
    }
    else if (is_array($value)) {
        for ($i = 0; $i < count($value); $i++) {
            $value[$i] = getSafeSessionParameter($value[$i], $unsafe);
        }
        return $value;
    }
    else {
        return $value;
    }
}

/**
 * ランダムな文字列を返します。
 * @param integer $len 文字列長です。
 * @return string ランダムな文字列です。
 */
function getRandomString($len) {
    $str   = "";
    $chars = array(
        "a", "b", "c", "d", "e", "f", "g", "h", "i", "j",
        "k", "l", "m", "n", "o", "p", "q", "r", "s", "t",
        "u", "v", "w", "x", "y", "z",
        "A", "B", "C", "D", "E", "F", "G", "H", "I", "J",
        "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T",
        "U", "V", "W", "X", "Y", "Z",
        "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"
    );
    $charCount = count($chars);

    for ($i = 0; $i < $len; $i++) {
        $str .= $chars[mt_rand(0, $charCount - 1)];
    }

    return $str;
}
?>

このライブラリはセッションデータの暗号化にCrypt_Blowfishを使用します
(http://pear.php.net/package/Crypt_Blowfish)。
ダウンロードしてPHPが見つけられるところに置いておけばOKです。
ちなみに、Crypt_Blowfishがなくても一応動きますが、
データは平文で格納されてしまいます。
データファイルの格納パスがドキュメントルートより上ならばいいですが、
このライブラリが必要になるようなサーバだと、
ドキュメントルート下にしかファイルを置けないことが多いと思いますので、
Crypt_Blowfishは必要かと思います。



ソースコードの最初の方の行にある、
定数SESS_DIRとSESS_CRYPT_KEYには任意の値を設定して下さい。
SESS_DIRはセッションデータファイルの格納ディレクトリパスで、
ドキュメントルート下であればせめてランダムな文字列にしておくべきです
(「Options Indexes」みたいにファイル一覧が表示されないようになっていることは大前提)。
また、SESS_CRYPT_KEYは暗号化用のキーですので、
32文字のランダムな文字列を指定する必要があります。



セッションIDはクッキー経由のみなので、
携帯には使えません。
使いたい方は改造してみて下さい。

んで、使い方。

<?
// セッションを使い始めるときには必ず実行。
createSession();

// 値を取得。
$name = session("name");

// 値を設定。
if (strlen($name) > 50) {
    session("errmsg", "名前が長すぎです。");
    
    // セッションの処理が終わったらファイルに書き込む。
    commitSession();
    header("Location: http://hoge.com/top.php");
    exit();
}

// 必要がなくなったらファイルを削除。
destroySession();
?>

あんま使い道ないかも知れませんが、
もし使うときは自己責任で!