[ Index ]

PHP Cross Reference of Joomla 4.2.2 documentation

title

Body

[close]

/plugins/system/stats/ -> stats.php (source)

   1  <?php
   2  
   3  /**
   4   * @package     Joomla.Plugin
   5   * @subpackage  System.stats
   6   *
   7   * @copyright   (C) 2015 Open Source Matters, Inc. <https://www.joomla.org>
   8   * @license     GNU General Public License version 2 or later; see LICENSE.txt
   9  
  10   * @phpcs:disable PSR1.Classes.ClassDeclaration.MissingNamespace
  11   */
  12  
  13  use Joomla\CMS\Cache\Cache;
  14  use Joomla\CMS\Factory;
  15  use Joomla\CMS\Http\HttpFactory;
  16  use Joomla\CMS\Language\Text;
  17  use Joomla\CMS\Layout\FileLayout;
  18  use Joomla\CMS\Log\Log;
  19  use Joomla\CMS\Plugin\CMSPlugin;
  20  use Joomla\CMS\Uri\Uri;
  21  use Joomla\CMS\User\UserHelper;
  22  
  23  // phpcs:disable PSR1.Files.SideEffects
  24  \defined('_JEXEC') or die;
  25  // phpcs:enable PSR1.Files.SideEffects
  26  
  27  // Uncomment the following line to enable debug mode for testing purposes. Note: statistics will be sent on every page load
  28  // define('PLG_SYSTEM_STATS_DEBUG', 1);
  29  
  30  /**
  31   * Statistics system plugin. This sends anonymous data back to the Joomla! Project about the
  32   * PHP, SQL, Joomla and OS versions
  33   *
  34   * @since  3.5
  35   */
  36  class PlgSystemStats extends CMSPlugin
  37  {
  38      /**
  39       * Indicates sending statistics is always allowed.
  40       *
  41       * @var    integer
  42       *
  43       * @since  3.5
  44       */
  45      public const MODE_ALLOW_ALWAYS = 1;
  46  
  47      /**
  48       * Indicates sending statistics is never allowed.
  49       *
  50       * @var    integer
  51       *
  52       * @since  3.5
  53       */
  54      public const MODE_ALLOW_NEVER = 3;
  55  
  56      /**
  57       * @var    \Joomla\CMS\Application\CMSApplication
  58       *
  59       * @since  3.5
  60       */
  61      protected $app;
  62  
  63      /**
  64       * @var    \Joomla\Database\DatabaseDriver
  65       *
  66       * @since  3.5
  67       */
  68      protected $db;
  69  
  70      /**
  71       * URL to send the statistics.
  72       *
  73       * @var    string
  74       *
  75       * @since  3.5
  76       */
  77      protected $serverUrl = 'https://developer.joomla.org/stats/submit';
  78  
  79      /**
  80       * Unique identifier for this site
  81       *
  82       * @var    string
  83       *
  84       * @since  3.5
  85       */
  86      protected $uniqueId;
  87  
  88      /**
  89       * Listener for the `onAfterInitialise` event
  90       *
  91       * @return  void
  92       *
  93       * @since   3.5
  94       */
  95      public function onAfterInitialise()
  96      {
  97          if (!$this->app->isClient('administrator') || !$this->isAllowedUser()) {
  98              return;
  99          }
 100  
 101          if ($this->isCaptiveMFA()) {
 102              return;
 103          }
 104  
 105          if (!$this->isDebugEnabled() && !$this->isUpdateRequired()) {
 106              return;
 107          }
 108  
 109          if (Uri::getInstance()->getVar('tmpl') === 'component') {
 110              return;
 111          }
 112  
 113          // Load plugin language files only when needed (ex: they are not needed in site client).
 114          $this->loadLanguage();
 115      }
 116  
 117      /**
 118       * Listener for the `onAfterDispatch` event
 119       *
 120       * @return  void
 121       *
 122       * @since   4.0.0
 123       */
 124      public function onAfterDispatch()
 125      {
 126          if (!$this->app->isClient('administrator') || !$this->isAllowedUser()) {
 127              return;
 128          }
 129  
 130          if ($this->isCaptiveMFA()) {
 131              return;
 132          }
 133  
 134          if (!$this->isDebugEnabled() && !$this->isUpdateRequired()) {
 135              return;
 136          }
 137  
 138          if (Uri::getInstance()->getVar('tmpl') === 'component') {
 139              return;
 140          }
 141  
 142          if ($this->app->getDocument()->getType() !== 'html') {
 143              return;
 144          }
 145  
 146          $this->app->getDocument()->getWebAssetManager()
 147              ->registerAndUseScript('plg_system_stats.message', 'plg_system_stats/stats-message.js', [], ['defer' => true], ['core']);
 148      }
 149  
 150      /**
 151       * User selected to always send data
 152       *
 153       * @return  void
 154       *
 155       * @since   3.5
 156       *
 157       * @throws  Exception         If user is not allowed.
 158       * @throws  RuntimeException  If there is an error saving the params or sending the data.
 159       */
 160      public function onAjaxSendAlways()
 161      {
 162          if (!$this->isAllowedUser() || !$this->isAjaxRequest()) {
 163              throw new Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED'), 403);
 164          }
 165  
 166          $this->params->set('mode', static::MODE_ALLOW_ALWAYS);
 167  
 168          if (!$this->saveParams()) {
 169              throw new RuntimeException('Unable to save plugin settings', 500);
 170          }
 171  
 172          echo json_encode(['sent' => (int) $this->sendStats()]);
 173      }
 174  
 175      /**
 176       * User selected to never send data.
 177       *
 178       * @return  void
 179       *
 180       * @since   3.5
 181       *
 182       * @throws  Exception         If user is not allowed.
 183       * @throws  RuntimeException  If there is an error saving the params.
 184       */
 185      public function onAjaxSendNever()
 186      {
 187          if (!$this->isAllowedUser() || !$this->isAjaxRequest()) {
 188              throw new Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED'), 403);
 189          }
 190  
 191          $this->params->set('mode', static::MODE_ALLOW_NEVER);
 192  
 193          if (!$this->saveParams()) {
 194              throw new RuntimeException('Unable to save plugin settings', 500);
 195          }
 196  
 197          if (!$this->disablePlugin()) {
 198              throw new RuntimeException('Unable to disable the statistics plugin', 500);
 199          }
 200  
 201          echo json_encode(['sent' => 0]);
 202      }
 203  
 204      /**
 205       * Send the stats to the server.
 206       * On first load | on demand mode it will show a message asking users to select mode.
 207       *
 208       * @return  void
 209       *
 210       * @since   3.5
 211       *
 212       * @throws  Exception         If user is not allowed.
 213       * @throws  RuntimeException  If there is an error saving the params, disabling the plugin or sending the data.
 214       */
 215      public function onAjaxSendStats()
 216      {
 217          if (!$this->isAllowedUser() || !$this->isAjaxRequest()) {
 218              throw new Exception(Text::_('JGLOBAL_AUTH_ACCESS_DENIED'), 403);
 219          }
 220  
 221          // User has not selected the mode. Show message.
 222          if ((int) $this->params->get('mode') !== static::MODE_ALLOW_ALWAYS) {
 223              $data = [
 224                  'sent' => 0,
 225                  'html' => $this->getRenderer('message')->render($this->getLayoutData()),
 226              ];
 227  
 228              echo json_encode($data);
 229  
 230              return;
 231          }
 232  
 233          if (!$this->saveParams()) {
 234              throw new RuntimeException('Unable to save plugin settings', 500);
 235          }
 236  
 237          echo json_encode(['sent' => (int) $this->sendStats()]);
 238      }
 239  
 240      /**
 241       * Get the data through events
 242       *
 243       * @param   string  $context  Context where this will be called from
 244       *
 245       * @return  array
 246       *
 247       * @since   3.5
 248       */
 249      public function onGetStatsData($context)
 250      {
 251          return $this->getStatsData();
 252      }
 253  
 254      /**
 255       * Debug a layout of this plugin
 256       *
 257       * @param   string  $layoutId  Layout identifier
 258       * @param   array   $data      Optional data for the layout
 259       *
 260       * @return  string
 261       *
 262       * @since   3.5
 263       */
 264      public function debug($layoutId, $data = [])
 265      {
 266          $data = array_merge($this->getLayoutData(), $data);
 267  
 268          return $this->getRenderer($layoutId)->debug($data);
 269      }
 270  
 271      /**
 272       * Get the data for the layout
 273       *
 274       * @return  array
 275       *
 276       * @since   3.5
 277       */
 278      protected function getLayoutData()
 279      {
 280          return [
 281              'plugin'       => $this,
 282              'pluginParams' => $this->params,
 283              'statsData'    => $this->getStatsData(),
 284          ];
 285      }
 286  
 287      /**
 288       * Get the layout paths
 289       *
 290       * @return  array
 291       *
 292       * @since   3.5
 293       */
 294      protected function getLayoutPaths()
 295      {
 296          $template = Factory::getApplication()->getTemplate();
 297  
 298          return [
 299              JPATH_ADMINISTRATOR . '/templates/' . $template . '/html/layouts/plugins/' . $this->_type . '/' . $this->_name,
 300              __DIR__ . '/layouts',
 301          ];
 302      }
 303  
 304      /**
 305       * Get the plugin renderer
 306       *
 307       * @param   string  $layoutId  Layout identifier
 308       *
 309       * @return  \Joomla\CMS\Layout\LayoutInterface
 310       *
 311       * @since   3.5
 312       */
 313      protected function getRenderer($layoutId = 'default')
 314      {
 315          $renderer = new FileLayout($layoutId);
 316  
 317          $renderer->setIncludePaths($this->getLayoutPaths());
 318  
 319          return $renderer;
 320      }
 321  
 322      /**
 323       * Get the data that will be sent to the stats server.
 324       *
 325       * @return  array
 326       *
 327       * @since   3.5
 328       */
 329      private function getStatsData()
 330      {
 331          $data = [
 332              'unique_id'   => $this->getUniqueId(),
 333              'php_version' => PHP_VERSION,
 334              'db_type'     => $this->db->name,
 335              'db_version'  => $this->db->getVersion(),
 336              'cms_version' => JVERSION,
 337              'server_os'   => php_uname('s') . ' ' . php_uname('r'),
 338          ];
 339  
 340          // Check if we have a MariaDB version string and extract the proper version from it
 341          if (preg_match('/^(?:5\.5\.5-)?(mariadb-)?(?P<major>\d+)\.(?P<minor>\d+)\.(?P<patch>\d+)/i', $data['db_version'], $versionParts)) {
 342              $data['db_version'] = $versionParts['major'] . '.' . $versionParts['minor'] . '.' . $versionParts['patch'];
 343          }
 344  
 345          return $data;
 346      }
 347  
 348      /**
 349       * Get the unique id. Generates one if none is set.
 350       *
 351       * @return  integer
 352       *
 353       * @since   3.5
 354       */
 355      private function getUniqueId()
 356      {
 357          if (null === $this->uniqueId) {
 358              $this->uniqueId = $this->params->get('unique_id', hash('sha1', UserHelper::genRandomPassword(28) . time()));
 359          }
 360  
 361          return $this->uniqueId;
 362      }
 363  
 364      /**
 365       * Check if current user is allowed to send the data
 366       *
 367       * @return  boolean
 368       *
 369       * @since   3.5
 370       */
 371      private function isAllowedUser()
 372      {
 373          return Factory::getUser()->authorise('core.admin');
 374      }
 375  
 376      /**
 377       * Check if the debug is enabled
 378       *
 379       * @return  boolean
 380       *
 381       * @since   3.5
 382       */
 383      private function isDebugEnabled()
 384      {
 385          return defined('PLG_SYSTEM_STATS_DEBUG');
 386      }
 387  
 388      /**
 389       * Check if last_run + interval > now
 390       *
 391       * @return  boolean
 392       *
 393       * @since   3.5
 394       */
 395      private function isUpdateRequired()
 396      {
 397          $last     = (int) $this->params->get('lastrun', 0);
 398          $interval = (int) $this->params->get('interval', 12);
 399          $mode     = (int) $this->params->get('mode', 0);
 400  
 401          if ($mode === static::MODE_ALLOW_NEVER) {
 402              return false;
 403          }
 404  
 405          // Never updated or debug enabled
 406          if (!$last || $this->isDebugEnabled()) {
 407              return true;
 408          }
 409  
 410          return abs(time() - $last) > $interval * 3600;
 411      }
 412  
 413      /**
 414       * Check valid AJAX request
 415       *
 416       * @return  boolean
 417       *
 418       * @since   3.5
 419       */
 420      private function isAjaxRequest()
 421      {
 422          return strtolower($this->app->input->server->get('HTTP_X_REQUESTED_WITH', '')) === 'xmlhttprequest';
 423      }
 424  
 425      /**
 426       * Render a layout of this plugin
 427       *
 428       * @param   string  $layoutId  Layout identifier
 429       * @param   array   $data      Optional data for the layout
 430       *
 431       * @return  string
 432       *
 433       * @since   3.5
 434       */
 435      public function render($layoutId, $data = [])
 436      {
 437          $data = array_merge($this->getLayoutData(), $data);
 438  
 439          return $this->getRenderer($layoutId)->render($data);
 440      }
 441  
 442      /**
 443       * Save the plugin parameters
 444       *
 445       * @return  boolean
 446       *
 447       * @since   3.5
 448       */
 449      private function saveParams()
 450      {
 451          // Update params
 452          $this->params->set('lastrun', time());
 453          $this->params->set('unique_id', $this->getUniqueId());
 454          $interval = (int) $this->params->get('interval', 12);
 455          $this->params->set('interval', $interval ?: 12);
 456  
 457          $paramsJson = $this->params->toString('JSON');
 458          $db         = $this->db;
 459  
 460          $query = $db->getQuery(true)
 461              ->update($db->quoteName('#__extensions'))
 462              ->set($db->quoteName('params') . ' = :params')
 463              ->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
 464              ->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
 465              ->where($db->quoteName('element') . ' = ' . $db->quote('stats'))
 466              ->bind(':params', $paramsJson);
 467  
 468          try {
 469              // Lock the tables to prevent multiple plugin executions causing a race condition
 470              $db->lockTable('#__extensions');
 471          } catch (Exception $e) {
 472              // If we can't lock the tables it's too risky to continue execution
 473              return false;
 474          }
 475  
 476          try {
 477              // Update the plugin parameters
 478              $result = $db->setQuery($query)->execute();
 479  
 480              $this->clearCacheGroups(['com_plugins']);
 481          } catch (Exception $exc) {
 482              // If we failed to execute
 483              $db->unlockTables();
 484              $result = false;
 485          }
 486  
 487          try {
 488              // Unlock the tables after writing
 489              $db->unlockTables();
 490          } catch (Exception $e) {
 491              // If we can't lock the tables assume we have somehow failed
 492              $result = false;
 493          }
 494  
 495          return $result;
 496      }
 497  
 498      /**
 499       * Send the stats to the stats server
 500       *
 501       * @return  boolean
 502       *
 503       * @since   3.5
 504       *
 505       * @throws  RuntimeException  If there is an error sending the data and debug mode enabled.
 506       */
 507      private function sendStats()
 508      {
 509          $error = false;
 510  
 511          try {
 512              // Don't let the request take longer than 2 seconds to avoid page timeout issues
 513              $response = HttpFactory::getHttp()->post($this->serverUrl, $this->getStatsData(), [], 2);
 514  
 515              if (!$response) {
 516                  $error = 'Could not send site statistics to remote server: No response';
 517              } elseif ($response->code !== 200) {
 518                  $data = json_decode($response->body);
 519  
 520                  $error = 'Could not send site statistics to remote server: ' . $data->message;
 521              }
 522          } catch (UnexpectedValueException $e) {
 523              // There was an error sending stats. Should we do anything?
 524              $error = 'Could not send site statistics to remote server: ' . $e->getMessage();
 525          } catch (RuntimeException $e) {
 526              // There was an error connecting to the server or in the post request
 527              $error = 'Could not connect to statistics server: ' . $e->getMessage();
 528          } catch (Exception $e) {
 529              // An unexpected error in processing; don't let this failure kill the site
 530              $error = 'Unexpected error connecting to statistics server: ' . $e->getMessage();
 531          }
 532  
 533          if ($error !== false) {
 534              // Log any errors if logging enabled.
 535              Log::add($error, Log::WARNING, 'jerror');
 536  
 537              // If Stats debug mode enabled, or Global Debug mode enabled, show error to the user.
 538              if ($this->isDebugEnabled() || $this->app->get('debug')) {
 539                  throw new RuntimeException($error, 500);
 540              }
 541  
 542              return false;
 543          }
 544  
 545          return true;
 546      }
 547  
 548      /**
 549       * Clears cache groups. We use it to clear the plugins cache after we update the last run timestamp.
 550       *
 551       * @param   array  $clearGroups  The cache groups to clean
 552       *
 553       * @return  void
 554       *
 555       * @since   3.5
 556       */
 557      private function clearCacheGroups(array $clearGroups)
 558      {
 559          foreach ($clearGroups as $group) {
 560              try {
 561                  $options = [
 562                      'defaultgroup' => $group,
 563                      'cachebase'    => $this->app->get('cache_path', JPATH_CACHE),
 564                  ];
 565  
 566                  $cache = Cache::getInstance('callback', $options);
 567                  $cache->clean();
 568              } catch (Exception $e) {
 569                  // Ignore it
 570              }
 571          }
 572      }
 573  
 574      /**
 575       * Disable this plugin, if user selects once or never, to stop Joomla loading the plugin on every page load and
 576       * therefore regaining a tiny bit of performance
 577       *
 578       * @since   4.0.0
 579       *
 580       * @return  boolean
 581       */
 582      private function disablePlugin()
 583      {
 584          $db = $this->db;
 585  
 586          $query = $db->getQuery(true)
 587              ->update($db->quoteName('#__extensions'))
 588              ->set($db->quoteName('enabled') . ' = 0')
 589              ->where($db->quoteName('type') . ' = ' . $db->quote('plugin'))
 590              ->where($db->quoteName('folder') . ' = ' . $db->quote('system'))
 591              ->where($db->quoteName('element') . ' = ' . $db->quote('stats'));
 592  
 593          try {
 594              // Lock the tables to prevent multiple plugin executions causing a race condition
 595              $db->lockTable('#__extensions');
 596          } catch (Exception $e) {
 597              // If we can't lock the tables it's too risky to continue execution
 598              return false;
 599          }
 600  
 601          try {
 602              // Update the plugin parameters
 603              $result = $db->setQuery($query)->execute();
 604  
 605              $this->clearCacheGroups(['com_plugins']);
 606          } catch (Exception $exc) {
 607              // If we failed to execute
 608              $db->unlockTables();
 609              $result = false;
 610          }
 611  
 612          try {
 613              // Unlock the tables after writing
 614              $db->unlockTables();
 615          } catch (Exception $e) {
 616              // If we can't lock the tables assume we have somehow failed
 617              $result = false;
 618          }
 619  
 620          return $result;
 621      }
 622  
 623      /**
 624       * Are we in a Multi-factor Authentication page?
 625       *
 626       * @return  bool
 627       * @since   4.2.1
 628       */
 629      private function isCaptiveMFA(): bool
 630      {
 631          return method_exists($this->app, 'isMultiFactorAuthenticationPage')
 632              && $this->app->isMultiFactorAuthenticationPage(true);
 633      }
 634  }


Generated: Wed Sep 7 05:41:13 2022 Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer