diff --git a/CHANGELOG.md b/CHANGELOG.md index a6f94f13e..d9116a008 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ This file includes only changes we consider noteworthy for users, admins and plu - Password: Removed the (insecure) virtualmin driver (#8007) - Fix jqueryui plugin's minicolors.css issue with custom skins (#9967) - Fix `skin_logo` with a relative URL (#10030) +- Replace session attribute `changed` by `expires_at` to allow for variable session lengths per-user. ## Release 1.7-beta2 diff --git a/SQL/mysql.initial.sql b/SQL/mysql.initial.sql index bde2b728a..b0afca75f 100644 --- a/SQL/mysql.initial.sql +++ b/SQL/mysql.initial.sql @@ -7,11 +7,11 @@ SET FOREIGN_KEY_CHECKS=0; CREATE TABLE `session` ( `sess_id` varchar(128) NOT NULL, - `changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00', + `expires_at` datetime NOT NULL DEFAULT '1000-01-01 00:00:00', `ip` varchar(40) NOT NULL, `vars` mediumtext NOT NULL, PRIMARY KEY(`sess_id`), - INDEX `changed_index` (`changed`) + INDEX `expires_at_index` (`expires_at`) ) ROW_FORMAT=DYNAMIC ENGINE=INNODB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; @@ -272,4 +272,4 @@ CREATE TABLE `system` ( SET FOREIGN_KEY_CHECKS=1; -INSERT INTO `system` (`name`, `value`) VALUES ('roundcube-version', '2022100100'); +INSERT INTO `system` (`name`, `value`) VALUES ('roundcube-version', '2025092300'); diff --git a/SQL/mysql/2025092300.sql b/SQL/mysql/2025092300.sql new file mode 100644 index 000000000..fa88bc240 --- /dev/null +++ b/SQL/mysql/2025092300.sql @@ -0,0 +1,3 @@ +ALTER TABLE `session` RENAME COLUMN `changed` TO `expires_at`; +ALTER TABLE `session` RENAME INDEX `changed_index` TO `expires_at_index`; +UPDATE sessions SET expires_at = ADDTIME(expires_at, '00:10:00'); diff --git a/SQL/postgres.initial.sql b/SQL/postgres.initial.sql index 5a3bafd1d..4c506efe4 100644 --- a/SQL/postgres.initial.sql +++ b/SQL/postgres.initial.sql @@ -37,12 +37,12 @@ CREATE TABLE users ( CREATE TABLE "session" ( sess_id varchar(128) DEFAULT '' PRIMARY KEY, - changed timestamp with time zone DEFAULT now() NOT NULL, + expires_at timestamp with time zone DEFAULT now() NOT NULL, ip varchar(41) NOT NULL, vars text NOT NULL ); -CREATE INDEX session_changed_idx ON session (changed); +CREATE INDEX session_expires_at_idx ON session (expires_at); -- @@ -390,4 +390,4 @@ CREATE TABLE "system" ( value text ); -INSERT INTO "system" (name, value) VALUES ('roundcube-version', '2022100100'); +INSERT INTO "system" (name, value) VALUES ('roundcube-version', '2025092300'); diff --git a/SQL/postgres/2025092300.sql b/SQL/postgres/2025092300.sql new file mode 100644 index 000000000..8adf05317 --- /dev/null +++ b/SQL/postgres/2025092300.sql @@ -0,0 +1,3 @@ +ALTER TABLE `session` RENAME COLUMN `changed` TO `expires_at`; +ALTER TABLE `session` RENAME INDEX `session_changed_idx` TO `session_expires_at_idx`; +UPDATE sessions SET expires_at = expires_at + INTERVAL '10 minutes'; diff --git a/SQL/sqlite.initial.sql b/SQL/sqlite.initial.sql index 5634625d7..e6fe2f77f 100644 --- a/SQL/sqlite.initial.sql +++ b/SQL/sqlite.initial.sql @@ -124,12 +124,12 @@ CREATE INDEX ix_responses_user_id ON responses(user_id, del); CREATE TABLE session ( sess_id varchar(128) NOT NULL PRIMARY KEY, - changed datetime NOT NULL default '0000-00-00 00:00:00', + expires_at datetime NOT NULL default '0000-00-00 00:00:00', ip varchar(40) NOT NULL default '', vars text NOT NULL ); -CREATE INDEX ix_session_changed ON session (changed); +CREATE INDEX ix_session_expires_at ON session (expires_at); -- -- Table structure for table dictionary @@ -274,4 +274,4 @@ CREATE TABLE system ( value text NOT NULL ); -INSERT INTO system (name, value) VALUES ('roundcube-version', '2022100100'); +INSERT INTO system (name, value) VALUES ('roundcube-version', '2025092300'); diff --git a/SQL/sqlite/2025092300.sql b/SQL/sqlite/2025092300.sql new file mode 100644 index 000000000..ffd8d2151 --- /dev/null +++ b/SQL/sqlite/2025092300.sql @@ -0,0 +1,3 @@ +ALTER TABLE `session` RENAME COLUMN `changed` TO `expires_at`; +ALTER TABLE `session` RENAME INDEX `ix_session_changed` TO `ix_session_expires_at`; +UPDATE sessions SET expires_at = DATETIME(expires_at, '+10 minutes'); diff --git a/bin/gc.sh b/bin/gc.sh index 21f50a43b..7d0b97897 100755 --- a/bin/gc.sh +++ b/bin/gc.sh @@ -25,13 +25,12 @@ require INSTALL_PATH . 'program/include/clisetup.php'; $rcmail = rcube::get_instance(); $session_driver = $rcmail->config->get('session_storage', 'db'); -$session_lifetime = $rcmail->config->get('session_lifetime', 0) * 60 * 2; // Clean expired SQL sessions -if ($session_driver == 'db' && $session_lifetime) { +if ($session_driver == 'db') { $db = $rcmail->get_dbh(); $db->query('DELETE FROM ' . $db->table_name('session') - . ' WHERE changed < ' . $db->now(-$session_lifetime)); + . ' WHERE expires_at < ' . $db->now()); } // Clean caches and temp directory diff --git a/installer/test.php b/installer/test.php index 7a4baa6f9..1e8ebc501 100644 --- a/installer/test.php +++ b/installer/test.php @@ -202,7 +202,7 @@ if ($DB) { // write test $insert_id = md5(uniqid()); $db_write = $DB->query('INSERT INTO ' . $DB->quote_identifier($RCI->config['db_prefix'] . 'session') - . ' (`sess_id`, `changed`, `ip`, `vars`) VALUES (?, ' . $DB->now() . ", '127.0.0.1', 'foo')", $insert_id); + . ' (`sess_id`, `expires_at`, `ip`, `vars`) VALUES (?, ' . $DB->now() . ", '127.0.0.1', 'foo')", $insert_id); if ($db_write) { $RCI->pass('DB Write'); diff --git a/program/lib/Roundcube/rcube_session.php b/program/lib/Roundcube/rcube_session.php index b1c762a16..19586d444 100644 --- a/program/lib/Roundcube/rcube_session.php +++ b/program/lib/Roundcube/rcube_session.php @@ -29,7 +29,7 @@ abstract class rcube_session implements \SessionHandlerInterface protected $key; protected $ip; protected $cookie; - protected $changed; + protected $expires_at; protected $start; protected $vars; protected $now; diff --git a/program/lib/Roundcube/session/db.php b/program/lib/Roundcube/session/db.php index 68300ad79..2fc0d78ac 100644 --- a/program/lib/Roundcube/session/db.php +++ b/program/lib/Roundcube/session/db.php @@ -106,12 +106,12 @@ class rcube_session_db extends rcube_session public function read($key) { if ($this->lifetime) { - $expire_time = $this->db->now(-$this->lifetime); - $expire_check = "CASE WHEN `changed` < {$expire_time} THEN 1 ELSE 0 END AS expired"; + $expire_time = $this->db->now(); + $expire_check = "CASE WHEN `expires_at` < {$expire_time} THEN 1 ELSE 0 END AS expired"; } $sql_result = $this->db->query( - 'SELECT `vars`, `ip`, `changed`, ' . $this->db->now() . ' AS ts' + 'SELECT `vars`, `ip`, `expires_at`, ' . $this->db->now() . ' AS ts' . (isset($expire_check) ? ", {$expire_check}" : '') . " FROM {$this->table_name} WHERE `sess_id` = ?", $key ); @@ -125,7 +125,7 @@ class rcube_session_db extends rcube_session $time_diff = time() - strtotime($sql_arr['ts']); - $this->changed = strtotime($sql_arr['changed']) + $time_diff; // local (PHP) time + $this->expires_at = strtotime($sql_arr['expires_at']) + $time_diff; // local (PHP) time $this->ip = $sql_arr['ip']; $this->vars = base64_decode($sql_arr['vars']); $this->key = $key; @@ -153,11 +153,11 @@ class rcube_session_db extends rcube_session return true; } - $now = $this->db->now(); + $expires_at_str = $this->db->now($this->lifetime); $this->db->query("INSERT INTO {$this->table_name}" - . ' (`sess_id`, `vars`, `ip`, `changed`)' - . " VALUES (?, ?, ?, {$now})", + . ' (`sess_id`, `vars`, `ip`, `expires_at`)' + . " VALUES (?, ?, ?, {$expires_at_str})", $key, base64_encode($vars), (string) $this->ip ); @@ -176,17 +176,17 @@ class rcube_session_db extends rcube_session #[\Override] protected function update($key, $newvars, $oldvars) { - $now = $this->db->now(); + $expires_at_str = $this->db->now($this->lifetime); $ts = microtime(true); // if new and old data are not the same, update data // else update expire timestamp only when certain conditions are met if ($newvars !== $oldvars) { $this->db->query("UPDATE {$this->table_name} " - . "SET `changed` = {$now}, `vars` = ? WHERE `sess_id` = ?", + . "SET `expires_at` = {$expires_at_str}, `vars` = ? WHERE `sess_id` = ?", base64_encode($newvars), $key); - } elseif ($ts - $this->changed > $this->lifetime / 2) { - $this->db->query("UPDATE {$this->table_name} SET `changed` = {$now}" + } elseif ($this->expires_at - $ts < $this->lifetime / 2) { + $this->db->query("UPDATE {$this->table_name} SET `expires_at` = {$expires_at_str}" . ' WHERE `sess_id` = ?', $key); } @@ -200,10 +200,9 @@ class rcube_session_db extends rcube_session { // just clean all old sessions when this GC is called $this->db->query('DELETE FROM ' . $this->db->table_name('session') - . ' WHERE `changed` < ' . $this->db->now(-$this->gc_enabled)); + . ' WHERE `expires_at` < ' . $this->db->now()); - $this->log('Session GC (DB): remove records < ' - . date('Y-m-d H:i:s', time() - $this->gc_enabled) - . '; rows = ' . intval($this->db->affected_rows())); + $this->log('Session GC (DB): removed expired records; rows = ' + . intval($this->db->affected_rows())); } } diff --git a/program/lib/Roundcube/session/memcache.php b/program/lib/Roundcube/session/memcache.php index c68947b88..407ade633 100644 --- a/program/lib/Roundcube/session/memcache.php +++ b/program/lib/Roundcube/session/memcache.php @@ -114,7 +114,8 @@ class rcube_session_memcache extends rcube_session { if ($value = $this->memcache->get($key)) { $arr = unserialize($value); - $this->changed = $arr['changed']; + // Use a fallback to avoid errors in case expires_at is unset + $this->expires_at = $arr['expires_at'] ?? time(); $this->ip = $arr['ip']; $this->vars = $arr['vars']; $this->key = $key; @@ -142,7 +143,7 @@ class rcube_session_memcache extends rcube_session return true; } - $data = serialize(['changed' => time(), 'ip' => $this->ip, 'vars' => $vars]); + $data = serialize(['expires_at' => time() + $this->lifetime, 'ip' => $this->ip, 'vars' => $vars]); $result = $this->memcache->set($key, $data, \MEMCACHE_COMPRESSED, $this->lifetime + 60); if ($this->debug) { @@ -166,8 +167,8 @@ class rcube_session_memcache extends rcube_session { $ts = microtime(true); - if ($newvars !== $oldvars || $ts - $this->changed > $this->lifetime / 3) { - $data = serialize(['changed' => time(), 'ip' => $this->ip, 'vars' => $newvars]); + if ($newvars !== $oldvars || $this->expires_at - $ts > $this->lifetime / 3) { + $data = serialize(['expires_at' => time() + $this->lifetime, 'ip' => $this->ip, 'vars' => $newvars]); $result = $this->memcache->set($key, $data, \MEMCACHE_COMPRESSED, $this->lifetime + 60); if ($this->debug) { diff --git a/program/lib/Roundcube/session/memcached.php b/program/lib/Roundcube/session/memcached.php index 87e3c38f1..1d3a8f3b5 100644 --- a/program/lib/Roundcube/session/memcached.php +++ b/program/lib/Roundcube/session/memcached.php @@ -113,7 +113,8 @@ class rcube_session_memcached extends rcube_session public function read($key) { if ($arr = $this->memcache->get($key)) { - $this->changed = $arr['changed']; + // Use a fallback to avoid errors in case expires_at is unset + $this->expires_at = $arr['expires_at'] ?? time(); $this->ip = $arr['ip']; $this->vars = $arr['vars']; $this->key = $key; @@ -141,7 +142,7 @@ class rcube_session_memcached extends rcube_session return true; } - $data = ['changed' => time(), 'ip' => $this->ip, 'vars' => $vars]; + $data = ['expires_at' => time() + $this->lifetime, 'ip' => $this->ip, 'vars' => $vars]; $result = $this->memcache->set($key, $data, $this->lifetime + 60); if ($this->debug) { @@ -165,8 +166,8 @@ class rcube_session_memcached extends rcube_session { $ts = microtime(true); - if ($newvars !== $oldvars || $ts - $this->changed > $this->lifetime / 3) { - $data = ['changed' => time(), 'ip' => $this->ip, 'vars' => $newvars]; + if ($newvars !== $oldvars || $this->expires_at - $ts > $this->lifetime / 3) { + $data = ['expires_at' => time() + $this->lifetime, 'ip' => $this->ip, 'vars' => $newvars]; $result = $this->memcache->set($key, $data, $this->lifetime + 60); if ($this->debug) { diff --git a/program/lib/Roundcube/session/php.php b/program/lib/Roundcube/session/php.php index db26f18b8..851d3065e 100644 --- a/program/lib/Roundcube/session/php.php +++ b/program/lib/Roundcube/session/php.php @@ -97,6 +97,6 @@ class rcube_session_php extends rcube_session $this->key = session_id(); $this->ip = $_SESSION['__IP'] ?? null; - $this->changed = $_SESSION['__MTIME'] ?? null; + $this->expires_at = time() + $this->lifetime; } } diff --git a/program/lib/Roundcube/session/redis.php b/program/lib/Roundcube/session/redis.php index 1a125438c..e94557a58 100644 --- a/program/lib/Roundcube/session/redis.php +++ b/program/lib/Roundcube/session/redis.php @@ -128,7 +128,8 @@ class rcube_session_redis extends rcube_session if ($value) { $arr = unserialize($value); - $this->changed = $arr['changed']; + // Use a fallback to avoid errors in case expires_at is unset + $this->expires_at = $arr['expires_at'] ?? time(); $this->ip = $arr['ip']; $this->vars = $arr['vars']; $this->key = $key; @@ -151,8 +152,8 @@ class rcube_session_redis extends rcube_session { $ts = microtime(true); - if ($newvars !== $oldvars || $ts - $this->changed > $this->lifetime / 3) { - $data = serialize(['changed' => time(), 'ip' => $this->ip, 'vars' => $newvars]); + if ($newvars !== $oldvars || $this->expires_at - $ts > $this->lifetime / 3) { + $data = serialize(['expires_at' => time() + $this->lifetime, 'ip' => $this->ip, 'vars' => $newvars]); $result = false; try { @@ -190,7 +191,7 @@ class rcube_session_redis extends rcube_session $data = null; try { - $data = serialize(['changed' => time(), 'ip' => $this->ip, 'vars' => $vars]); + $data = serialize(['expires_at' => time() + $this->lifetime, 'ip' => $this->ip, 'vars' => $vars]); $result = $this->redis->setex($key, $this->lifetime + 60, $data); } catch (\Exception $e) { rcube::raise_error($e, true, true);