diff --git a/inc/class-cli-command.php b/inc/class-cli-command.php index 6d0a1625..411db60d 100644 --- a/inc/class-cli-command.php +++ b/inc/class-cli-command.php @@ -79,6 +79,21 @@ public function delete( $args, $assoc_args ) { } } + /** + * Run session garbage collection. + * + * @subcommand gc + */ + public function gc( $args, $assoc_args ) { + if ( ! PANTHEON_SESSIONS_ENABLED ) { + WP_CLI::error( 'Pantheon Sessions is currently disabled.' ); + } + + $pantheon_session = \Pantheon_Sessions::get_instance(); + $pantheon_session->garbage_collection(); + WP_CLI::success( 'Session garbage collection complete.' ); + } + /** * Set id as primary key in the Native PHP Sessions plugin table. * diff --git a/inc/class-session.php b/inc/class-session.php index 71325312..e76be7d5 100644 --- a/inc/class-session.php +++ b/inc/class-session.php @@ -87,6 +87,7 @@ public static function create_for_sid( $sid ) { $insert_data = [ 'session_id' => $sid, 'user_id' => (int) get_current_user_id(), + 'datetime' => gmdate( 'Y-m-d H:i:s' ), ]; if ( function_exists( 'is_ssl' ) && is_ssl() ) { $insert_data['secure_session_id'] = $sid; diff --git a/pantheon-sessions.php b/pantheon-sessions.php index d701092f..f0689917 100644 --- a/pantheon-sessions.php +++ b/pantheon-sessions.php @@ -60,10 +60,6 @@ private function load() { return; } - if ( defined( 'DOING_CRON' ) && DOING_CRON ) { - return; - } - $this->define_constants(); $this->require_files(); @@ -74,12 +70,30 @@ private function load() { $this->set_ini_values(); add_action( 'set_logged_in_cookie', [ __CLASS__, 'action_set_logged_in_cookie' ], 10, 4 ); add_action( 'clear_auth_cookie', [ __CLASS__, 'action_clear_auth_cookie' ] ); + + if ( ! wp_next_scheduled( 'pantheon_sessions_gc' ) ) { + wp_schedule_event( time(), 'hourly', 'pantheon_sessions_gc' ); + } + add_action( 'pantheon_sessions_gc', [ $this, 'garbage_collection' ] ); } add_action( 'admin_enqueue_scripts', [ $this, 'enqueue_scripts' ] ); add_action( 'wp_ajax_dismiss_notice', [ $this, 'dismiss_notice' ] ); } + /** + * Runs the garbage collection process. + * + * @param int $maxlifetime Maximum lifetime in seconds. + */ + public function garbage_collection( $maxlifetime = null ) { + if ( null === $maxlifetime ) { + $maxlifetime = (int) ini_get( 'session.gc_maxlifetime' ); + } + $handler = new \Pantheon_Sessions\Session_Handler(); + $handler->gc( $maxlifetime ); + } + /** * Enqueue scripts */ @@ -240,7 +254,7 @@ private function setup_database() { `session_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'A session ID. The value is generated by plugin''s session handlers.', `secure_session_id` varchar(128) NOT NULL DEFAULT '' COMMENT 'Secure session ID. The value is generated by plugin''s session handlers.', `ip_address` varchar(128) NOT NULL DEFAULT '' COMMENT 'The IP address that last used this session ID.', - `datetime` datetime DEFAULT NULL COMMENT 'The datetime value when this session last requested a page. Old records are purged by PHP automatically.', + `datetime` datetime DEFAULT NULL COMMENT 'The datetime value when this session was last written to.', `data` mediumblob COMMENT 'The serialized contents of \$_SESSION, an array of name/value pairs that persists across page requests by this session ID. Plugin loads \$_SESSION from here at the start of each request and saves it at the end.', KEY `session_id` (`session_id`), KEY `secure_session_id` (`secure_session_id`) diff --git a/readme.txt b/readme.txt index 1ce2b7be..1ad2d1ea 100644 --- a/readme.txt +++ b/readme.txt @@ -99,6 +99,7 @@ Adds a WP-CLI command to add an index to the sessions table if one does not exis == Changelog == = 1.4.6-dev = +* Run session garbage collection hourly through WordPress scheduled task. Create WP-CLI command to run GC manually = 1.4.5 (December 2025) = * Compatibility: Supports Wordpress 6.9 diff --git a/tests/phpunit/test-sessions.php b/tests/phpunit/test-sessions.php index 354e8336..1efa9a9b 100644 --- a/tests/phpunit/test-sessions.php +++ b/tests/phpunit/test-sessions.php @@ -46,6 +46,7 @@ public function setUp(): void { if ( ! Session::get_by_sid( session_id() ) ) { Session::create_for_sid( session_id() ); } + parent::setUp(); } @@ -126,23 +127,21 @@ public function test_session_sync_user_id_login_logout() { } /** - * Ensures the garbage collection function wroks as expected. + * Ensures the garbage collection function works as expected. */ public function test_session_garbage_collection() { - $this->markTestSkipped( 'ini_set() never works once headers have been set' ); - global $wpdb; $_SESSION['foo'] = 'bar'; session_commit(); $this->assertEquals( 1, $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->pantheon_sessions" ) ); - $current_val = ini_get( 'session.gc_maxlifetime' ); - ini_set( 'session.gc_maxlifetime', 100000000 ); - _pantheon_session_garbage_collection( ini_get( 'session.gc_maxlifetime' ) ); + + // Should NOT delete the session with a very long lifetime. + Pantheon_Sessions()->garbage_collection( 100000000 ); $this->assertEquals( 1, $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->pantheon_sessions" ) ); - ini_set( 'session.gc_maxlifetime', 0 ); - _pantheon_session_garbage_collection( ini_get( 'session.gc_maxlifetime' ) ); + + // Should delete the session with a lifetime of 0. + Pantheon_Sessions()->garbage_collection( 0 ); $this->assertEquals( 0, $wpdb->get_var( "SELECT COUNT(*) FROM $wpdb->pantheon_sessions" ) ); - ini_set( 'session.gc_maxlifetime', $current_val ); } /**