diff --git a/administrator/language/en-GB/en-GB.plg_system_stats.ini b/administrator/language/en-GB/en-GB.plg_system_stats.ini index dd908ac8af4d2..c9995e85d74c8 100644 --- a/administrator/language/en-GB/en-GB.plg_system_stats.ini +++ b/administrator/language/en-GB/en-GB.plg_system_stats.ini @@ -4,9 +4,23 @@ ; Note : All ini files need to be saved as UTF-8 PLG_SYSTEM_STATS="System - Joomla! Statistics" +PLG_SYSTEM_STATS_BTN_NEVER_SEND="Never" +PLG_SYSTEM_STATS_BTN_SEND_ALWAYS="Always" +PLG_SYSTEM_STATS_BTN_SEND_NOW="Once" +PLG_SYSTEM_STATS_DEBUG_DESC="Enable debug for testing purposes. Stats sent every page load." +PLG_SYSTEM_STATS_DEBUG_LABEL="Debug" +PLG_SYSTEM_STATS_INTERVAL_DESC="Stats will be sent each X hours. Default is 12" +PLG_SYSTEM_STATS_INTERVAL_LABEL="Interval (hours)" +PLG_SYSTEM_STATS_LABEL_MESSAGE_TITLE="Joomla! would like your permission collect some basic statistics." +PLG_SYSTEM_STATS_MODE_DESC="Selec the way you want that statistics are sent" +PLG_SYSTEM_STATS_MODE_LABEL="Mode" +PLG_SYSTEM_STATS_MODE_OPTION_ALWAYS_SEND="Always send" +PLG_SYSTEM_STATS_MODE_OPTION_NEVER_SEND="Never send" +PLG_SYSTEM_STATS_MODE_OPTION_ON_DEMAND="On demand" +PLG_SYSTEM_STATS_MSG_ALLOW_SENDING_DATA="Enable Joomla Statistics?" +PLG_SYSTEM_STATS_MSG_JOOMLA_WANTS_TO_SEND_DATA="In order to better understand our install base and end user environments, this plugin has been created to send those stats back to a Joomla controlled central server. No identifying data is captured at any point. You can change this settings later from Plugins > System Joomla! Statistics." +PLG_SYSTEM_STATS_MSG_WHAT_DATA_WILL_BE_SENT="Click here to see which information will be sent." PLG_SYSTEM_STATS_RESET_UNIQUE_ID="Reset Unique Id" PLG_SYSTEM_STATS_UNIQUE_ID_DESC="An identifier that allows the Joomla! project to count unique installs of the plugin. This is sent with the statistics back to the server." PLG_SYSTEM_STATS_UNIQUE_ID_LABEL="Unique ID" -PLG_SYSTEM_STATS_URL_DESC="The official Joomla server url" -PLG_SYSTEM_STATS_URL_LABEL="Url" PLG_SYSTEM_STATS_XML_DESCRIPTION="System Plugin that sends environment statistics to a server controlled by the Joomla! project for statistical analysis. Statistics sent include PHP version, CMS version, Database type, Database version and Server type." diff --git a/media/plg_system_stats/js/stats.js b/media/plg_system_stats/js/stats.js new file mode 100644 index 0000000000000..6604bfe84bcdc --- /dev/null +++ b/media/plg_system_stats/js/stats.js @@ -0,0 +1,78 @@ +(function ($) { + $(document).ready(function () { + var ajaxData = { + 'option' : 'com_ajax', + 'group' : 'system', + 'plugin' : 'renderStatsMessage', + 'format' : 'raw' + }, + messageContainer = $('#system-message-container'); + + /** + * Initialise events for the message container + * + * @return void + */ + function initStatsEvents() + { + var globalContainer = messageContainer.find('.js-pstats-alert'), + detailsContainer = messageContainer.find('.js-pstats-data-details'); + + // Show details about the information being sent + messageContainer.on('click', '.js-pstats-btn-details', function(e){ + detailsContainer.toggle(200); + e.preventDefault(); + }); + + // Always allow + messageContainer.on('click', '.js-pstats-btn-allow-always', function(e){ + + // Remove message + globalContainer.hide(200); + detailsContainer.remove(); + ajaxData.plugin = 'sendAlways'; + + $.getJSON('index.php', ajaxData, function(response){}); + e.preventDefault(); + }); + + // Allow once + messageContainer.on('click', '.js-pstats-btn-allow-once', function(e){ + + // Remove message + globalContainer.hide(200); + detailsContainer.remove(); + + ajaxData.plugin = 'sendOnce'; + + $.getJSON('index.php', ajaxData, function(response){}); + e.preventDefault(); + }); + + // Never allow + messageContainer.on('click', '.js-pstats-btn-allow-never', function(e){ + + // Remove message + globalContainer.hide(200); + detailsContainer.remove(); + + ajaxData.plugin = 'sendNever'; + + $.getJSON('index.php', ajaxData, function(response){}); + e.preventDefault(); + }); + } + + ajaxData.plugin = 'sendStats'; + + $.getJSON('index.php', ajaxData, function(response){ + if (response && response.html) { + messageContainer + .append(response.html) + .find('.js-pstats-alert').show(200); + + initStatsEvents(); + } + }); + }); +})(jQuery); diff --git a/plugins/system/stats/layouts/message.php b/plugins/system/stats/layouts/message.php new file mode 100644 index 0000000000000..88174f2dce31f --- /dev/null +++ b/plugins/system/stats/layouts/message.php @@ -0,0 +1,38 @@ + + diff --git a/plugins/system/stats/stats.php b/plugins/system/stats/stats.php index 25cbdb2360af4..718f868fc527b 100644 --- a/plugins/system/stats/stats.php +++ b/plugins/system/stats/stats.php @@ -9,9 +9,6 @@ defined('_JEXEC') or die; -// Uncomment the following line to enable debug mode (stats sent every single time) -// define('PLG_SYSTEM_STATS_DEBUG', 1); - /** * Statistics system plugin. This sends anonymous data back to the Joomla! Project about the * PHP, SQL, Joomla and OS versions @@ -20,6 +17,24 @@ */ class PlgSystemStats extends JPlugin { + /** + * @const integer + * @since 3.5 + */ + const MODE_ALLOW_ALWAYS = 1; + + /** + * @const integer + * @since 3.5 + */ + const MODE_ALLOW_ONCE = 2; + + /** + * @const integer + * @since 3.5 + */ + const MODE_ALLOW_NEVER = 3; + /** * Application object * @@ -28,6 +43,14 @@ class PlgSystemStats extends JPlugin */ protected $app; + /** + * Load plugin language files automatically + * + * @var boolean + * @since 3.5 + */ + protected $autoloadLanguage = true; + /** * Database object * @@ -36,6 +59,21 @@ class PlgSystemStats extends JPlugin */ protected $db; + /** + * Url to send the statistics. + * + * @var string + */ + protected $serverUrl = 'https://developer.joomla.org/stats/submit'; + + /** + * Unique identifier for this site + * + * @var string + * @since 3.5 + */ + protected $uniqueId; + /** * Listener for the `onAfterInitialise` event * @@ -45,91 +83,294 @@ class PlgSystemStats extends JPlugin */ public function onAfterInitialise() { - // Only run this in admin - if (!$this->app->isAdmin()) + if (!$this->app->isAdmin() || !$this->isAllowedUser()) { return; } - $uniqueId = $this->params->get('unique_id', ''); + if (!$this->isDebugEnabled() && !$this->isUpdateRequired()) + { + return; + } - /* - * If the unique ID is empty (because we have never submitted a piece of data before or because the refresh button - * has been used - generate a new ID and store it in the database for future use. - */ - if (empty($uniqueId)) + JHtml::_('jquery.framework'); + JHtml::script('plg_system_stats/stats.js', false, true, false); + } + + /** + * User selected to always send data + * + * @return void + * + * @since 3.5 + * + * @throws Exception If user is not allowed. + * @throws RuntimeException If there is an error saving the params or sending the data. + */ + public function onAjaxSendAlways() + { + if (!$this->isAllowedUser() || !$this->isAjaxRequest()) { - $uniqueId = hash('sha1', JUserHelper::genRandomPassword(28) . time()); - $this->params->set('unique_id', $uniqueId); + throw new Exception(JText::_('JGLOBAL_AUTH_ACCESS_DENIED'), 403); } - $last = (int) $this->params->get('lastrun', 0); + $this->params->set('mode', static::MODE_ALLOW_ALWAYS); - // What's the time? - $now = time(); + if (!$this->saveParams()) + { + throw new RuntimeException('Unable to save plugin settings', 500); + } + + $this->sendStats(); + + echo json_encode(array('sent' => 1)); + } - if ($last == 0) + /** + * User selected to never send data. + * + * @return void + * + * @since 3.5 + * + * @throws Exception If user is not allowed. + * @throws RuntimeException If there is an error saving the params. + */ + public function onAjaxSendNever() + { + if (!$this->isAllowedUser() || !$this->isAjaxRequest()) { - // This is the first run of the plugin, we save a last time some hours in the future - // It allows people to disable the plugin before data is send the first time - $this->params->set('lastrun', $now + 21600); + throw new Exception(JText::_('JGLOBAL_AUTH_ACCESS_DENIED'), 403); + } - $result = $this->saveParams(); + $this->params->set('mode', static::MODE_ALLOW_NEVER); - return; + if (!$this->saveParams()) + { + throw new RuntimeException('Unable to save plugin settings', 500); } - /* - * Do we need to run? Compare the last run timestamp stored in the plugin's options with the current - * timestamp. If the difference is greater than the cache timeout we shall not execute again. - * 12 hours - 60*60*12 = 43200 - */ - if (!defined('PLG_SYSTEM_STATS_DEBUG') && (abs($now - $last) < 43200)) + echo json_encode(array('sent' => 0)); + } + + /** + * User selected to send data once. + * + * @return void + * + * @since 3.5 + * + * @throws Exception If user is not allowed. + * @throws RuntimeException If there is an error saving the params or sending the data. + */ + public function onAjaxSendOnce() + { + if (!$this->isAllowedUser() || !$this->isAjaxRequest()) { - return; + throw new Exception(JText::_('JGLOBAL_AUTH_ACCESS_DENIED'), 403); } - // Update last run status - $this->params->set('lastrun', $now); + $this->params->set('mode', static::MODE_ALLOW_ONCE); - $result = $this->saveParams(); + if (!$this->saveParams()) + { + throw new RuntimeException('Unable to save plugin settings', 500); + } + + $this->sendStats(); + + echo json_encode(array('sent' => 1)); + } - // Abort on failure - if (!$result) + /** + * Send the stats to the server. + * On first load | on demand mode it will show a message asking users to select mode. + * + * @return void + * + * @since 3.5 + * + * @throws Exception If user is not allowed. + * @throws RuntimeException If there is an error saving the params or sending the data. + */ + public function onAjaxSendStats() + { + if (!$this->isAllowedUser() || !$this->isAjaxRequest()) { + throw new Exception(JText::_('JGLOBAL_AUTH_ACCESS_DENIED'), 403); + } + + // User has not selected the mode. Show message. + if ((int) $this->params->get('mode') !== static::MODE_ALLOW_ALWAYS) + { + $data = array( + 'sent' => 0, + 'html' => $this->getRenderer('message')->render($this->getLayoutData()) + ); + + echo json_encode($data); + return; } - $data = array( - 'unique_id' => $uniqueId, + if (!$this->saveParams()) + { + throw new RuntimeException('Unable to save plugin settings', 500); + } + + $this->sendStats(); + + echo json_encode(array('sent' => 1)); + } + + /** + * Get the data for the layout + * + * @return array + * + * @since 3.5 + */ + protected function getLayoutData() + { + return array( + 'plugin' => $this, + 'pluginParams' => $this->params, + 'statsData' => $this->getStatsData() + ); + } + + /** + * Get the layout paths + * + * @return array() + * + * @since 3.5 + */ + protected function getLayoutsPaths() + { + $template = JFactory::getApplication()->getTemplate(); + + return array( + JPATH_ADMINISTRATOR . '/templates/' . $template . '/html/layouts/plugins/' . $this->_type . '/' . $this->_name, + __DIR__ . '/layouts', + ); + } + + /** + * Get the plugin renderer + * + * @param string $layoutId Layout identifier + * + * @return JLayout + * + * @since 3.5 + */ + protected function getRenderer($layoutId = 'default') + { + $renderer = new JLayoutFile($layoutId); + + $renderer->setIncludePaths($this->getLayoutsPaths()); + + return $renderer; + } + + /** + * Get the data that will be sent to the stats server. + * + * @return array. + * + * @since 3.5 + */ + private function getStatsData() + { + return array( + 'unique_id' => $this->getUniqueId(), 'php_version' => PHP_VERSION, 'db_type' => $this->db->name, 'db_version' => $this->db->getVersion(), 'cms_version' => JVERSION, 'server_os' => php_uname('s') . ' ' . php_uname('r') ); + } - // Don't let the request take longer than 2 seconds to avoid page timeout issues - try - { - // Don't let the request take longer than 2 seconds to avoid page timeout issues - JHttpFactory::getHttp()->post($this->params->get('url', 'https://developer.joomla.org/stats/submit'), $data, null, 2); - } - catch (UnexpectedValueException $e) + /** + * Get the unique id. Generates one if none is set. + * + * @return integer + * + * @since 3.5 + */ + private function getUniqueId() + { + if (null === $this->uniqueId) { - // There was an error sending stats. Should we do anything? - JLog::add('Could not send site statistics to remote server: ' . $e->getMessage(), JLog::WARNING, 'stats'); + $this->uniqueId = $this->params->get('unique_id', hash('sha1', JUserHelper::genRandomPassword(28) . time())); } - catch (RuntimeException $e) + + return $this->uniqueId; + } + + /** + * Check if current user is allowed to send the data + * + * @return boolean + * + * @since 3.5 + */ + private function isAllowedUser() + { + return JFactory::getUser()->authorise('core.admin'); + } + + /** + * Check if the debug is enabled + * + * @return boolean + * + * @since 3.5 + */ + private function isDebugEnabled() + { + return ((int) $this->params->get('debug', 0) === 1); + } + + /** + * Check if last_run + interval > now + * + * @return boolean + * + * @since 3.5 + */ + private function isUpdateRequired() + { + $last = (int) $this->params->get('lastrun', 0); + $interval = (int) $this->params->get('interval', 12); + $mode = (int) $this->params->get('mode', 0); + + if ($mode === static::MODE_ALLOW_NEVER) { - // There was an error connecting to the server or in the post request - JLog::add('Could not connect to statistics server: ' . $e->getMessage(), JLog::WARNING, 'stats'); + return false; } - catch (Exception $e) + + // Never updated or debug enabled + if (!$last || $this->isDebugEnabled()) { - // An unexpected error in processing; don't let this failure kill the site - JLog::add('Unexpected error connecting to statistics server: ' . $e->getMessage(), JLog::WARNING, 'stats'); + return true; } + + return (abs(time() - $last) > $interval * 3600); + } + + /** + * Check valid AJAX request + * + * @return boolean + * + * @since 3.5 + */ + private function isAjaxRequest() + { + return strtolower($this->app->input->server->get('HTTP_X_REQUESTED_WITH', '')) == 'xmlhttprequest'; } /** @@ -141,6 +382,12 @@ public function onAfterInitialise() */ private function saveParams() { + // Update params + $this->params->set('lastrun', time()); + $this->params->set('unique_id', $this->getUniqueId()); + $interval = (int) $this->params->get('interval', 12); + $this->params->set('interval', $interval ? $interval : 12); + $query = $this->db->getQuery(true) ->update($this->db->quoteName('#__extensions')) ->set($this->db->quoteName('params') . ' = ' . $this->db->quote($this->params->toString('JSON'))) @@ -187,6 +434,41 @@ private function saveParams() return $result; } + /** + * Send the stats to the stats server + * + * @return boolean + * + * @since 3.5 + * + * @throws RuntimeException If there is an error sending the data. + */ + private function sendStats() + { + try + { + // Don't let the request take longer than 2 seconds to avoid page timeout issues + JHttpFactory::getHttp()->post($this->serverUrl, $this->getStatsData(), null, 2); + } + catch (UnexpectedValueException $e) + { + // There was an error sending stats. Should we do anything? + throw new RuntimeException('Could not send site statistics to remote server: ' . $e->getMessage(), 500); + } + catch (RuntimeException $e) + { + // There was an error connecting to the server or in the post request + throw new RuntimeException('Could not connect to statistics server: ' . $e->getMessage(), 500); + } + catch (Exception $e) + { + // An unexpected error in processing; don't let this failure kill the site + throw new RuntimeException('Unexpected error connecting to statistics server: ' . $e->getMessage(), 500); + } + + return true; + } + /** * Clears cache groups. We use it to clear the plugins cache after we update the last run timestamp. * diff --git a/plugins/system/stats/stats.xml b/plugins/system/stats/stats.xml index 5efc359eff2b8..b043fad8a2c82 100644 --- a/plugins/system/stats/stats.xml +++ b/plugins/system/stats/stats.xml @@ -10,8 +10,9 @@ 3.5.0 PLG_SYSTEM_STATS_XML_DESCRIPTION - stats.php fields + layouts + stats.php en-GB/en-GB.plg_system_stats.ini @@ -28,14 +29,24 @@ size="10" /> + + + + + +
+ + + + +