<?php
/** 
 * JS APP renderer system plugin
 * @package JREALTIMEANALYTICS::plugins::system
 * @author Joomla! Extensions Store
 * @copyright (C) 2013 - Joomla! Extensions Store
 * @license GNU/GPLv2 http://www.gnu.org/licenses/gpl-2.0.html  
 */
// no direct access
defined ( '_JEXEC' ) or die ( 'Restricted access' );
use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\Factory;
use Joomla\CMS\Router\Route;
use Joomla\Event\SubscriberInterface;
use Joomla\Event\DispatcherInterface;
use Joomla\Event\Event;
use Joomla\Filter\OutputFilter;
use JExtstore\Component\JRealtimeAnalytics\Administrator\Framework\Helpers\Users as JRealtimeHelpersUsers;
use JExtstore\Component\JRealtimeAnalytics\Administrator\Framework\Helpers\Language as JRealtimeHelpersLanguage;


class PlgSystemJRealtimeAnalytics extends CMSPlugin implements SubscriberInterface {
	/**
	 * @access private
	 * @var boolean
	 */
	private $isPluginStopped;
	
	/**
	 * App reference
	 *
	 * @access protected
	 * @var Object
	 */
	protected $appInstance;
	
	/**
	 * DB reference
	 *
	 * @access protected
	 * @var Object
	 */
	protected $dbInstance;
	
	/**
	 * Component configuration
	 *
	 * @access protected
	 * @var Object
	 */
	protected $componentConfig;
	
	/**
	 * User object
	 *
	 * @access protected
	 * @var Object
	 */
	protected $myUser;
	
	/**
	 * Manage obsservers notify for attached observers
	 *
	 * @access private
	 * @param array $states
	 *        	Additional states for observable object that drive methods execution inside observer
	 * @return array
	 */
	private function notifyObservers($states = array()) {
		// Require base models and helpers
		require_once JPATH_BASE . '/administrator/components/com_jrealtimeanalytics/Framework/Helpers/Users.php';
		require_once JPATH_BASE . '/administrator/components/com_jrealtimeanalytics/Framework/Helpers/Device.php';
		require_once JPATH_BASE . '/administrator/components/com_jrealtimeanalytics/Framework/Exception/Exception.php';
		require_once JPATH_BASE . '/administrator/components/com_jrealtimeanalytics/Framework/Model/Model.php';
		require_once JPATH_BASE . '/administrator/components/com_jrealtimeanalytics/Framework/Model/IObservable.php';
		require_once JPATH_BASE . '/administrator/components/com_jrealtimeanalytics/Framework/Model/Observer.php';
		require_once JPATH_BASE . '/administrator/components/com_jrealtimeanalytics/Framework/Model/Observable.php';
		
		// Instantiate session object for Dependency Injection into main model
		$userSessionTable = JRealtimeHelpersUsers::getSessiontable ();
		
		// Instantiate Observer Model instance
		$observerServerstatsModelInstance = $this->appInstance->bootComponent('com_jrealtimeanalytics')->getMVCFactory()->createModel('ServerstatsObsrv', 'Site', ['sessiontable' => $userSessionTable]);
		
		// Instantiate Observer Model instance
		$observerEventstatsModelInstance = $this->appInstance->bootComponent('com_jrealtimeanalytics')->getMVCFactory()->createModel('EventstatsObsrv', 'Site', ['sessiontable' => $userSessionTable]);
		
		// Instantiate Observable Model instance
		$observableModelInstance = $this->appInstance->bootComponent('com_jrealtimeanalytics')->getMVCFactory()->createModel('Stream', 'Site', ['sessiontable' => $userSessionTable]);
		
		// Now attach observers to abservable model, plugin acts like a display controller main execution for front component
		$observableModelInstance->attach ( $observerServerstatsModelInstance );
		$observableModelInstance->attach ( $observerEventstatsModelInstance );
		$observableModelInstance->setState ( 'initialize', 1 );
		$observableModelInstance->setState ( 'appdispatch', true ); // Always indicates to track Referral is any valid
		                                                            
		// Manage additional state injected for observers methods exec
		if (count ( $states )) {
			foreach ( $states as $state ) {
				$observableModelInstance->setState ( $state [0], $state [1] );
			}
		}
		
		return $observableModelInstance->notify ();
	}
	
	/**
	 * Effettua l'app js output
	 *
	 * @param Object& $cParams        	
	 * @param Object& $doc        	
	 * @return boolean
	 */
	private function injectApp($cParams, $doc) {
		$option = $this->appInstance->getInput()->get ( 'option' );
		$base = Uri::base ();
		
		// Output JS APP nel Document esclusione
		if ($doc->getType () !== 'html' || $this->appInstance->getInput()->getCmd ( 'tmpl' ) === 'component') {
			return false;
		}
		
		// Require language helper for JS App inject translations
		if(!class_exists('JExtstore\Component\JRealtimeAnalytics\Administrator\Framework\Helpers\Language')) {
			require_once JPATH_BASE . '/administrator/components/com_jrealtimeanalytics/Framework/Helpers/Users.php';
		}
		
		// Create language object
		$language = $this->appInstance->getLanguage ();
		$language->load ( 'com_jrealtimeanalytics', JPATH_SITE . '/components/com_jrealtimeanalytics' );
		$language->load ( 'com_jrealtimeanalytics', JPATH_SITE, null, true );
		$jrealtimeLanguage = JRealtimeHelpersLanguage::getInstance ();
		
		$translations = array (
				'COM_JREALTIME_NETWORK_ERROR' 
		);
		$jrealtimeLanguage->injectJsTranslations ( $translations, $doc );
		
		// Output JS APP nel Document
		$doc->getWebAssetManager()->registerAndUseStyle ( 'jrealtime.mainstyle', 'components/com_jrealtimeanalytics/css/mainstyle.css');
		
		// Scripts loading
		$defer = $cParams->get('scripts_loading', null) == 'defer' ? true : false;
		$async = $cParams->get('scripts_loading', null) == 'async' ? true : false;
		
		if ($this->componentConfig->get ( 'includejquery', 1 )) {
			$doc->getWebAssetManager()->useScript('jquery');
		}
		
		if ($this->componentConfig->get ( 'noconflict', 0 )) {
			$doc->getWebAssetManager()->useScript('jquery-noconflict');
		}
		
		$doc->getWebAssetManager()->addInlineScript ( "var jrealtimeBaseURI='$base';" );
		$doc->getWebAssetManager()->addInlineScript ( "var jrealtimeTrackQueryString=" . $cParams->get('track_query_string', 1) . ";" );
		
		$doc->getWebAssetManager()->registerAndUseScript ( 'jrealtime.stream', 'components/com_jrealtimeanalytics/js/stream.js', [], ['defer'=>$defer, 'async'=>$async], ['jquery']);
		
		if ($cParams->get ( 'heatmap_status', 1 ) && $this->appInstance->getInput()->get('option') != 'com_jrealtimeanalytics') {
			$doc->getWebAssetManager()->registerAndUseScript ( 'jrealtime.heatmap', 'components/com_jrealtimeanalytics/js/heatmap.js', [], ['defer'=>$defer, 'async'=>$async], ['jquery']);
		}
		
		// Check if the request is directed to com_finder, if so track the searched keywords, mimic the onContentSearch observer method
		$notifications = array ();
		$option = $this->appInstance->getInput()->get ( 'option' );
		$view = $this->appInstance->getInput()->get ( 'view' );
		if ($option === 'com_finder' && $view === 'search') {
			// Found a request for com_finder Smart Search, so record the searched keywords
			$keywords = $this->appInstance->getInput()->getString ( 'q', '' );
			if (trim ( $keywords )) {
				$eventObject = new Event('jrealtimeOnContentSearch', array($keywords, 'jrealtime_plugin', null, null));
				$notifications = $this->trackContentSearch ( $eventObject );
			}
		}
		
		// If valid plugin execution on main app dispatch
		$notifications = array_merge ( $notifications, $this->notifyObservers () );
		
		// Manage observers exceptions, cycle on $notifications responses to enqueue messages into app object for debug purpouse
		if ($this->componentConfig->get ( 'enable_debug', false )) {
			foreach ( $notifications as $exception ) {
				// Found an exception, set into app response message queue for client side debug
				if ($exception instanceof \JExtstore\Component\JRealtimeAnalytics\Administrator\Framework\Exception) {
					$this->appInstance->enqueueMessage ( $exception->getMessage (), $exception->getExceptionLevel () );
				}
			}
		}
		
		return true;
	}
	
	/**
	 * Renders custom visual styles in the page body output
	 *
	 * @access private
	 * @return void
	 */
	private function injectCustomStyles($doc) {
		// Retrieve and init parameters for the query
		$pageurl = $this->appInstance->getInput()->getString('jes_pageurl');
		$dateFrom = $this->appInstance->getInput()->get('jes_from');
		$dateTo = $this->appInstance->getInput()->get('jes_to');

		// Get new query object and retrieve all styles
		try {
			$query = "SELECT count(c.id) AS numclicks, s.selector" .
					 "\n FROM #__realtimeanalytics_heatmap AS s" .
					 "\n INNER JOIN #__realtimeanalytics_heatmap_clicks AS c" .
					 "\n ON s.id = c.heatmap_id" .
					 "\n WHERE s.pageurl = " . $this->dbInstance->quote($pageurl) .
					 "\n AND c.record_date >= " . $this->dbInstance->quote($dateFrom) .
					 "\n AND c.record_date <= " . $this->dbInstance->quote($dateTo) .
					 "\n GROUP BY s.selector" .
					 "\n ORDER BY numclicks DESC";

			$heatmapSelectors = $this->dbInstance->setQuery ( $query )->loadObjectList ();
		} catch ( \Exception $e ) {
			// No error reporting in this case
		}
	
		// Get importantify mode
		$importantify = ' !important';
	
		// Check if styles are found
		if (is_array ( $heatmapSelectors ) && count ( $heatmapSelectors )) {
			// run max value function and store in variable
			$max = $heatmapSelectors[0]->numclicks;
			$n = 100; // Declare the number of groups

			// Define the ending colour, which is white
			$xr = 255; // Red value
			$xg = 255; // Green value
			$xb = 255; // Blue value

			// Define the starting colour #f32075
			$yr = 243;
			$yg = 32;
			$yb = 117;

			$stylesheet = null;
			$additionalInjects = array();
			// Build the dynamic stylesheet based on custom visual styles
			foreach ( $heatmapSelectors as $selector ) {
				$val = $selector->numclicks;
				$pos = intval(($val / $max) * 100);
				$alpha = round($pos / 100, 1, PHP_ROUND_HALF_DOWN);
				$alpha = $alpha <= 0.2 ? 0.2 : $alpha;
				$alpha = $alpha >= 0.9 ? 0.9 : $alpha;

				// Calculate heat colors
				$red = intval($xr + (( $pos * ($yr - $xr)) / ($n-1)));
				$green = intval($xg + (( $pos * ($yg - $xg)) / ($n-1)));
				$blue = intval($xb + (( $pos * ($yb - $xb)) / ($n-1)));

				$stylesheet .= $selector->selector . '{background: radial-gradient(at 50% 50%, rgba(41, 154, 11, ' . $alpha . ') 0%, rgba(11, 109, 145, ' . $alpha . ') 32%, ' .
														'rgba(155, 168, 13, ' . $alpha . ') 48%, rgba(' . $red . ',' . $green . ',' . $blue . ', ' . $alpha . ') 74%) !important;' .
														'border: none;border-radius:50px;box-shadow:1px 1px 55px 25px rgba(' . $red . ',' . $green . ',' . $blue . ', ' . ($alpha+0.1) . ') !important;}';
				$stylesheet .= $selector->selector . ':after{content: "' . $selector->numclicks . ' clicks";display:inline-block;margin: 5px;padding:2px 4px;font-size:12px;' .
															'background-color:#FF0000;border-radius:3px;font-weight:bold;line-height:14px;color:#fff;}';

				// Match final input element in the selector and manage after append by JS
				if(preg_match('/((>input|>select|>textarea|>img)[\.a-zA-Z0-9-_]*)$/i', $selector->selector)) {
					$additionalInjects[] = ('$("' . $selector->selector . '").after("<span style=\'display:inline-block;margin: 5px;padding:2px 4px;font-size:12px;' .
																					'background-color:#FF0000;border-radius:3px;font-weight:bold;line-height:14px;color:#fff;\'>' .
																			$selector->numclicks . ' clicks</span>");');
				}
			}
	
			// Now inject
			$stylesheet = str_ireplace('@', '\@', $stylesheet);
			$doc->getWebAssetManager()->addInlineStyle ( $stylesheet );
			if(count($additionalInjects)) {
				if ($this->componentConfig->get ( 'includejquery', 1 )) {
					$doc->getWebAssetManager()->useScript('jquery');
				}
				
				if ($this->componentConfig->get ( 'noconflict', 0 )) {
					$doc->getWebAssetManager()->useScript('jquery-noconflict');
				}
				
				$doc->getWebAssetManager()->addInlineScript('jQuery(function($){' . implode(' ', $additionalInjects) . '});');
			}
		}
	}
	
	/**
	 * Manage notrack cases exclusions, posted internal pages, by IP, IP range, Pages, User groups
	 *
	 * @access private
	 * @return boolean
	 */
	private function validateTracking() {
		// Manage notrack case by posted internal pages
		if ($this->appInstance->getInput()->get ( 'notrack', false )) {
			return false;
		}
		
		// Exclude always for backend
		if (!$this->appInstance->isClient('site')) {
			return false;
		}
		
		$doc = $this->appInstance->getDocument();
		// Output JS APP nel Document esclusione
		if ($doc->getType () !== 'html' || $this->appInstance->getInput()->getCmd ( 'tmpl' ) === 'component') {
			return false;
		}
		
		// Special exclusion for Akeeba LoginGuard 2SV page and J4 Captive Login
		if (($this->appInstance->getInput()->get ( 'option' ) == 'com_users' && $this->appInstance->getInput()->get ( 'view' ) == 'captive') || $this->appInstance->getInput()->get ( 'option' ) == 'com_loginguard') {
			return false;
		}
		
		// Check for IP exclusion
		$ipAddressRegex = '/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}\z/i';
		$clientIP = $this->componentConfig->get('cloudflare_ip_masking', 0) == 0 ? $_SERVER['REMOTE_ADDR'] : @$_SERVER['HTTP_CF_CONNECTING_IP'];
		
		// Evaluate the IP address masking or alternatives
		if($serverIpOverride = $this->componentConfig->get('server_ip_override', '')) {
			if($serverIpOverride == 'realip') {
				$clientIP = @$_SERVER['HTTP_X_REAL_IP'];
			} elseif($serverIpOverride == 'remoteip') {
				$clientIP = @$_SERVER['HTTP_X_REMOTE_IP'];
			}
		}
		
		// Fallback to empty string if null
		if(!$clientIP) {
			$clientIP = '';
		}
		
		if($clientIP) {
			$clientIpDec = ( float ) sprintf ( "%u", ip2long ( $clientIP ) );
			$ipSingleAddress = trim($this->componentConfig->get ( 'ipaddress', '' ));
			$validIpSingleAddress = preg_match ( $ipAddressRegex, $ipSingleAddress );
			if ($validIpSingleAddress) {
				$singleIpDec = ( float ) sprintf ( "%u", ip2long ( $ipSingleAddress ) );
				if ($singleIpDec == $clientIpDec) {
					return false;
				}
			}
		} else {
			$clientIpDec = '';
		}
		
		// IPv6 exclusion logic
		$ipv6SingleAddress = trim($this->componentConfig->get('ipv6address', ''));
		if (strpos($clientIP, ':') !== false && $ipv6SingleAddress) {
			$clientPacked = @inet_pton($clientIP);
			$ipv6Packed   = @inet_pton($ipv6SingleAddress);
			
			if ($clientPacked !== false && $ipv6Packed !== false && $clientPacked === $ipv6Packed) {
				return false;
			}
		}
		
		// Check for IP range exclusions
		$ipStart = trim($this->componentConfig->get ( 'iprange_start', '' ));
		$ipEnd = trim($this->componentConfig->get ( 'iprange_end', '' ));
		$validIpRangeStart = preg_match ( $ipAddressRegex, $ipStart );
		$validIpRangeEnd = preg_match ( $ipAddressRegex, $ipEnd );
		if ($validIpRangeStart && $validIpRangeEnd) {
			$lowerIpDec = ( float ) sprintf ( "%u", ip2long ( $ipStart ) );
			$upperIpDec = ( float ) sprintf ( "%u", ip2long ( $ipEnd ) );
			if (($clientIpDec >= $lowerIpDec) && ($clientIpDec <= $upperIpDec)) {
				return false;
			}
		}
		
		// IPv6 range exclusion logic
		$ipv6Start = trim($this->componentConfig->get('ipv6range_start', ''));
		$ipv6End = trim($this->componentConfig->get('ipv6range_end', ''));
		
		if (strpos($clientIP, ':') !== false && $ipv6Start && $ipv6End) {
			$clientPacked = @inet_pton($clientIP);
			$startPacked = @inet_pton($ipv6Start);
			$endPacked = @inet_pton($ipv6End);
			
			if ($clientPacked !== false && $startPacked !== false && $endPacked !== false) {
				// Binary-safe range check
				if (strcmp($clientPacked, $startPacked) >= 0 && strcmp($clientPacked, $endPacked) <= 0) {
					return false;
				}
			}
		}
		
		// Check for multiple IP exclusions
		$ipMultipleAddresses = $this->componentConfig->get ( 'ip_multiple', null);
		// Check if data are not null
		if($ipMultipleAddresses) {
			// Try to load every range, one per row
			$explodeIpsRows = explode(PHP_EOL, $ipMultipleAddresses);
			if(!empty($explodeIpsRows)) {
				foreach ($explodeIpsRows as $singleCycledIpAddress) {
					$singleCycledIpAddress = trim($singleCycledIpAddress);
					$validSingleCycledIpAddress = preg_match ( $ipAddressRegex, $singleCycledIpAddress );
					if($validSingleCycledIpAddress) {
						$validSingleCycledIpAddressDec = ( float ) sprintf ( "%u", ip2long ( $singleCycledIpAddress ) );
						if ($validSingleCycledIpAddressDec == $clientIpDec) {
							return false;
						}
					}
				}
			}
		}
		
		// IPv6 multiple IP exclusions
		$ipv6MultipleAddresses = $this->componentConfig->get('ipv6_multiple', null);
		// Check if data is not null and the current IP is IPv6
		if ($ipv6MultipleAddresses && strpos($clientIP, ':') !== false) {
			$explodeIpv6Rows = explode(PHP_EOL, $ipv6MultipleAddresses);
			if (!empty($explodeIpv6Rows)) {
				$clientPacked = @inet_pton($clientIP);
				if ($clientPacked !== false) {
					foreach ($explodeIpv6Rows as $singleIpv6) {
						$singleIpv6 = trim($singleIpv6);
						if ($singleIpv6) {
							$ipv6Packed = @inet_pton($singleIpv6);
							if ($ipv6Packed !== false && $clientPacked === $ipv6Packed) {
								return false;
							}
						}
					}
				}
			}
		}
		
		// Check for IP multiple ranges exclusions
		$ipRanges = $this->componentConfig->get ( 'iprange_multiple', null);
		// Check if data are not null
		if($ipRanges) {
			// Try to load every range, one per row
			$explodeRows = explode(PHP_EOL, $ipRanges);
			if(!empty($explodeRows)) {
				foreach ($explodeRows as $singleRange) {
					// Try to detect single range
					$explodeRange = explode('-', $singleRange);
					if(!empty($explodeRange) && count($explodeRange) == 2) {
						$ipStart = trim($explodeRange[0]);
						$ipEnd = trim($explodeRange[1]);
						$validIpRangeStart = preg_match ( $ipAddressRegex, $ipStart );
						$validIpRangeEnd = preg_match ( $ipAddressRegex, $ipEnd );
						if ($validIpRangeStart && $validIpRangeEnd) {
							$lowerIpDec = ( float ) sprintf ( "%u", ip2long ( $ipStart ) );
							$upperIpDec = ( float ) sprintf ( "%u", ip2long ( $ipEnd ) );
							if (($clientIpDec >= $lowerIpDec) && ($clientIpDec <= $upperIpDec)) {
								return false;
							}
						}
					}
				}
			}
		}
		
		// IPv6 multiple ranges exclusions
		$ipv6Ranges = $this->componentConfig->get('ipv6range_multiple', null);
		if ($ipv6Ranges && strpos($clientIP, ':') !== false) {
			$explodeIpv6Rows = explode(PHP_EOL, $ipv6Ranges);
			if (!empty($explodeIpv6Rows)) {
				$clientPacked = @inet_pton($clientIP);
				if ($clientPacked !== false) {
					foreach ($explodeIpv6Rows as $ipv6Range) {
						$ipv6Range = trim($ipv6Range);
						if (strpos($ipv6Range, '-') !== false) {
							list($rangeStart, $rangeEnd) = explode('-', $ipv6Range, 2);
							$rangeStart = trim($rangeStart);
							$rangeEnd = trim($rangeEnd);
							if ($rangeStart && $rangeEnd) {
								$startPacked = @inet_pton($rangeStart);
								$endPacked = @inet_pton($rangeEnd);
								if ($startPacked !== false && $endPacked !== false) {
									if (strcmp($clientPacked, $startPacked) >= 0 && strcmp($clientPacked, $endPacked) <= 0) {
										return false;
									}
								}
							}
						}
					}
				}
			}
		}
		
		// Check for pages exclusions
		$menu = $this->appInstance->getMenu ()->getActive ();
		if (is_object ( $menu )) {
			$menuItemid = $menu->id;
			$menuExcluded = $this->componentConfig->get ( 'daemon_exclusions' );
			if (is_array ( $menuExcluded ) && ! in_array ( 0, $menuExcluded, false ) && in_array ( $menuItemid, $menuExcluded )) {
				return false;
			}
		}
		
		// Check for users groups exclusions
		$groupsExclusions = $this->componentConfig->get ( 'groups_exclusions', array ('0') );
		if (is_array ( $groupsExclusions ) && ! in_array ( 0, $groupsExclusions, false )) {
			// Check for user groups current user belong to
			$userGroups = $this->myUser->getAuthorisedGroups ();
			// Intersect to recognize groups
			$intersectResult = array_intersect ( $userGroups, $groupsExclusions );
			$isInExcludedGroup = ( bool ) (count ( $intersectResult ));
			
			// Eventually limit query to users that belong to groups
			if ($isInExcludedGroup) {
				return false;
			}
		}
		
		// Check for the country code exclusion
		$countryCodeExclusions = $this->componentConfig->get ( 'countrycode_exclusions', array ('0') );
		if (is_array ( $countryCodeExclusions ) && ! in_array ( '0', $countryCodeExclusions, true )) {
			// Check for the user country code
			$userCountryCode = $this->getUserCountry();
			$isInExcludedCountry = in_array($userCountryCode, $countryCodeExclusions, false);

			// Eventually limit query to users that belong to groups
			if ($isInExcludedCountry) {
				return false;
			}
		}
		
		// Check for user agent exclusion
		if(isset($_SERVER ['HTTP_USER_AGENT'])) {
			$useragent = $_SERVER ['HTTP_USER_AGENT'];
			
			// Check if empty useragents are excluded
			if(!$useragent && $this->componentConfig->get('useragent_empty_exclude', 0)) {
				return false;
			}
			
			$useragentsExcludedString = trim($this->componentConfig->get('useragents_exclusions', 'googlebot,bingbot'));
			$useragentsExcluded = explode(',', $useragentsExcludedString);
			if(!empty($useragentsExcluded) && $useragentsExcludedString) {
				foreach ($useragentsExcluded as $useragentExcluded) {
					$cleanedUseragentExcluded = strtolower(trim($useragentExcluded));
					if(stripos($useragent, $cleanedUseragentExcluded) !== false) {
						return false;
					}
				}
			}
		}
		
		return true;
	}
	
	/**
	 * Validate cronjob report by email
	 *
	 * @access private
	 * @param Object $doc
	 * @return boolean
	 */
	private function validateReporting($doc) {
		// Session object
		$session = $this->appInstance->getSession();

		if(!$this->componentConfig->get('report_byemail', 0)) {
			return false;
		}

		if(!$this->appInstance->isClient('site')) {
			return false;
		}

		if($doc->getType () !== 'html') {
			return false;
		}

		if($this->appInstance->getInput()->getCmd ( 'tmpl' ) == 'component') {
			return false;
		}

		if($session->get('cronjob_user', false)) {
			return false;
		} else {
			$session->set('cronjob_user', true);
		}
		
		$cronjobFilename = JPATH_ROOT . '/components/com_jrealtimeanalytics/cronjob.txt';
		$frequency = (int) $this->componentConfig->get('report_frequency', 7);
		
		// Monthly scheduler: send only on last day of month and only once per month
		if($frequency === 30) {
			$isLastDayOfMonth = (date('j') == date('t'));
			if(!$isLastDayOfMonth) {
				return false;
			}
			
			$currentMonthKey = 'monthly:' . date('Y-m');
			if(!file_exists($cronjobFilename)) {
				file_put_contents($cronjobFilename, $currentMonthKey);
				return true;
			}
			
			$lastSend = trim((string) file_get_contents($cronjobFilename));
			if($lastSend === $currentMonthKey) {
				return false;
			}
			
			file_put_contents($cronjobFilename, $currentMonthKey);
			return true;
		}
		
		if(!file_exists($cronjobFilename)) {
			file_put_contents($cronjobFilename, date('Y-m-d'));
			return true;
		} else {
			$lastSend = trim((string) file_get_contents($cronjobFilename));
			// Validate sending for the current frequency
			$now = time();
			$lastSendTimestamp = strtotime($lastSend);
			$expireTime = strtotime("+ " . $frequency . " days", $lastSendTimestamp ?: 0);
			// Cronjob expired, new send email and update date on file for last send
			if($expireTime < $now) {
				file_put_contents($cronjobFilename, date('Y-m-d'));
				return true;
			}
		}

		return false;
	}
	
	/**
	 * CURL based file_get_contents
	 *
	 * @var string
	 * @access private
	 */
	private function curl_get_contents ($url) {
		if (!function_exists('curl_init')){
			return false;
		}
		$ch = curl_init();
		curl_setopt($ch, CURLOPT_URL, $url);
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
		$output = curl_exec($ch);
		return $output;
	}
	
	/**
	 * Get the user country code for filtering
	 *
	 * @access private
	 * @return string
	 */
	private function getUserCountry() {
		$code = null;
		$isBot = false;
		
		// Prevent IP based geolocation if a bot is detected avoiding to be blacklisted by geoplugin.net > 120/min
		if (isset ( $_SERVER ['HTTP_USER_AGENT'] )) {
			$user_agent = $_SERVER ['HTTP_USER_AGENT'];
			$botRegexPattern = "(Googlebot\/|Googlebot\-Mobile|Googlebot\-Image|Google favicon|Mediapartners\-Google|bingbot|slurp|java|wget|curl|Commons\-HttpClient|Python\-urllib|libwww|httpunit|nutch|phpcrawl|msnbot|jyxobot|FAST\-WebCrawler|FAST Enterprise Crawler|biglotron|teoma|convera|seekbot|gigablast|exabot|ngbot|ia_archiver|GingerCrawler|webmon |httrack|webcrawler|grub\.org|UsineNouvelleCrawler|antibot|netresearchserver|speedy|fluffy|bibnum\.bnf|findlink|msrbot|panscient|yacybot|AISearchBot|IOI|ips\-agent|tagoobot|MJ12bot|dotbot|woriobot|yanga|buzzbot|mlbot|yandexbot|purebot|Linguee Bot|Voyager|CyberPatrol|voilabot|baiduspider|citeseerxbot|spbot|twengabot|postrank|turnitinbot|scribdbot|page2rss|sitebot|linkdex|Adidxbot|blekkobot|ezooms|dotbot|Mail\.RU_Bot|discobot|heritrix|findthatfile|europarchive\.org|NerdByNature\.Bot|sistrix crawler|ahrefsbot|Aboundex|domaincrawler|wbsearchbot|summify|ccbot|edisterbot|seznambot|ec2linkfinder|gslfbot|aihitbot|intelium_bot|facebookexternalhit|yeti|RetrevoPageAnalyzer|lb\-spider|sogou|lssbot|careerbot|wotbox|wocbot|ichiro|DuckDuckBot|lssrocketcrawler|drupact|webcompanycrawler|acoonbot|openindexspider|gnam gnam spider|web\-archive\-net\.com\.bot|backlinkcrawler|coccoc|integromedb|content crawler spider|toplistbot|seokicks\-robot|it2media\-domain\-crawler|ip\-web\-crawler\.com|siteexplorer\.info|elisabot|proximic|changedetection|blexbot|arabot|WeSEE:Search|niki\-bot|CrystalSemanticsBot|rogerbot|360Spider|psbot|InterfaxScanBot|Lipperhey SEO Service|CC Metadata Scaper|g00g1e\.net|GrapeshotCrawler|urlappendbot|brainobot|fr\-crawler|binlar|SimpleCrawler|Livelapbot|Twitterbot|cXensebot|smtbot|bnf\.fr_bot|A6\-Indexer|ADmantX|Facebot|Twitterbot|OrangeBot|memorybot|AdvBot|MegaIndex|SemanticScholarBot|ltx71|nerdybot|xovibot|BUbiNG|Qwantify|archive\.org_bot|Applebot|TweetmemeBot|crawler4j|findxbot|SemrushBot|yoozBot|lipperhey|y!j\-asr|Domain Re\-Animator Bot|AddThis)";
			$isBot = preg_match("/{$botRegexPattern}/i", $user_agent);
		}
		
		// IP based geolocation by default
		if(!$isBot && $this->componentConfig->get('geolocation_mode', 'ip') == 'ip') {
			$geolocationService = $this->componentConfig->get('stats_geolocation_service', 'geoplugin') == 'geoplugin' ? 'http://ip-api.com/json/' : 'https://json.geoiplookup.io/';
			$IPAddress = $this->componentConfig->get('cloudflare_ip_masking', 0) == 0 ? $_SERVER['REMOTE_ADDR'] : @$_SERVER['HTTP_CF_CONNECTING_IP'];
			
			// Evaluate the IP address masking or alternatives
			if($serverIpOverride = $this->componentConfig->get('server_ip_override', '')) {
				if($serverIpOverride == 'realip') {
					$IPAddress = @$_SERVER['HTTP_X_REAL_IP'];
				} elseif($serverIpOverride == 'remoteip') {
					$IPAddress = @$_SERVER['HTTP_X_REMOTE_IP'];
				}
			}
			
			if($this->componentConfig->get('geolocation_php_func', 'file') == 'curl') {
				$webServiceResponse = $this->curl_get_contents($geolocationService . $IPAddress);
			} else {
				$webServiceResponse = file_get_contents($geolocationService . $IPAddress);
			}
			if($webServiceResponse) {
				$decodedResponse = json_decode($webServiceResponse);
				$code = isset($decodedResponse->countryCode) ? $decodedResponse->countryCode : (isset($decodedResponse->country_code) ? $decodedResponse->country_code : null);
			}
		}
		
		// Browser header based geolocation, by param or fallback
		if(($this->componentConfig->get('geolocation_mode', 'ip') == 'browser') || !$code || ($code == 'XX') || ($code == 'ZZ')) {
			$chunkHttpAcceptHeader = $_SERVER ['HTTP_ACCEPT_LANGUAGE'];
			// Patch per header HTTP Internet Explorer
			if (strlen ( $chunkHttpAcceptHeader ) > 2) {
				$spliced = explode ( '-', $chunkHttpAcceptHeader );
				$code = substr ( $spliced [1], 0, 2 );
			} else {
				$code = $chunkHttpAcceptHeader;
			}
		}
		return strtoupper ( $code );
	}
	
	/**
	 * Dispatch handler
	 *
	 * @access private
	 * @return void
	 */
	private function startApp() {
		// Get document
		$doc = $this->appInstance->getDocument();
		
		// Evaluate and add a js snippet to inject an async iframe to execute the cronjob report emailer
		if($this->validateReporting($doc)) {
			$livesite = rtrim(Uri::root(), '/');
			$reportFormat = $this->componentConfig->get('report_format', 'emailxls');

			$jsSnippet = "var iframeReport = document.createElement('iframe');
						  iframeReport.style.display = 'none';
						  iframeReport.src = '$livesite/index.php?option=com_jrealtimeanalytics&task=serverstats.$reportFormat&tmpl=component';
						  document.body.appendChild(iframeReport);

						  var iframeReportOverview = document.createElement('iframe');
						  iframeReportOverview.style.display = 'none';
						  iframeReportOverview.src = '$livesite/index.php?option=com_jrealtimeanalytics&task=overlook.emailpdf&tmpl=component';
						  document.body.appendChild(iframeReportOverview);

						  var iframeReportWebmasters = document.createElement('iframe');
						  iframeReportWebmasters.style.display = 'none';
						  iframeReportWebmasters.src = '$livesite/index.php?option=com_jrealtimeanalytics&task=webmasters.emailxls&tmpl=component';
						  document.body.appendChild(iframeReportWebmasters);";
			
			
			// Output JS snippet nel Document
			if($this->componentConfig->get('report_script_jquery_onload_event', 0)) {
				$doc->getWebAssetManager()->addInlineScript("jQuery(window).on('load', function(){ $jsSnippet });");
			} else {
				$doc->getWebAssetManager()->addInlineScript("window.onload = function() { $jsSnippet };");
			}
		}
		
		// Execute heatmap app, in this case skip entirely the tracking app
		if ($this->appInstance->getInput()->getInt('jes_heatmap') === 1 && 
			$this->appInstance->getInput()->getString('token') == md5(date('Y-m-d')) &&
			$this->appInstance->isClient ('site') && $doc->getType () === 'html') {
			$this->injectCustomStyles ( $doc );
			return true;
		}
		
		// Ensure tracking is valid
		$validTrack = $this->validateTracking ();
		
		// Execute solo nel frontend
		if ($validTrack) {
			return $this->injectApp ( $this->componentConfig, $doc );
		}
		
		return true;
	}
	
	/**
	 * onBeforeCompileHead handler
	 *
	 * @param Event $event
	 * @access	public
	 * @return null
	 */
	public function injectAppBeforeCompileHead(Event $event) {
		if($this->appInstance->isClient('administrator')) {
			// Redirect to the component configuration if the Joomla global configuration is requested instead
			$dispatchedComponent = $this->appInstance->getInput()->get ( 'option' );
			$dispatchedView = $this->appInstance->getInput()->get ( 'view' );
			$componentConfig = $this->appInstance->getInput()->get ( 'component' );
			if($dispatchedComponent == 'com_config' && $dispatchedView == 'component' && $componentConfig == 'com_jrealtimeanalytics') {
				$this->appInstance->redirect(Route::_('index.php?option=com_jrealtimeanalytics&task=config.display', false));
				return;
			}
		}
		
		if($this->appInstance->isClient('site') && $this->componentConfig->get('start_application_event', 'afterdispatch') == 'beforecompilehead') {
			$this->startApp();
		}
	}
	
	/**
	 * onAfterInitialise handler
	 * Retain 'afterroute' for J3 B/C upgrade
	 *
	 * @param Event $event
	 * @access	public
	 * @return null
	 */
	public function injectAppAfterDispatch(Event $event) {
		if($this->appInstance->isClient('site') && ($this->componentConfig->get('start_application_event', 'afterdispatch') == 'afterdispatch' ||
											$this->componentConfig->get('start_application_event', 'afterdispatch') == 'afterroute')) {
			$this->startApp();
		}
	}
	
	/**
	 * Pseudo onContentSearch for finder smart search
	 * 
	 * @param Event $event
	 * @subparam   string  $text      Target search string.
	 * @subparam   string  $phrase    Matching option (possible values: exact|any|all).  Default is "any".
	 * @subparam   string  $ordering  Ordering option (possible values: newest|oldest|popular|alpha|category).  Default is "newest".
	 * @subparam   mixed   $areas     An array if the search it to be restricted to areas or null to search all areas.
	 * @access public
	 * @return array
	 */
	public function trackContentSearch(Event $event) {
		// subparams: $text, $phrase, $ordering, $areas
		$arguments = $event->getArguments();
		$text = $event->getArgument(0);
		$phrase = $event->getArgument(1);
		$ordering = $event->getArgument(2);
		$areas = $event->getArgument(3);
		
		// Exclude always for backend
		if (!$this->appInstance->isClient('site')) {
			return false;
		}
		
		$doc = $this->appInstance->getDocument();
		// Output JS APP nel Document esclusione
		if ($doc->getType () !== 'html' || $this->appInstance->getInput()->getCmd ( 'tmpl' ) === 'component') {
			return false;
		}
		
		// Discard other params and take only text keywords
		if (trim ( $text )) {
			$notifications = $this->notifyObservers ( array (
					array (
							'searchdispatch',
							$text 
					) 
			) );
			
			// Return notifications/Exception if called from this plugin
			if ($phrase == 'jrealtime_plugin') {
				return $notifications;
			} else {
				// This method has been called from Joomla search return a dummy void and manage Exception here if any
				if ($this->componentConfig->get ( 'enable_debug', false )) {
					foreach ( $notifications as $exception ) {
						// Found an exception, set into app response message queue for client side debug
						if ($exception instanceof \JExtstore\Component\JRealtimeAnalytics\Administrator\Framework\Exception) {
							$this->appInstance->enqueueMessage ( 'Error JRealtime Search', $exception->getExceptionLevel () );
						}
					}
				}
				return;
			}
		}
	}

	/**
	 * Manage the Joomla session metadata in all cases even if disabled by the global configuration
	 *
	 * @param Event $event
	 * @access public
	 * @return void
	 */
	public function manageSessionMetadata(Event $event) {
		// Check if the plugin has not been stopped by the constructor
		if ($this->isPluginStopped) {
			$dispatcher = $this->appInstance->getDispatcher();
			$dispatcher->removeSubscriber ( $this );
			return;
		}

		// Always force session metadata tracking
		if (! $this->appInstance->getConfig ()->get ( 'session_metadata', 1 ) || ! $this->appInstance->getConfig ()->get ( 'session_metadata_for_guest', 0 )) {
			$container = Factory::getContainer ();
			$session = $this->appInstance->getSession ();
			$user = $this->appInstance->getIdentity ();
			if ($user) {
				$container->get ( \Joomla\CMS\Session\MetadataManager::class )->createOrUpdateRecord ( $session, $user );
			}
		}
	}
	
	/**
	 * Event to manipulate the menu item dashboard in backend
	 *
	 * @param Event $event
	 * @subparam   array  &$policy  The privacy policy status data, passed by reference, with keys "published" and "editLink"
	 *
	 * @return  void
	 */
	public function processMenuItemsDashboard(Event $event) {
		// subparams: $context, $items
		$arguments = $event->getArguments();
		$context = isset($arguments[0]) ? $event->getArgument(0) : $event->getArgument('context');
		$items = isset($arguments[1]) ? $event->getArgument(1) : $event->getArgument('subject');
		
		if(!empty($items) && $context == 'administrator.module.mod_submenu') {
			foreach ($items as $item) {
				if($item->element == 'com_jrealtimeanalytics') {
					$item->img = Uri::base() . 'components/com_jrealtimeanalytics/images/jrealtime-16x16.png';
					$item->title = 'COM_JREALTIMEANALYTICS_DASHBOARD_TITLE';
				}
			}
		}
		
		// Kill com_joomlaupdate informations about extensions missing updater info, leave only main one
		$document = $this->appInstance->getDocument();
		if(!$this->appInstance->get('jextstore_joomlaupdate_script') && $this->appInstance->getInput()->get('option') == 'com_joomlaupdate' && !$this->appInstance->getInput()->get('view') && !$this->appInstance->getInput()->get('task')) {
			$document->getWebAssetManager()->addInlineScript ("
				window.addEventListener('DOMContentLoaded', function(e) {
					if(document.querySelector('#preupdatecheck')) {
						var jextensionsIntervalCount = 0;
						var jextensionsIntervalTimer = setInterval(function() {
						    [].slice.call(document.querySelectorAll('#compatibilityTable1 tbody tr th.exname')).forEach(function(th) {
						        let txt = th.innerText;
						        if (txt && txt.toLowerCase().match(/jsitemap|gdpr|gptranslate|jpagebuilder|responsivizer|jchatsocial|jcomment|jshortcodes|jrealtime|jspeed|jredirects|vsutility|visualstyles|visual\sstyles|instant\sfacebook\slogin|instantpaypal|screen\sreader|jspeed|jamp/i)) {
						            th.parentElement.style.display = 'none';
						            th.parentElement.classList.remove('error');
									th.parentElement.classList.add('jextcompatible');
						        }
						    });
							[].slice.call(document.querySelectorAll('#compatibilityTable2 tbody tr th.exname')).forEach(function(th) {
						        let txt = th.innerText;
						        if (txt && txt.toLowerCase().match(/jsitemap|gdpr|gptranslate|jpagebuilder|responsivizer|jchatsocial|jcomment|jshortcodes|jrealtime|jspeed|jredirects|vsutility|visualstyles|visual\sstyles|instant\sfacebook\slogin|instantpaypal|screen\sreader|jspeed|jamp/i)) {
									th.parentElement.classList.remove('error');
									th.parentElement.classList.add('jextcompatible');
						            let smallDiv = th.querySelector(':scope div.small');
									if(smallDiv) {
										smallDiv.style.display = 'none';
									}
						        }
						    });
							if (document.querySelectorAll('#compatibilityTable0 tbody tr').length == 0 &&
								document.querySelectorAll('#compatibilityTable1 tbody tr:not(.jextcompatible)').length == 0 &&
								document.querySelectorAll('#compatibilityTable2 tbody tr:not(.jextcompatible)').length == 0) {
						        [].slice.call(document.querySelectorAll('#preupdatecheckbox, #preupdateCheckCompleteProblems')).forEach(function(element) {
						            element.style.display = 'none';
						        });
								if(document.querySelector('#noncoreplugins')) {
									document.querySelector('#noncoreplugins').checked = true;
								}
								if(document.querySelector('button.submitupdate')) {
							        document.querySelector('button.submitupdate').disabled = false;
							        document.querySelector('button.submitupdate').classList.remove('disabled');
								}
								if(document.querySelector('#joomlaupdate-precheck-extensions-tab span.fa')) {
									let tabIcon = document.querySelector('#joomlaupdate-precheck-extensions-tab span.fa');
									tabIcon.classList.remove('fa-times');
									tabIcon.classList.remove('text-danger');
									tabIcon.classList.remove('fa-exclamation-triangle');
									tabIcon.classList.remove('text-warning');
									tabIcon.classList.add('fa-check');
									tabIcon.classList.add('text-success');
								}
						    };
					
							if (document.querySelectorAll('#compatibilityTable0 tbody tr').length == 0) {
								if(document.querySelectorAll('#compatibilityTable1 tbody tr:not(.jextcompatible)').length == 0) {
									let compatibilityTable1 = document.querySelector('#compatibilityTable1');
									if(compatibilityTable1) {
										compatibilityTable1.style.display = 'none';
									}
								}
								clearInterval(jextensionsIntervalTimer);
							}
					
						    jextensionsIntervalCount++;
						}, 1000);
					};
				});");
			$this->appInstance->set('jextstore_joomlaupdate_script', true);
		}
	}
	
	/** Manage the Joomla updater based on the user license
	 * 
	 * @param Event $event
	 * @subparam   string  The $url for the package update download
	 * @subparam   array  The headers array.
	 * @access public
	 * @return void
	 */
	public function jrealtimeUpdateInstall(Event $event) {
		// subparams: &$url, &$headers
		$arguments = $event->getArguments();
		$url = isset($arguments[0]) ? $event->getArgument(0) : $event->getArgument('url');
		$headers = isset($arguments[1]) ? $event->getArgument(1) : $event->getArgument('headers');
		
		$uri 	= Uri::getInstance($url);
		$parts 	= explode('/', $uri->getPath());
		if ($uri->getHost() == 'storejextensions.org' && in_array('com_jrealtimeanalytics.zip', $parts)) {
			// Init as false unless the license is valid
			$validUpdate = false;
				
			// Manage partial language translations
			$jLang = $this->appInstance->getLanguage();
			$jLang->load('com_jrealtimeanalytics', JPATH_BASE . '/components/com_jrealtimeanalytics', 'en-GB', true, true);
			if($jLang->getTag() != 'en-GB') {
				$jLang->load('com_jrealtimeanalytics', JPATH_BASE, null, true, false);
				$jLang->load('com_jrealtimeanalytics', JPATH_BASE . '/components/com_jrealtimeanalytics', null, true, false);
			}
				
			// Email license validation API call and &$url building construction override
			$cParams = ComponentHelper::getParams('com_jrealtimeanalytics');
			$registrationEmail = $cParams->get('registration_email', null);
				
			// License
			if($registrationEmail) {
				$prodCode = 'jrealtime';
				$cdFuncUsed = 'str_' . 'ro' . 't' . '13';
	
				// Retrieve license informations from the remote REST API
				$apiResponse = null;
				$apiEndpoint = $cdFuncUsed('uggc' . '://' . 'fgberwrkgrafvbaf' . '.bet') . "/option,com_easycommerce/action,licenseCode/email,$registrationEmail/productcode,$prodCode";
				if (function_exists('curl_init')){
					$ch = curl_init();
					curl_setopt($ch, CURLOPT_URL, $apiEndpoint);
					curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
					$apiResponse = curl_exec($ch);
				}
				$objectApiResponse = json_decode($apiResponse);
	
				if(!is_object($objectApiResponse)) {
					// Message user about error retrieving license informations
					$this->appInstance->enqueueMessage(Text::_('COM_JREALTIME_ERROR_RETRIEVING_LICENSE_INFO'));
				} else {
					if(!$objectApiResponse->success) {
						switch ($objectApiResponse->reason) {
							// Message user about the reason the license is not valid
							case 'nomatchingcode':
								$this->appInstance->enqueueMessage(Text::_('COM_JREALTIME_LICENSE_NOMATCHING'));
								break;
	
							case 'expired':
								// Message user about license expired on $objectApiResponse->expireon
								$this->appInstance->enqueueMessage(Text::sprintf('COM_JREALTIME_LICENSE_EXPIRED', $objectApiResponse->expireon));
								break;
						}
							
					}
						
					// Valid license found, builds the URL update link and message user about the license expiration validity
					if($objectApiResponse->success) {
						$url = $cdFuncUsed('uggc' . '://' . 'fgberwrkgrafvbaf' . '.bet' . '/XZY1402ZQOFnifs3243564832knfnxj35td1rtt1404q.ugzy');
						// Joomla 5+ native BeforePackageDownloadEvent
						if(method_exists($event, 'updateUrl')) {
							$event->updateUrl($url);
						} else {
							// Fallback to generic Event up to Joomla 4
							$event->setArgument(0, $url);
						}
						$validUpdate = true;
						$this->appInstance->enqueueMessage(Text::sprintf('COM_JREALTIME_EXTENSION_UPDATED_SUCCESS', $objectApiResponse->expireon));
					}
				}
			} else {
				// Message user about missing email license code
				$this->appInstance->enqueueMessage(Text::sprintf('COM_JREALTIME_MISSING_REGISTRATION_EMAIL_ADDRESS', OutputFilter::ampReplace('index.php?option=com_jrealtimeanalytics&task=config.display#_licensepreferences')));
			}
				
			if(!$validUpdate) {
				$this->appInstance->enqueueMessage(Text::_('COM_JREALTIME_UPDATER_STANDARD_ADVISE'), 'notice');
			}
		}
	}
	
	/**
	 * Returns an array of events this subscriber will listen to.
	 *
	 * @return  array
	 *
	 * @since 4.0.0
	 */
	public static function getSubscribedEvents(): array {
		return [
				'onAfterInitialise' => 'manageSessionMetadata',
				'onAfterDispatch' => 'injectAppAfterDispatch',
				'onBeforeCompileHead' => 'injectAppBeforeCompileHead',
				'onPreprocessMenuItems' => 'processMenuItemsDashboard',
				'onInstallerBeforePackageDownload' => 'jrealtimeUpdateInstall'
		];
	}
	
	/**
	 * Override registers Listeners to the Dispatcher
	 * It allows to legacy stop a plugin execution based on the return value of its constructor
	 *
	 * @override
	 * @return  void
	 */
	public function registerListeners(?DispatcherInterface $dispatcher = null) {
		// Check if the plugin has not been stopped by the constructor
		if(!$this->isPluginStopped) {
			parent::registerListeners($dispatcher);
		}
	}
	
	/**
	 * Constructor
	 *
	 * @param
	 *        	object &$subject The object to observe
	 * @param array $config
	 *        	An optional associative array of configuration settings.
	 *        	Recognized key values include 'name', 'group', 'params', 'language'
	 *        	(this list is not meant to be comprehensive).
	 *        	
	 * @since 1.5
	 */
	public function __construct($subject, $config = []) {
		parent::__construct ( $subject, $config );
		
		// Init application
		$this->appInstance = Factory::getApplication();
		
		// Init database
		$this->dbInstance = Factory::getContainer()->get('DatabaseDriver');
		
		// Exclude always the api client
		if ($this->appInstance->isClient ('api')) {
			$this->isPluginStopped = true;
			return;
		}
		
		$this->myUser = $this->appInstance->getIdentity ();
		$this->componentConfig = ComponentHelper::getParams ( 'com_jrealtimeanalytics' );
	}
}