Search code examples
phpjsonsessionsession-variables

Custom Session Handler With JSON in PHP Destroy Session by Mistake


I have a task to implement my Custom Session Handler in PHP, where data from each session is saved in a specified directory in a JSON Text file.

Here is the code (it's a combination of code from documentation and user's notes from it):

class CustomSessionHandler implements SessionHandlerInterface
{
    //this field contains folder, where all sessions files will be stored
    private string $savePath;

    //according to docs
    public function close(): bool
    {
        return true;
    }

    //delete file of session with specified ID
    public function destroy(string $id): bool
    {
        $sessionPath = $this->savePath . DIRECTORY_SEPARATOR . "sess_" . $id . ".json";
        if (file_exists($sessionPath)) {
            unlink($sessionPath);
        }
        return true;
    }

    //according to docs
    public function gc(int $max_lifetime): int|false
    {
        foreach (glob($this->savePath . DIRECTORY_SEPARATOR . "sess_*" . ".json") as $file) {
            if (filemtime($file) + $max_lifetime < time() && file_exists($file)) {
                unlink($file);
            }
        }
        return true;
    }

    //generally, 'if' doesn't make any sense because save directory in my
    //study project will always exist
    public function open(string $path, string $name): bool
    {
        $this->savePath = $path;
        if (!is_dir($path)) {
            mkdir($path);
        }
        return true;
    }

    //I think the problem is here...
    public function read(string $id): string
    {
        $sessionPath = $this->savePath . DIRECTORY_SEPARATOR . "sess_" . $id . ".json";
        if (!file_exists($sessionPath)) {
            file_put_contents($sessionPath, "");
            chmod($sessionPath, 0777);
        }
        return file_get_contents($sessionPath);
    }

    //create JSON string from serialized session string
    public function write(string $id, string $data): bool
    {
        $rawData = [];
        try {
            $rawData = self::unserialize_php($data);
        } catch (Exception $e) {
            echo $e->getMessage();
        }
        $jsonData = json_encode($rawData);
        return !(file_put_contents($this->savePath . DIRECTORY_SEPARATOR . 'sess_' . $id . ".json", $jsonData) === false);
    }

    /**
     * @throws Exception
     */
    //this code is from comment section from docs and it works fine
    private static function unserialize_php($sessionData): array
    {
        $returnData = [];
        $offset = 0;
        while ($offset < strlen($sessionData)) {
            if (!str_contains(substr($sessionData, $offset), "|")) {
                throw new Exception("invalid data, remaining: " . substr($sessionData, $offset));
            }
            $pos = strpos($sessionData, "|", $offset);
            $num = $pos - $offset;
            $varName = substr($sessionData, $offset, $num);
            $offset += $num + 1;
            $data = unserialize(substr($sessionData, $offset));
            $returnData[$varName] = $data;
            $offset += strlen(serialize($data));
        }
        return $returnData;
    }
}

I create an authentication system. In the beginning of the file named login.php, which is responsible for checking user credentials, the following code is in place:

$handler = new CustomSessionHandler();
session_set_save_handler($handler);
session_start([
    'save_path' => '/var/www/phpcourse.loc/hometask_3/sessions',
]);

if (isset($_SESSION['logged_in']) && $_SESSION['logged_in'] === true) {
    //welcome.php is the page, where user just see 'Hi, {username}' and can logout
    header('Location: welcome.php');
    exit;
}

The problem is that sessions files are created and deleted when it shouldn't happen.

Because of this I can't login and I have a warning:

Warning: session_start(): Failed to decode session object. Session has been destroyed in /var/www/phpcourse.loc/hometask_3/login.php on line 12

Next, if user's credentials are correct, $_SESSION variables are set:

if (password_verify($password, $hashed_password)) {
    $handler = new CustomSessionHandler();
    session_set_save_handler($handler);
    session_start([
        'save_path' => '/var/www/phpcourse.loc/hometask_3/sessions',
    ]);

    $_SESSION['logged_in'] = true;
    $_SESSION['id'] = $id;
    $_SESSION['username'] = $username;

    header('Location: welcome.php');
}

If I debug this code and go through each step, here what happens:

  1. On the first download of the login page session file is created completely empty

  2. After pressing LOGIN button session file is getting destroyed just after returning value from read() function of CustomSessionHandler class. It happens in session_start() that is first in code above.

  3. When the code reach second session_start(), when all credentials are correct and next page (welcome.php) is opening, session file is created again and all variables are getting written to it in JSON format.

But when I start session in the beginning of script welcome.php the same way as in file login.php file is getting destroyed again after read(). And I'm again on Login page.

What is going on?

Upate: It seems that something is wrong with my CustomSessionHandler, because if I just put simple session_start() at the beginning of each script, everything is OK.

Maybe there is a problem with includes? Here is files, where session_start() is called. I don't put file where session is getting destroyed when user logout.

login.php

<?php

use hometask_3\DBConnection;
use hometask_3\CustomSessionHandler;

require_once 'classes/DBConnection.php';
require_once 'classes/CustomSessionHandler.php';

$handler = new CustomSessionHandler();
session_set_save_handler($handler);
session_start([
    'save_path' => '/var/www/phpcourse.loc/hometask_3/sessions',
]);

if (isset($_SESSION['logged_in']) && $_SESSION['logged_in'] === true) {
    header('Location: welcome.php');
    exit;
}

//next step is form validation, I just leave code where session
//variables are used
if (password_verify($password, $hashed_password)) {
    $_SESSION['logged_in'] = true;
    $_SESSION['id'] = $id;
    $_SESSION['username'] = $username;
    header('Location: welcome.php');
}

Again, after reloading login page warning about destroyed session object is showed.

welcome.php

<?php

use hometask_3\CustomSessionHandler;

require_once 'classes/CustomSessionHandler.php';

$handler = new CustomSessionHandler();
session_set_save_handler($handler);
session_start([
    'save_path' => '/var/www/phpcourse.loc/hometask_3/sessions',
]);

if (!isset($_SESSION['logged_in']) || $_SESSION['logged_in'] !== true) {
    header('Location: login.php');
    exit;
}

include 'view/welcomeForm.html.php';

I still can't figure out, why method destroy() from session handler is called, when I call session_start() in file welcome.php.


Solution

  • SOLVED

    As I said, problem was in method read. I completely forget about PHP serialization methods, and read() method returned false because it couldn't unserialize the session data.

    As I said, the problem was in read() method of CustomSessionHandler class. I forgot, that this method should return string that can be unserialized by standard PHP serialization method for session's data.

    Because my method returns a JSON Text string, an error happened and session got immediately destroyed, so, destroy() was called.

    Now my read method looks like this:

    public function read(string $id): string
    {
        $sessionPath = $this->savePath . DIRECTORY_SEPARATOR . 
                       "sess_" . $id . ".json";
        if (!file_exists($sessionPath)) {
            file_put_contents($sessionPath, "");
            chmod($sessionPath, 0777);
        }
        $fileData = file_get_contents($sessionPath);
        $arrData = json_decode($fileData);
        if (!$fileData) {
            return '';
        } else {
            return self::serialize_php($arrData);
        }
    }
    

    Method serialize_php($data) was stolen as well from the docs or Github. It just manually serializes data in the necessary for PHP format.

    public static function serialize_php(array|stdClass $data): string
    {
        $res = '';
        foreach ($data as $key => $value) {
            if (strcmp($key, (string)intval($key)) === 0) {
                //unsupported integer key
                continue;
            }
            if (strcspn($key, '|!') !== strlen($key)) {
                //unsupported characters
                return '';
            }
            $res .= $key . '|' . serialize($value);
        }
        return $res;
    }
    

    So, I don't think that this solution is the best, but now everything works. At first I wanted to redefine session constant session.serialize_handler, but I didn't understood how to do it. Of course I removed one call of session_start() in my script.