diff --git a/administrator/components/com_admin/sql/updates/mysql/3.8.6-2018-02-14.sql b/administrator/components/com_admin/sql/updates/mysql/3.8.6-2018-02-14.sql index 955e669aada97..b88087a13504f 100644 --- a/administrator/components/com_admin/sql/updates/mysql/3.8.6-2018-02-14.sql +++ b/administrator/components/com_admin/sql/updates/mysql/3.8.6-2018-02-14.sql @@ -1,3 +1,6 @@ +INSERT INTO `#__extensions` (`extension_id`, `package_id`, `name`, `type`, `element`, `folder`, `client_id`, `enabled`, `access`, `protected`, `manifest_cache`, `params`, `custom_data`, `system_data`, `checked_out`, `checked_out_time`, `ordering`, `state`) VALUES +(480, 0, 'plg_system_sessiongc', 'plugin', 'sessiongc', 'system', 0, 1, 1, 0, '', '', '', '', 0, '0000-00-00 00:00:00', 0, 0); + INSERT INTO `#__postinstall_messages` (`extension_id`, `title_key`, `description_key`, `action_key`, `language_extension`, `language_client_id`, `type`, `action_file`, `action`, `condition_file`, `condition_method`, `version_introduced`, `enabled`) VALUES (700, 'PLG_PLG_RECAPTCHA_VERSION_1_POSTINSTALL_TITLE', 'PLG_PLG_RECAPTCHA_VERSION_1_POSTINSTALL_BODY', 'PLG_PLG_RECAPTCHA_VERSION_1_POSTINSTALL_ACTION', 'plg_captcha_recaptcha', 1, 'action', 'site://plugins/captcha/recaptcha/postinstall/actions.php', 'recaptcha_postinstall_action', 'site://plugins/captcha/recaptcha/postinstall/actions.php', 'recaptcha_postinstall_condition', '3.8.6', 1); diff --git a/administrator/components/com_admin/sql/updates/postgresql/3.8.6-2018-02-14.sql b/administrator/components/com_admin/sql/updates/postgresql/3.8.6-2018-02-14.sql index 8c57ae40bb5fb..bd70f521d3872 100644 --- a/administrator/components/com_admin/sql/updates/postgresql/3.8.6-2018-02-14.sql +++ b/administrator/components/com_admin/sql/updates/postgresql/3.8.6-2018-02-14.sql @@ -1,3 +1,6 @@ +INSERT INTO "#__extensions" ("extension_id", "package_id", "name", "type", "element", "folder", "client_id", "enabled", "access", "protected", "manifest_cache", "params", "custom_data", "system_data", "checked_out", "checked_out_time", "ordering", "state") VALUES +(480, 0, 'plg_system_sessiongc', 'plugin', 'sessiongc', 'system', 0, 1, 1, 0, '', '', '', '', 0, '1970-01-01 00:00:00', 0, 0); + INSERT INTO "#__postinstall_messages" ("extension_id", "title_key", "description_key", "action_key", "language_extension", "language_client_id", "type", "action_file", "action", "condition_file", "condition_method", "version_introduced", "enabled") VALUES (700, 'PLG_PLG_RECAPTCHA_VERSION_1_POSTINSTALL_TITLE', 'PLG_PLG_RECAPTCHA_VERSION_1_POSTINSTALL_BODY', 'PLG_PLG_RECAPTCHA_VERSION_1_POSTINSTALL_ACTION', 'plg_captcha_recaptcha', 1, 'action', 'site://plugins/captcha/recaptcha/postinstall/actions.php', 'recaptcha_postinstall_action', 'site://plugins/captcha/recaptcha/postinstall/actions.php', 'recaptcha_postinstall_condition', '3.8.6', 1); diff --git a/administrator/components/com_admin/sql/updates/sqlazure/3.8.6-2018-02-14.sql b/administrator/components/com_admin/sql/updates/sqlazure/3.8.6-2018-02-14.sql index 8c57ae40bb5fb..29ad515aec05a 100644 --- a/administrator/components/com_admin/sql/updates/sqlazure/3.8.6-2018-02-14.sql +++ b/administrator/components/com_admin/sql/updates/sqlazure/3.8.6-2018-02-14.sql @@ -1,3 +1,6 @@ +INSERT INTO "#__extensions" ("extension_id", "package_id", "name", "type", "element", "folder", "client_id", "enabled", "access", "protected", "manifest_cache", "params", "custom_data", "system_data", "checked_out", "checked_out_time", "ordering", "state") VALUES +(480, 0, 'plg_system_sessiongc', 'plugin', 'sessiongc', 'system', 0, 1, 1, 0, '', '', '', '', 0, '1900-01-01 00:00:00', 0, 0); + INSERT INTO "#__postinstall_messages" ("extension_id", "title_key", "description_key", "action_key", "language_extension", "language_client_id", "type", "action_file", "action", "condition_file", "condition_method", "version_introduced", "enabled") VALUES (700, 'PLG_PLG_RECAPTCHA_VERSION_1_POSTINSTALL_TITLE', 'PLG_PLG_RECAPTCHA_VERSION_1_POSTINSTALL_BODY', 'PLG_PLG_RECAPTCHA_VERSION_1_POSTINSTALL_ACTION', 'plg_captcha_recaptcha', 1, 'action', 'site://plugins/captcha/recaptcha/postinstall/actions.php', 'recaptcha_postinstall_action', 'site://plugins/captcha/recaptcha/postinstall/actions.php', 'recaptcha_postinstall_condition', '3.8.6', 1); diff --git a/administrator/language/en-GB/en-GB.plg_system_sessiongc.ini b/administrator/language/en-GB/en-GB.plg_system_sessiongc.ini new file mode 100644 index 0000000000000..6f8879115a291 --- /dev/null +++ b/administrator/language/en-GB/en-GB.plg_system_sessiongc.ini @@ -0,0 +1,15 @@ +; Joomla! Project +; Copyright (C) 2005 - 2018 Open Source Matters. All rights reserved. +; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php +; Note : All ini files need to be saved as UTF-8 + +PLG_SYSTEM_SESSIONGC="System - Session Data Purge" +PLG_SYSTEM_SESSIONGC_ENABLE_SESSION_GC_DESC="When enabled, this plugin will attempt to purge expired data based on the frequency calculated by the probability and divisor." +PLG_SYSTEM_SESSIONGC_ENABLE_SESSION_GC_LABEL="Enable Session Data Cleanup" +PLG_SYSTEM_SESSIONGC_ENABLE_SESSION_METADATA_GC_DESC="When enabled, this plugin will clean optional session metadata from the database. Note that this operation will not run when the database handler is in use as that data is cleared as part of the Session Data Cleanup operation." +PLG_SYSTEM_SESSIONGC_ENABLE_SESSION_METADATA_GC_LABEL="Enable Session Metadata Cleanup" +PLG_SYSTEM_SESSIONGC_GC_DIVISOR_DESC="In combination with the probability field, these two fields are used to determine the frequency of the session data cleanup operation being triggered on a request. The probability is calculated by using probability/divisor, e.g. 1/100 means there is a 1% chance that the process runs on each request." +PLG_SYSTEM_SESSIONGC_GC_DIVISOR_LABEL="Divisor" +PLG_SYSTEM_SESSIONGC_GC_PROBABILITY_DESC="In combination with the divisor field, these two fields are used to determine the frequency of the session data cleanup operation being triggered on a request." +PLG_SYSTEM_SESSIONGC_GC_PROBABILITY_LABEL="Probability" +PLG_SYSTEM_SESSIONGC_XML_DESCRIPTION="System Plugin that purges expired data and metadata depending on the session handler set in Global Configuration." diff --git a/administrator/language/en-GB/en-GB.plg_system_sessiongc.sys.ini b/administrator/language/en-GB/en-GB.plg_system_sessiongc.sys.ini new file mode 100644 index 0000000000000..415d42e49971b --- /dev/null +++ b/administrator/language/en-GB/en-GB.plg_system_sessiongc.sys.ini @@ -0,0 +1,7 @@ +; Joomla! Project +; Copyright (C) 2005 - 2018 Open Source Matters. All rights reserved. +; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php +; Note : All ini files need to be saved as UTF-8 + +PLG_SYSTEM_SESSIONGC="System - Session Data Purge" +PLG_SYSTEM_SESSIONGC_XML_DESCRIPTION="System Plugin that purges expired data and metadata depending on the session handler set in Global Configuration." diff --git a/cli/sessionMetadataGc.php b/cli/sessionMetadataGc.php new file mode 100644 index 0000000000000..aa2007b9dea72 --- /dev/null +++ b/cli/sessionMetadataGc.php @@ -0,0 +1,59 @@ +getExpire(); + + $metadataManager->deletePriorTo(time() - $sessionExpire); + } +} + +JApplicationCli::getInstance('SessionMetadataGc')->execute(); diff --git a/installation/sql/mysql/joomla.sql b/installation/sql/mysql/joomla.sql index ceeef4b235a1f..72dddaa5bb888 100644 --- a/installation/sql/mysql/joomla.sql +++ b/installation/sql/mysql/joomla.sql @@ -642,6 +642,7 @@ INSERT INTO `#__extensions` (`extension_id`, `package_id`, `name`, `type`, `elem (477, 0, 'plg_content_fields', 'plugin', 'fields', 'content', 0, 1, 1, 0, '', '', '', '', 0, '0000-00-00 00:00:00', 0, 0), (478, 0, 'plg_editors-xtd_fields', 'plugin', 'fields', 'editors-xtd', 0, 1, 1, 0, '', '', '', '', 0, '0000-00-00 00:00:00', 0, 0), (479, 0, 'plg_sampledata_blog', 'plugin', 'blog', 'sampledata', 0, 1, 1, 0, '', '', '', '', 0, '0000-00-00 00:00:00', 0, 0), +(480, 0, 'plg_system_sessiongc', 'plugin', 'sessiongc', 'system', 0, 1, 1, 0, '', '', '', '', 0, '0000-00-00 00:00:00', 0, 0), (503, 0, 'beez3', 'template', 'beez3', '', 0, 1, 1, 0, '', '{"wrapperSmall":"53","wrapperLarge":"72","sitetitle":"","sitedescription":"","navposition":"center","templatecolor":"nature"}', '', '', 0, '0000-00-00 00:00:00', 0, 0), (504, 0, 'hathor', 'template', 'hathor', '', 1, 1, 1, 0, '', '{"showSiteName":"0","colourChoice":"0","boldText":"0"}', '', '', 0, '0000-00-00 00:00:00', 0, 0), (506, 0, 'protostar', 'template', 'protostar', '', 0, 1, 1, 0, '', '{"templateColor":"","logoFile":"","googleFont":"1","googleFontName":"Open+Sans","fluidContainer":"0"}', '', '', 0, '0000-00-00 00:00:00', 0, 0), diff --git a/installation/sql/postgresql/joomla.sql b/installation/sql/postgresql/joomla.sql index 6ac7c372043de..d5686a2e6f61c 100644 --- a/installation/sql/postgresql/joomla.sql +++ b/installation/sql/postgresql/joomla.sql @@ -656,6 +656,7 @@ INSERT INTO "#__extensions" ("extension_id", "package_id", "name", "type", "elem (477, 0, 'plg_content_fields', 'plugin', 'fields', 'content', 0, 1, 1, 0, '', '', '', '', 0, '1970-01-01 00:00:00', 0, 0), (478, 0, 'plg_editors-xtd_fields', 'plugin', 'fields', 'editors-xtd', 0, 1, 1, 0, '', '', '', '', 0, '1970-01-01 00:00:00', 0, 0), (479, 0, 'plg_sampledata_blog', 'plugin', 'blog', 'sampledata', 0, 1, 1, 0, '', '', '', '', 0, '1970-01-01 00:00:00', 0, 0), +(480, 0, 'plg_system_sessiongc', 'plugin', 'sessiongc', 'system', 0, 1, 1, 0, '', '', '', '', 0, '1970-01-01 00:00:00', 0, 0), (503, 0, 'beez3', 'template', 'beez3', '', 0, 1, 1, 0, '', '{"wrapperSmall":"53","wrapperLarge":"72","sitetitle":"","sitedescription":"","navposition":"center","templatecolor":"nature"}', '', '', 0, '1970-01-01 00:00:00', 0, 0), (504, 0, 'hathor', 'template', 'hathor', '', 1, 1, 1, 0, '', '{"showSiteName":"0","colourChoice":"0","boldText":"0"}', '', '', 0, '1970-01-01 00:00:00', 0, 0), (506, 0, 'protostar', 'template', 'protostar', '', 0, 1, 1, 0, '', '{"templateColor":"","logoFile":"","googleFont":"1","googleFontName":"Open+Sans","fluidContainer":"0"}', '', '', 0, '1970-01-01 00:00:00', 0, 0), diff --git a/installation/sql/sqlazure/joomla.sql b/installation/sql/sqlazure/joomla.sql index 2815ea5c573ef..06cba75faa2b8 100644 --- a/installation/sql/sqlazure/joomla.sql +++ b/installation/sql/sqlazure/joomla.sql @@ -871,6 +871,7 @@ INSERT INTO "#__extensions" ("extension_id", "package_id", "name", "type", "elem (477, 0, 'plg_content_fields', 'plugin', 'fields', 'content', 0, 1, 1, 0, '', '', '', '', 0, '1900-01-01 00:00:00', 0, 0), (478, 0, 'plg_editors-xtd_fields', 'plugin', 'fields', 'editors-xtd', 0, 1, 1, 0, '', '', '', '', 0, '1900-01-01 00:00:00', 0, 0), (479, 0, 'plg_sampledata_blog', 'plugin', 'blog', 'sampledata', 0, 1, 1, 0, '', '', '', '', 0, '1900-01-01 00:00:00', 0, 0), +(480, 0, 'plg_system_sessiongc', 'plugin', 'sessiongc', 'system', 0, 1, 1, 0, '', '', '', '', 0, '1900-01-01 00:00:00', 0, 0), (503, 0, 'beez3', 'template', 'beez3', '', 0, 1, 1, 0, '', '{"wrapperSmall":"53","wrapperLarge":"72","sitetitle":"","sitedescription":"","navposition":"center","templatecolor":"nature"}', '', '', 0, '1900-01-01 00:00:00', 0, 0), (504, 0, 'hathor', 'template', 'hathor', '', 1, 1, 1, 0, '', '{"showSiteName":"0","colourChoice":"0","boldText":"0"}', '', '', 0, '1900-01-01 00:00:00', 0, 0), (506, 0, 'protostar', 'template', 'protostar', '', 0, 1, 1, 0, '', '{"templateColor":"","logoFile":"","googleFont":"1","googleFontName":"Open+Sans","fluidContainer":"0"}', '', '', 0, '1900-01-01 00:00:00', 0, 0), diff --git a/libraries/src/Application/CMSApplication.php b/libraries/src/Application/CMSApplication.php index 5d7409602d9e3..ef13f58fcd1e3 100644 --- a/libraries/src/Application/CMSApplication.php +++ b/libraries/src/Application/CMSApplication.php @@ -11,6 +11,7 @@ defined('JPATH_PLATFORM') or die; use Joomla\CMS\Input\Input; +use Joomla\CMS\Session\MetadataManager; use Joomla\Registry\Registry; /** @@ -62,6 +63,14 @@ class CMSApplication extends WebApplication */ protected $_messageQueue = array(); + /** + * The session metadata manager + * + * @var MetadataManager + * @since __DEPLOY_VERSION__ + */ + protected $metadataManager = null; + /** * The name of the application. * @@ -106,6 +115,8 @@ public function __construct(Input $input = null, Registry $config = null, \JAppl { parent::__construct($input, $config, $client); + $this->metadataManager = new MetadataManager($this, \JFactory::getDbo()); + // Load and set the dispatcher $this->loadDispatcher(); @@ -134,60 +145,6 @@ public function __construct(Input $input = null, Registry $config = null, \JAppl } } - /** - * After the session has been started we need to populate it with some default values. - * - * @return void - * - * @since 3.2 - */ - public function afterSessionStart() - { - $session = \JFactory::getSession(); - - if ($session->isNew()) - { - $session->set('registry', new Registry); - $session->set('user', new \JUser); - } - - // Get the session handler from the configuration. - $handler = $this->get('session_handler', 'none'); - - $time = time(); - - // If the database session handler is not in use and the current time is a divisor of 5, purge session metadata after the response is sent - if ($handler !== 'database' && $time % 5 === 0) - { - $this->registerEvent( - 'onAfterRespond', - function () use ($session, $time) - { - // TODO: At some point we need to get away from having session data always in the db. - $db = \JFactory::getDbo(); - - $query = $db->getQuery(true) - ->delete($db->quoteName('#__session')) - ->where($db->quoteName('time') . ' < ' . $db->quote((int) ($time - $session->getExpire()))); - - $db->setQuery($query); - - try - { - $db->execute(); - } - catch (\JDatabaseExceptionExecuting $exception) - { - /* - * The database API logs errors on failures so we don't need to add any error handling mechanisms here. - * Since garbage collection does not result in a fatal error when run in the session API, we don't allow it here either. - */ - } - } - ); - } - } - /** * Checks the user session. * @@ -201,63 +158,7 @@ function () use ($session, $time) */ public function checkSession() { - $db = \JFactory::getDbo(); - $session = \JFactory::getSession(); - $user = \JFactory::getUser(); - - $query = $db->getQuery(true) - ->select($db->quoteName('session_id')) - ->from($db->quoteName('#__session')) - ->where($db->quoteName('session_id') . ' = ' . $db->quote($session->getId())); - - $db->setQuery($query, 0, 1); - $exists = $db->loadResult(); - - // If the session record doesn't exist initialise it. - if (!$exists) - { - $query->clear(); - - $time = $session->isNew() ? time() : $session->get('session.timer.start'); - - $columns = array( - $db->quoteName('session_id'), - $db->quoteName('guest'), - $db->quoteName('time'), - $db->quoteName('userid'), - $db->quoteName('username'), - ); - - $values = array( - $db->quote($session->getId()), - (int) $user->guest, - $db->quote((int) $time), - (int) $user->id, - $db->quote($user->username), - ); - - if (!$this->get('shared_session', '0')) - { - $columns[] = $db->quoteName('client_id'); - $values[] = (int) $this->getClientId(); - } - - $query->insert($db->quoteName('#__session')) - ->columns($columns) - ->values(implode(', ', $values)); - - $db->setQuery($query); - - // If the insert failed, exit the application. - try - { - $db->execute(); - } - catch (\RuntimeException $e) - { - throw new \RuntimeException(\JText::_('JERROR_SESSION_STARTUP'), $e->getCode(), $e); - } - } + $this->metadataManager->createRecordIfNonExisting(\JFactory::getSession(), \JFactory::getUser()); } /** diff --git a/libraries/src/Session/MetadataManager.php b/libraries/src/Session/MetadataManager.php new file mode 100644 index 0000000000000..4ca837bc07d22 --- /dev/null +++ b/libraries/src/Session/MetadataManager.php @@ -0,0 +1,160 @@ +app = $app; + $this->db = $db; + } + + /** + * Create the metadata record if it does not exist. + * + * @param Session $session The session to create the metadata record for. + * @param User $user The user to associate with the record. + * + * @return void + * + * @since __DEPLOY_VERSION__ + * @throws \RuntimeException + */ + public function createRecordIfNonExisting(Session $session, User $user) + { + $query = $this->db->getQuery(true) + ->select($this->db->quoteName('session_id')) + ->from($this->db->quoteName('#__session')) + ->where($this->db->quoteName('session_id') . ' = ' . $this->db->quote($session->getId())); + + $this->db->setQuery($query, 0, 1); + $exists = $this->db->loadResult(); + + // If the session record doesn't exist initialise it. + if ($exists) + { + return; + } + + $query->clear(); + + $time = $session->isNew() ? time() : $session->get('session.timer.start'); + + $columns = array( + $this->db->quoteName('session_id'), + $this->db->quoteName('guest'), + $this->db->quoteName('time'), + $this->db->quoteName('userid'), + $this->db->quoteName('username'), + ); + + $values = array( + $this->db->quote($session->getId()), + (int) $user->guest, + $this->db->quote((int) $time), + (int) $user->id, + $this->db->quote($user->username), + ); + + if ($this->app instanceof CMSApplication && !$this->app->get('shared_session', '0')) + { + $columns[] = $this->db->quoteName('client_id'); + $values[] = (int) $this->app->getClientId(); + } + + $query->insert($this->db->quoteName('#__session')) + ->columns($columns) + ->values(implode(', ', $values)); + + $this->db->setQuery($query); + + try + { + $this->db->execute(); + } + catch (\RuntimeException $e) + { + /* + * Because of how our session handlers are structured, we must abort the request if this insert query fails, + * especially in the case of the database handler which does not support "INSERT or UPDATE" logic. With the + * change to the `joomla/session` Framework package in 4.0, where the required logic is implemented in the + * handlers, we can change this catch block so that the error is gracefully handled and does not result + * in a fatal error for the request. + */ + throw new \RuntimeException(\JText::_('JERROR_SESSION_STARTUP'), $e->getCode(), $e); + } + } + + /** + * Delete records with a timestamp prior to the given time. + * + * @param integer $time The time records should be deleted if expired before. + * + * @return void + * + * @since __DEPLOY_VERSION__ + */ + public function deletePriorTo($time) + { + $query = $this->db->getQuery(true) + ->delete($this->db->quoteName('#__session')) + ->where($this->db->quoteName('time') . ' < ' . $this->db->quote($time)); + + $this->db->setQuery($query); + + try + { + $this->db->execute(); + } + catch (\JDatabaseExceptionExecuting $exception) + { + /* + * The database API logs errors on failures so we don't need to add any error handling mechanisms here. + * Since garbage collection does not result in a fatal error when run in the session API, we don't allow it here either. + */ + } + } +} diff --git a/plugins/system/sessiongc/sessiongc.php b/plugins/system/sessiongc/sessiongc.php new file mode 100644 index 0000000000000..3693fb4daa286 --- /dev/null +++ b/plugins/system/sessiongc/sessiongc.php @@ -0,0 +1,78 @@ +params->get('enable_session_gc', 1)) + { + $probability = $this->params->get('gc_probability', 1); + $divisor = $this->params->get('gc_divisor', 100); + + $random = $divisor * lcg_value(); + + if ($probability > 0 && $random < $probability) + { + $session->gc(); + } + } + + if ($this->app->get('session_handler', 'none') !== 'database' && $this->params->get('enable_session_metadata_gc', 1)) + { + $probability = $this->params->get('gc_probability', 1); + $divisor = $this->params->get('gc_divisor', 100); + + $random = $divisor * lcg_value(); + + if ($probability > 0 && $random < $probability) + { + $metadataManager = new MetadataManager($this->app, $this->db); + $metadataManager->deletePriorTo(time() - $session->getExpire()); + } + } + } +} diff --git a/plugins/system/sessiongc/sessiongc.xml b/plugins/system/sessiongc/sessiongc.xml new file mode 100644 index 0000000000000..f9d9886169158 --- /dev/null +++ b/plugins/system/sessiongc/sessiongc.xml @@ -0,0 +1,74 @@ + + + plg_system_sessiongc + Joomla! Project + February 2018 + Copyright (C) 2005 - 2018 Open Source Matters. All rights reserved. + GNU General Public License version 2 or later; see LICENSE.txt + admin@joomla.org + www.joomla.org + __DEPLOY_VERSION__ + PLG_SYSTEM_SESSIONGC_XML_DESCRIPTION + + sessiongc.php + + + en-GB/en-GB.plg_system_sessiongc.ini + en-GB/en-GB.plg_system_sessiongc.sys.ini + + + +
+ + + + + + + + + + + + + +
+
+
+