// dev/core#1762 Don't log subscription_history
$this->tables = preg_grep('/^civicrm_subscription_history/', $this->tables, PREG_GREP_INVERT);
+ // Don't log sessions
+ $this->tables = preg_grep('/^civicrm_session/', $this->tables, PREG_GREP_INVERT);
+
// do not log civicrm_mailing_recipients table, CRM-16193
$this->tables = array_diff($this->tables, ['civicrm_mailing_recipients']);
$this->logTableSpec = array_fill_keys($this->tables, []);
*/
use Civi\Standalone\Security;
+use Civi\Standalone\SessionHandler;
/**
* Standalone specific stuff goes here.
// TODO: Prettier error page
}
+ /**
+ * Start a new session.
+ */
+ public function sessionStart() {
+ $session_handler = new SessionHandler();
+ session_set_save_handler($session_handler);
+
+ $session_max_lifetime = Civi::settings()->get('standaloneusers_session_max_lifetime');
+
+ session_start([
+ 'cookie_httponly' => 1,
+ 'gc_maxlifetime' => $session_max_lifetime,
+ 'name' => 'CIVISOSESSID',
+ 'use_cookies' => 1,
+ 'use_only_cookies' => 1,
+ 'use_strict_mode' => 1,
+ ]);
+ }
+
}
--- /dev/null
+<?php
+
+class CRM_Standaloneusers_BAO_Session extends CRM_Standaloneusers_DAO_Session {
+
+ /**
+ * Delete all expired sessions
+ *
+ * @param DB_mysqli $db
+ * @param string $expiration_date
+ * @return void
+ */
+ public static function deleteExpired($db, $expiration_date) {
+ $table_name = self::$_tableName;
+ $stmt = $db->prepare("DELETE FROM $table_name WHERE last_accessed < ?");
+ $db->execute($stmt, $expiration_date);
+ }
+
+ /**
+ * Delete a session with a specific session ID
+ *
+ * @param DB_mysqli $db
+ * @param string $session_id
+ * @return void
+ */
+ public static function destroy($db, $session_id) {
+ $table_name = self::$_tableName;
+ $stmt = $db->prepare("DELETE FROM $table_name WHERE session_id = ?");
+ $db->execute($stmt, $session_id);
+ }
+
+ /**
+ * Read serialized session data
+ *
+ * @param DB_mysqli $db
+ * @param string $session_id
+ * @return string
+ */
+ public static function read($db, $session_id) {
+ $table_name = self::$_tableName;
+ $stmt = $db->prepare("SELECT * FROM $table_name WHERE session_id = ? FOR UPDATE");
+
+ return $db->execute($stmt, $session_id)->fetchRow(DB_FETCHMODE_ASSOC);
+ }
+
+ /**
+ * Update session data or just the last_accessed timestamp if no data is provided
+ *
+ * @param DB_mysqli $db
+ * @param string $session_id
+ * @param string $data
+ * @return void
+ */
+ public static function write($db, $session_id, $data = NULL) {
+ $table_name = self::$_tableName;
+
+ if (is_null($data)) {
+ $stmt = $db->prepare("
+ INSERT INTO $table_name (session_id, last_accessed) VALUES (?, NOW())
+ ON DUPLICATE KEY UPDATE last_accessed = NOW()
+ ");
+
+ $db->execute($stmt, $session_id);
+ }
+ else {
+ $stmt = $db->prepare("
+ INSERT INTO $table_name (session_id, data, last_accessed) VALUES (?, ?, NOW())
+ ON DUPLICATE KEY UPDATE data = ?, last_accessed = NOW()
+ ");
+
+ $db->execute($stmt, [$session_id, $data, $data]);
+ }
+ }
+
+}
--- /dev/null
+<?php
+
+/**
+ * @package CRM
+ * @copyright CiviCRM LLC https://civicrm.org/licensing
+ *
+ * Generated from standaloneusers/xml/schema/CRM/Standaloneusers/Session.xml
+ * DO NOT EDIT. Generated by CRM_Core_CodeGen
+ * (GenCodeChecksum:aa80480a6d443480358eca06cc367bdd)
+ */
+use CRM_Standaloneusers_ExtensionUtil as E;
+
+/**
+ * Database access object for the Session entity.
+ */
+class CRM_Standaloneusers_DAO_Session extends CRM_Core_DAO {
+ const EXT = E::LONG_NAME;
+ const TABLE_ADDED = '';
+
+ /**
+ * Static instance to hold the table name.
+ *
+ * @var string
+ */
+ public static $_tableName = 'civicrm_session';
+
+ /**
+ * Should CiviCRM log any modifications to this table in the civicrm_log table.
+ *
+ * @var bool
+ */
+ public static $_log = FALSE;
+
+ /**
+ * Unique Session ID
+ *
+ * @var int|string|null
+ * (SQL type: int)
+ * Note that values will be retrieved from the database as a string.
+ */
+ public $id;
+
+ /**
+ * Hexadecimal Session Identifier
+ *
+ * @var string
+ * (SQL type: char(64))
+ * Note that values will be retrieved from the database as a string.
+ */
+ public $session_id;
+
+ /**
+ * Session Data
+ *
+ * @var string|null
+ * (SQL type: longtext)
+ * Note that values will be retrieved from the database as a string.
+ */
+ public $data;
+
+ /**
+ * Timestamp of the last session access
+ *
+ * @var string|null
+ * (SQL type: datetime)
+ * Note that values will be retrieved from the database as a string.
+ */
+ public $last_accessed;
+
+ /**
+ * Class constructor.
+ */
+ public function __construct() {
+ $this->__table = 'civicrm_session';
+ parent::__construct();
+ }
+
+ /**
+ * Returns localized title of this entity.
+ *
+ * @param bool $plural
+ * Whether to return the plural version of the title.
+ */
+ public static function getEntityTitle($plural = FALSE) {
+ return $plural ? E::ts('Sessions') : E::ts('Session');
+ }
+
+ /**
+ * Returns all the column names of this table
+ *
+ * @return array
+ */
+ public static function &fields() {
+ if (!isset(Civi::$statics[__CLASS__]['fields'])) {
+ Civi::$statics[__CLASS__]['fields'] = [
+ 'id' => [
+ 'name' => 'id',
+ 'type' => CRM_Utils_Type::T_INT,
+ 'title' => E::ts('ID'),
+ 'description' => E::ts('Unique Session ID'),
+ 'required' => TRUE,
+ 'usage' => [
+ 'import' => FALSE,
+ 'export' => FALSE,
+ 'duplicate_matching' => FALSE,
+ 'token' => FALSE,
+ ],
+ 'where' => 'civicrm_session.id',
+ 'table_name' => 'civicrm_session',
+ 'entity' => 'Session',
+ 'bao' => 'CRM_Standaloneusers_DAO_Session',
+ 'localizable' => 0,
+ 'html' => [
+ 'type' => 'Text',
+ ],
+ 'readonly' => TRUE,
+ 'add' => NULL,
+ ],
+ 'session_id' => [
+ 'name' => 'session_id',
+ 'type' => CRM_Utils_Type::T_STRING,
+ 'title' => E::ts('Session ID'),
+ 'description' => E::ts('Hexadecimal Session Identifier'),
+ 'required' => TRUE,
+ 'maxlength' => 64,
+ 'size' => CRM_Utils_Type::BIG,
+ 'usage' => [
+ 'import' => FALSE,
+ 'export' => FALSE,
+ 'duplicate_matching' => FALSE,
+ 'token' => FALSE,
+ ],
+ 'where' => 'civicrm_session.session_id',
+ 'table_name' => 'civicrm_session',
+ 'entity' => 'Session',
+ 'bao' => 'CRM_Standaloneusers_DAO_Session',
+ 'localizable' => 0,
+ 'html' => [
+ 'type' => 'Text',
+ ],
+ 'add' => NULL,
+ ],
+ 'data' => [
+ 'name' => 'data',
+ 'type' => CRM_Utils_Type::T_LONGTEXT,
+ 'title' => E::ts('Data'),
+ 'description' => E::ts('Session Data'),
+ 'usage' => [
+ 'import' => FALSE,
+ 'export' => FALSE,
+ 'duplicate_matching' => FALSE,
+ 'token' => FALSE,
+ ],
+ 'where' => 'civicrm_session.data',
+ 'table_name' => 'civicrm_session',
+ 'entity' => 'Session',
+ 'bao' => 'CRM_Standaloneusers_DAO_Session',
+ 'localizable' => 0,
+ 'add' => NULL,
+ ],
+ 'last_accessed' => [
+ 'name' => 'last_accessed',
+ 'type' => CRM_Utils_Type::T_DATE + CRM_Utils_Type::T_TIME,
+ 'title' => E::ts('Last Accessed'),
+ 'description' => E::ts('Timestamp of the last session access'),
+ 'usage' => [
+ 'import' => FALSE,
+ 'export' => FALSE,
+ 'duplicate_matching' => FALSE,
+ 'token' => FALSE,
+ ],
+ 'where' => 'civicrm_session.last_accessed',
+ 'table_name' => 'civicrm_session',
+ 'entity' => 'Session',
+ 'bao' => 'CRM_Standaloneusers_DAO_Session',
+ 'localizable' => 0,
+ 'add' => NULL,
+ ],
+ ];
+ CRM_Core_DAO_AllCoreTables::invoke(__CLASS__, 'fields_callback', Civi::$statics[__CLASS__]['fields']);
+ }
+ return Civi::$statics[__CLASS__]['fields'];
+ }
+
+ /**
+ * Return a mapping from field-name to the corresponding key (as used in fields()).
+ *
+ * @return array
+ * Array(string $name => string $uniqueName).
+ */
+ public static function &fieldKeys() {
+ if (!isset(Civi::$statics[__CLASS__]['fieldKeys'])) {
+ Civi::$statics[__CLASS__]['fieldKeys'] = array_flip(CRM_Utils_Array::collect('name', self::fields()));
+ }
+ return Civi::$statics[__CLASS__]['fieldKeys'];
+ }
+
+ /**
+ * Returns the names of this table
+ *
+ * @return string
+ */
+ public static function getTableName() {
+ return self::$_tableName;
+ }
+
+ /**
+ * Returns if this table needs to be logged
+ *
+ * @return bool
+ */
+ public function getLog() {
+ return self::$_log;
+ }
+
+ /**
+ * Returns the list of fields that can be imported
+ *
+ * @param bool $prefix
+ *
+ * @return array
+ */
+ public static function &import($prefix = FALSE) {
+ $r = CRM_Core_DAO_AllCoreTables::getImports(__CLASS__, 'session', $prefix, []);
+ return $r;
+ }
+
+ /**
+ * Returns the list of fields that can be exported
+ *
+ * @param bool $prefix
+ *
+ * @return array
+ */
+ public static function &export($prefix = FALSE) {
+ $r = CRM_Core_DAO_AllCoreTables::getExports(__CLASS__, 'session', $prefix, []);
+ return $r;
+ }
+
+ /**
+ * Returns the list of indices
+ *
+ * @param bool $localize
+ *
+ * @return array
+ */
+ public static function indices($localize = TRUE) {
+ $indices = [
+ 'index_session_id' => [
+ 'name' => 'index_session_id',
+ 'field' => [
+ 0 => 'session_id',
+ ],
+ 'localizable' => FALSE,
+ 'unique' => TRUE,
+ 'sig' => 'civicrm_session::1::session_id',
+ ],
+ ];
+ return ($localize && !empty($indices)) ? CRM_Core_DAO_AllCoreTables::multilingualize(__CLASS__, $indices) : $indices;
+ }
+
+}
// return TRUE;
// }
+ /**
+ * Create table civicrm_session
+ *
+ * @return TRUE on success
+ * @throws Exception
+ */
+ public function upgrade_5691(): bool {
+ $this->ctx->log->info('Applying update 5691');
+ $this->executeSqlFile('sql/upgrade_5691.sql');
+ return TRUE;
+ }
+
}
--- /dev/null
+<?php
+namespace Civi\Api4;
+
+/**
+ * Session entity.
+ *
+ * Provided by the Standalone Users extension.
+ *
+ * @package Civi\Api4
+ */
+class Session extends Generic\DAOEntity {
+
+}
*/
public function logoutSession() {
\CRM_Core_Session::singleton()->reset();
+ session_destroy();
}
/**
--- /dev/null
+<?php
+
+namespace Civi\Standalone;
+
+use CRM_Standaloneusers_BAO_Session as Session;
+use DB;
+use SessionHandlerInterface;
+use SessionIdInterface;
+use SessionUpdateTimestampHandlerInterface;
+
+class SessionHandler implements SessionHandlerInterface, SessionIdInterface, SessionUpdateTimestampHandlerInterface {
+
+ /**
+ * @var bool
+ */
+ private $collectGarbage = FALSE;
+
+ /**
+ * @var string
+ */
+ private $data;
+
+ /**
+ * @var \DB_mysqli
+ */
+ private $db;
+
+ /**
+ * @var int
+ */
+ private $maxLifetime;
+
+ /**
+ * Closes the current session. This function is automatically executed when
+ * closing the session, or explicitly via session_write_close()
+ *
+ * @return bool
+ */
+ public function close(): bool {
+ $this->db->commit();
+ $this->db->autoCommit(TRUE);
+
+ if ($this->collectGarbage) {
+ $expiration_date = date('Y-m-d H:i:s', strtotime("- {$this->maxLifetime} seconds"));
+ Session::deleteExpired($this->db, $expiration_date);
+ }
+
+ $this->collectGarbage = FALSE;
+ unset($this->data);
+
+ return TRUE;
+ }
+
+ /**
+ * Create a unique session ID
+ *
+ * @return string
+ */
+ public function create_sid(): string {
+ return bin2hex(random_bytes(32));
+ }
+
+ /**
+ * Destroys a session. Called by session_regenerate_id() (with $destroy = true),
+ * session_destroy() and when session_decode() fails
+ *
+ * @param string $id
+ * @return bool
+ */
+ public function destroy($id): bool {
+ Session::destroy($this->db, $id);
+
+ return TRUE;
+ }
+
+ /**
+ * Cleans up expired sessions. Called by session_start(), based on
+ * session.gc_divisor, session.gc_probability and session.gc_maxlifetime settings
+ *
+ * @param int $max_lifetime
+ * @return int|false
+ */
+ public function gc($max_lifetime): int {
+ $this->collectGarbage = TRUE;
+ $this->maxLifetime = $max_lifetime;
+
+ return 0;
+ }
+
+ /**
+ * Re-initialize existing session, or creates a new one. Called when a session
+ * starts or when session_start() is invoked
+ *
+ * @param string $path
+ * @param string $name
+ * @return bool
+ */
+ public function open($path, $name): bool {
+ $this->db = DB::connect(\CRM_Core_Config::singleton()->dsn);
+ $this->db->autoCommit(FALSE);
+
+ return TRUE;
+ }
+
+ /**
+ * Reads the session data from the session storage, and returns the results.
+ * Called right after the session starts or when session_start() is called
+ *
+ * @param string $id
+ * @return string|false
+ */
+ public function read($id): string {
+ return $this->data ?? '';
+ }
+
+ /**
+ * Updates the last modification timestamp of the session. This function is
+ * automatically executed when a session is updated.
+ *
+ * @param string $id
+ * @param string $data
+ * @return bool
+ */
+ public function updateTimestamp($id, $data): bool {
+ Session::write($this->db, $id);
+
+ return TRUE;
+ }
+
+ /**
+ * Validates a given session ID. A session ID is valid, if a session with that
+ * ID already exists. This function is automatically executed when a session
+ * is to be started, a session ID is supplied and session.use_strict_mode is enabled.
+ *
+ * @param string $id
+ * @return bool
+ */
+ public function validateId($id): bool {
+ $session = Session::read($this->db, $id);
+
+ if (is_null($session)) {
+ return FALSE;
+ }
+
+ $this->data = $session['data'];
+ $max_lifetime = \Civi::settings()->get('standaloneusers_session_max_lifetime');
+
+ return strtotime($session['last_accessed']) >= strtotime("-$maxLifetime seconds");
+ }
+
+ /**
+ * Writes the session data to the session storage. Called by session_write_close(),
+ * when session_register_shutdown() fails, or during a normal shutdown.
+ *
+ * @param string $id
+ * @param string $data
+ * @return bool
+ */
+ public function write($id, $data): bool {
+ Session::write($this->db, $id, $data);
+
+ return TRUE;
+ }
+
+}
--- /dev/null
+<?php
+
+return [
+ 'standaloneusers_session_max_lifetime' => [
+ 'name' => 'standaloneusers_session_max_lifetime',
+ 'type' => 'Integer',
+ 'title' => ts('Maxiumum Session Lifetime'),
+ 'description' => ts('Duration (in seconds) until a user session expires'),
+ // 24 days (= Drupal default)
+ 'default' => 24 * 24 * 60 * 60,
+ 'html_type' => 'text',
+ 'is_domain' => 1,
+ 'is_contact' => 0,
+ ],
+];
SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS `civicrm_uf_match`;
+DROP TABLE IF EXISTS `civicrm_session`;
DROP TABLE IF EXISTS `civicrm_role`;
SET FOREIGN_KEY_CHECKS=1;
)
ENGINE=InnoDB;
+-- /*******************************************************
+-- *
+-- * civicrm_session
+-- *
+-- * Standalone User Sessions
+-- *
+-- *******************************************************/
+CREATE TABLE `civicrm_session` (
+ `id` int NOT NULL AUTO_INCREMENT COMMENT 'Unique Session ID',
+ `session_id` char(64) NOT NULL COMMENT 'Hexadecimal Session Identifier',
+ `data` longtext COMMENT 'Session Data',
+ `last_accessed` datetime COMMENT 'Timestamp of the last session access',
+ PRIMARY KEY (`id`),
+ UNIQUE INDEX `index_session_id`(session_id)
+)
+ENGINE=InnoDB;
+
-- /*******************************************************
-- *
-- * civicrm_uf_match
SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS `civicrm_uf_match`;
+DROP TABLE IF EXISTS `civicrm_session`;
DROP TABLE IF EXISTS `civicrm_role`;
SET FOREIGN_KEY_CHECKS=1;
\ No newline at end of file
--- /dev/null
+CREATE TABLE `civicrm_session` (
+ `id` int NOT NULL AUTO_INCREMENT COMMENT 'Unique Session ID',
+ `session_id` char(64) NOT NULL COMMENT 'Hexadecimal Session Identifier',
+ `data` longtext COMMENT 'Session Data',
+ `last_accessed` datetime COMMENT 'Timestamp of the last session access',
+ PRIMARY KEY (`id`),
+ UNIQUE INDEX `index_session_id`(session_id)
+)
+ENGINE=InnoDB;
--- /dev/null
+<?php
+
+namespace Civi\Standalone;
+
+use Civi\Api4;
+use Civi\Test\EndToEndInterface;
+use PHPUnit\Framework\TestCase;
+
+/**
+ * @group e2e
+ */
+class SessionHandlerTest extends TestCase implements EndToEndInterface {
+
+ public function setUp(): void {
+ parent::setUp();
+
+ if (CIVICRM_UF !== 'Standalone') {
+ $this->markTestSkipped('Test only applies on Standalone');
+ }
+ }
+
+ public function tearDown(): void {
+ Api4\Session::delete(FALSE)
+ ->addWhere('id', '>', 0)
+ ->execute();
+
+ parent::tearDown();
+ }
+
+ public function testHandler(): void {
+ $session_handler = new SessionHandler();
+
+ // Open a session
+ $session_handler->open('/path', 'CIVISOSESSID');
+
+ // Create a unique session ID
+ $session_id = $session_handler->create_sid();
+ $this->assertEquals(64, strlen($session_id));
+
+ // Try to read data from the session
+ $data = $session_handler->read($session_id);
+ // Should return an empty string because the session doesn't exist yet
+ $this->assertEquals('', $data);
+
+ // Write to the session
+ $session_handler->write($session_id, 'CiviCRM|a:1:{s:4:"ufID";i:1}');
+
+ // Close the session
+ $session_handler->close();
+
+ // Check the stored session
+ $session = self::getSession($session_id);
+ $this->assertIsArray($session);
+ $this->assertEquals('CiviCRM|a:1:{s:4:"ufID";i:1}', $session['data']);
+
+ $original_timestamp = $session['last_accessed'];
+
+ sleep(1);
+
+ // Re-open the session
+ $session_handler->open('/path', 'CIVISOSESSID');
+
+ // Validate the session
+ $this->assertTrue($session_handler->validateId($session_id));
+
+ // Read from the session
+ $data = $session_handler->read($session_id);
+ $this->assertEquals('CiviCRM|a:1:{s:4:"ufID";i:1}', $data, 'Session should have stored data');
+
+ // Update the session timestamp
+ $session_handler->updateTimestamp($session_id, 'CiviCRM|a:1:{s:4:"ufID";i:1}');
+
+ // Close the session
+ $session_handler->close();
+
+ // Check the stored session
+ $session = self::getSession($session_id);
+ $this->assertIsArray($session);
+ $this->assertEquals('CiviCRM|a:1:{s:4:"ufID";i:1}', $session['data']);
+
+ $updated_timestamp = $session['last_accessed'];
+
+ $this->assertGreaterThan(
+ strtotime($original_timestamp),
+ strtotime($updated_timestamp),
+ 'Session timestamp should have been updated'
+ );
+
+ sleep(1);
+
+ // Re-open the session
+ $session_handler->open('/path', 'CIVISOSESSID');
+
+ // Validate the session
+ $this->assertTrue($session_handler->validateId($session_id));
+
+ // Read from the session
+ $data = $session_handler->read($session_id);
+ $this->assertEquals('CiviCRM|a:1:{s:4:"ufID";i:1}', $data, 'Session data should not have changed');
+
+ // Destroy the session
+ $session_handler->destroy($session_id);
+
+ // Close the session
+ $session_handler->close();
+
+ // Check the stored session
+ $session = self::getSession($session_id);
+ $this->assertNull($session, 'Session should have been deleted');
+ }
+
+ public function testGarbageCollection() {
+ $session_handler = new SessionHandler();
+ $old_sid = $session_handler->create_sid();
+
+ // Create an old (expired) session
+ Api4\Session::create(FALSE)
+ ->addValue('session_id', $old_sid)
+ ->addValue('data', 'CiviCRM|a:0:{}')
+ ->addValue('last_accessed', date('Y-m-d H:i:s', strtotime('-1 week')))
+ ->execute();
+
+ // Open a new session to trigger garbage collection
+ $session_handler->open('/path', 'CIVISOSESSID');
+ $new_sid = $session_handler->create_sid();
+ $session_handler->read($new_sid);
+
+ // Run garbage collection with a $max_lifetime of 24 hours
+ $session_handler->gc(60 * 60 * 24);
+
+ // Finish the current session lifecycle
+ $session_handler->write($new_sid, 'CiviCRM|a:0:{}');
+ $session_handler->close();
+
+ // The old session should now be gone
+ $old_session = self::getSession($old_sid);
+ $this->assertNull($old_session, 'Expired session should have been deleted');
+
+ // The new session should still be here
+ $new_session = self::getSession($new_sid, 'New session should not have been deleted');
+ $this->assertIsArray($new_session);
+ }
+
+ public function testTransaction() {
+ $session_handler = new SessionHandler();
+
+ // Start a transaction
+ $tx = new \CRM_Core_Transaction();
+
+ // Create a new session
+ $session_handler->open('/path', 'CIVISOSESSID');
+ $session_id = $session_handler->create_sid();
+ $session_handler->read($session_id);
+ $session_handler->write($session_id, 'CiviCRM|a:0:{}');
+ $session_handler->close();
+
+ // Re-open the session
+ $session_handler->open('/path', 'CIVISOSESSID');
+ $session_handler->validateId($session_id);
+ $session_handler->read($session_id);
+ $session_handler->write($session_id, 'CiviCRM|a:1:{s:4:"ufID";i:1}');
+
+ // Rollback the transaction
+ $tx->rollback();
+
+ // Continue with session management
+ $session_handler->close();
+
+ // Make sure the session has not been affected by the transaction rollback
+ $session = self::getSession($session_id);
+ $this->assertIsArray($session);
+ $this->assertEquals('CiviCRM|a:1:{s:4:"ufID";i:1}', $session['data']);
+ }
+
+ private static function getSession($session_id) {
+ return Api4\Session::get(FALSE)
+ ->addSelect('*')
+ ->addWhere('session_id', '=', $session_id)
+ ->execute()
+ ->first();
+ }
+
+}
--- /dev/null
+<?php
+// This file declares a new entity type. For more details, see "hook_civicrm_entityTypes" at:
+// https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_entityTypes
+return [
+ [
+ 'name' => 'Session',
+ 'class' => 'CRM_Standaloneusers_DAO_Session',
+ 'table' => 'civicrm_session',
+ ],
+];
--- /dev/null
+<?xml version="1.0" encoding="iso-8859-1" ?>
+
+<table>
+ <base>CRM/Standaloneusers</base>
+ <class>Session</class>
+ <name>civicrm_session</name>
+ <comment>Standalone User Sessions</comment>
+ <log>false</log>
+
+ <field>
+ <name>id</name>
+ <type>int</type>
+ <required>true</required>
+ <comment>Unique Session ID</comment>
+ <html>
+ <type>Text</type>
+ </html>
+ </field>
+ <primaryKey>
+ <name>id</name>
+ <autoincrement>true</autoincrement>
+ </primaryKey>
+
+ <field>
+ <name>session_id</name>
+ <type>char</type>
+ <length>64</length>
+ <required>true</required>
+ <comment>Hexadecimal Session Identifier</comment>
+ <html>
+ <type>Text</type>
+ </html>
+ </field>
+ <index>
+ <fieldName>session_id</fieldName>
+ <name>index_session_id</name>
+ <unique>true</unique>
+ </index>
+
+ <field>
+ <name>data</name>
+ <type>longtext</type>
+ <comment>Session Data</comment>
+ </field>
+
+ <field>
+ <name>last_accessed</name>
+ <type>datetime</type>
+ <comment>Timestamp of the last session access</comment>
+ </field>
+
+</table>