[ Index ] |
PHP Cross Reference of Joomla 4.2.2 documentation |
[Summary view] [Print] [Text view]
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 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body
Generated: Wed Sep 7 05:41:13 2022 | Chilli.vc Blog - For Webmaster,Blog-Writer,System Admin and Domainer |