dpDesignz
dpDesignz

Reputation: 1959

session_write_close(): Failed to write session data using user defined save handler. PHP 7.3.1

I've written a custom session handler class which works for the most part, until I try to log my users out and destroy the session, then I keep getting the following error:

ErrorException: 0

session_write_close(): Failed to write session data using user defined save handler. (session.save_path: D:\xampp\tmp)

I am running PHP Version 7.3.1 on XAMPP.

This is my custom class

class CornerstoneSessionHandler implements SessionHandlerInterface {

    /**
     * Construct the Session
     * No parameters required, nothing will be returned
  */
  public function __construct() {

    // Set the handler to overide SESSION
    session_set_save_handler(
      array($this, "open"),
      array($this, "close"),
      array($this, "read"),
      array($this, "write"),
      array($this, "destroy"),
      array($this, "gc")
      );

      // Set the shutdown function
      register_shutdown_function('session_write_close');

      /** Define and initialise the Session Handler */
      session_start();
  }

    /**
     * Set the open callback
     *
     * @param string $savePath
     * @param string $sessionName
   *
   * @return bool return value should be true for success or false for failure
  */
  public function open($savePath, $sessionName) {

        // Make the $csdb global accessible
        global $csdb;

    // Check that the DB connection is set
    return ((!empty($csdb)) && $csdb->isConnected() != 1) ? FALSE : TRUE ;
  }

    /**
     * Set the close callback
   *
   * @return bool return value can only be true for success
  */
  public function close() {
    return TRUE;
  }

    /**
     * Set the read callback
     *
     * @param string $sessionID
   *
   * @return string return value should be the session data or an empty string
  */
  public function read($sessionID) {

        // Make the $csdb global accessible
        global $csdb;

    // Get the session from the database
    $csdb->query_prepared("SELECT session_data FROM cs_session WHERE session_id=?", [$sessionID]);

    // If results returned, continue
        if($csdb->getNum_Rows() > 0) {

            // Get the data
            $result = $csdb->get_row(NULL);
      return $result->session_data;

    } else { // Else return an empty string

      return '';

    }
  }

    /**
     * Set the write callback
     *
     * @param string $sessionID
     * @param string $data
   *
   * @return bool return value should be true for success or false for failure
  */
  public function write($sessionID, $data) {

        // Make the $csdb global accessible
        global $csdb;

    // Set the time stamp
    $access_dtm = new \DateTime();

    // Replace the data
    $csdb->query_prepared('REPLACE INTO cs_session(session_id, session_ip_address, session_data , session_access_dtm) VALUES (?, ?, ?, ?)', [$sessionID, $_SERVER['REMOTE_ADDR'], $data, $access_dtm->format('Y-m-d H:i:s')]);

    // If modified a success, return true
        if($csdb->getNum_Rows() > 0) {
      return TRUE;
    } else { // Else, return false
      return FALSE;
    }
  }

    /**
     * Set the destroy callback
     *
     * @param string $sessionID
   *
   * @return bool return value should be true for success or false for failure
  */
  public function destroy($sessionID) {

        // Make the $csdb global accessible
    global $csdb;

    // Delete the session from the database
    $csdb->delete('cs_session', where(eq('session_id', $sessionID)));

    // If results returned, return true
        if($csdb->affectedRows() > 0) {
      return TRUE;
    } else { // Else, return false
      return FALSE;
    }
  }

    /**
     * Set the garbage collector callback
     *
     * @param string $lifetime
   *
   * @return bool return value should be true for success or false for failure
  */
  public function gc($lifetime) {

        // Make the $csdb global accessible
        global $csdb;

    // Set the date calculation
    $expiredTime = new \DateTime();
    $expiredTime->modify('-' . $lifetime . ' seconds');

    // Get the session from the database
    $csdb->delete('cs_session', where(lt('session_access_dtm', $expiredTime->format('Y-m-d H:i:s'))));

    // If results deleted, return true
        if($csdb->affectedRows() > 0) {
      return TRUE;
    } else { // Else, return false
      return FALSE;
    }

  }
}

I am using ezSQL as my database handler.

This is the code in the top of all my pages

/**
 * Set session data so that logins work properly over all browsers
 * This should fix login errors some users can face
 * More info can be found at {@link https://www.php.net/manual/en/session.security.ini.php the php user manual}
 */
# PREVENTING SESSION HIJACKING
# Prevents javascript XSS attacks aimed to steal the session ID
ini_set('session.cookie_httponly', 1);
# Make sure the cookie lifetime is set to '0'
ini_set('session.cookie_lifetime', 0);
# Adds entropy into the randomization of the session ID, as PHP's random number
# generator has some known flaws
ini_set('session.entropy_file', '/dev/urandom');
# Uses a strong hash
ini_set('session.hash_function', 'whirlpool');
# Set the session save location (best for shared servers)
# Uncomment out the next line if you would like to set a custom path and haven't already set the value in your `php.ini` file.
# ini_set('session.save_path',realpath(ABSPATH . 'tmp' . DIRECTORY_SEPARATOR));
# Note: The folder referenced above must exist for it to work.
# Set the session garbage collection lifetime to custom defined minutes (PHP default is 24 minutes)
ini_set('session.gc_maxlifetime', (int)get_option("session_expire") * MINUTE_IN_SECONDS);
# Enable session garbage collection with a 1% chance of
# running on each session_start()
ini_set('session.gc_probability', 1);
ini_set('session.gc_divisor', 100);
# Uses a secure connection (HTTPS) if possible
# Allow cookies to be sent over insecure connections if not an HTTPS site
(SITE_HTTPS) ? ini_set('session.cookie_secure', 1) : ini_set('session.cookie_secure', false);
# PREVENTING SESSION FIXATION
# Session ID cannot be passed through URLs
# so only use cookies to store the session id on the client side
ini_set('session.use_only_cookies', 1);
# Set a custom session name
session_name('CSSESSID');
# Load the session class
new CornerstoneSessionHandler();
# Start the session if it's not already started
if (session_status() == PHP_SESSION_NONE) {session_start();}

The weird thing is, even though it's giving me that error, if I check the database the data is being updated no worries.

This is my logout code

/**
 * Logout the user
 *
 * @return bool
*/
public static function logoutUser() {

    // Make the $fear global accessible
    global $fear; // Direct access check

    // Check if session set (just in case) and start if it isn't
    if(session_id() == '') {session_start();}

    // Delete the $_SESSION data set in `authenticateUser()`
    unset($_SESSION['HTTP_USER_AGENT']);
    unset($_SESSION['_cs-uid']);
    unset($_SESSION['_cs-ue']);
    unset($_SESSION['_cs-ul']);
    unset($_SESSION['_cs-un']);

    /**
     * Get the "ext.auth.php" file and run `clearCustomAuth()` function
     * to clear any custom set $_SESSION items
    */
    require_once( get_lib_path('ext.auth.php') );
    clearCustomAuth();

    // Regenerate a new session ID just to be sure
    session_regenerate_id();

    // Destroy the session
    session_destroy();

    // Check if the $_COOKIE data if set
    if(isset( $_COOKIE['_cs-ti'] ) && !empty( $_COOKIE['_cs-ti'] )) {
        // Delete the cookie token from the database
        if(!self::deleteAuthCookie($_COOKIE['_cs-ti'])) {
            // Return false if cookie couldn't be deleted
            return false;
        }
    }

    // Return true if run all the way
    return true;
}

I've tried shifting around the session_regenerate_id() but if I put it after the session_destroy() it says there's no session to regenerate, but if I put it before the session_destroy() or don't have it at all, my session is deleted from my database but I can still see it in my storage in my inspector on Firefox.

It only gives me an error when trying to delete a session from the database. Everything else works no worries! I've manually tried to delete a session from the database in testing and I still get that error.

Is anyone able to see what I might be doing wrong or how I can fix it? I've only just learnt about this so pretty new to it. If I need to supply more information, just let me know. I've been searching over the web for nearly 2 days with no luck.

Upvotes: 1

Views: 14541

Answers (3)

Julesezaar
Julesezaar

Reputation: 3406

Did you try to turn it off and on? :) Hope this helps somebody but when using Docker (and Lando) we suddenly had this one in our local Symfony environment.

Tried everything, from removing specific containers to lando destroy,... Always the same error.

In the end a restart of the computer simply solved the issue. I now assume a Docker restart should also fix it. But anyway, hope this helps.

Upvotes: 0

You can use session_start optional parameters



session_set_save_handler(new CornerstoneSessionHandler(), true);

session_start([
    'read_and_close' => true, //you should add this 
]);


$_SESSION['a'] = 45;

echo $_SESSION['a'];

unset($_SESSION['a']);

Upvotes: 1

Jimbolino
Jimbolino

Reputation: 398

I don't see a reason why you need to set a shutdown handler. Sessions are automatically saved when php shuts down, even when you get a fatal error.

Also the write, destroy and gc should always return true, even if no rows were updated / deleted. This seems counter intuitive, but php considers deleting a non existing session also a success.

Upvotes: 2

Related Questions