<?php
/*
 * @package 	RSFirewall!
 * @copyright 	(c) 2009 - 2024 RSJoomla!
 * @link 		https://www.rsjoomla.com/joomla-extensions/joomla-security.html
 * @license 	GNU General Public License https://www.gnu.org/licenses/gpl-3.0.en.html
 */

\defined('_JEXEC') or die;

use Joomla\CMS\Plugin\CMSPlugin;
use Joomla\CMS\Factory;
use Joomla\CMS\Table\Table;
use Joomla\CMS\Version;
use Joomla\CMS\Language\Text;
use Joomla\CMS\Uri\Uri;
use Joomla\CMS\Authentication\Authentication;
use Joomla\CMS\Component\ComponentHelper;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\MVC\View\HtmlView;
use Joomla\CMS\MVC\Model\BaseDatabaseModel;
use Joomla\Filesystem\Folder;

class plgSystemRsfirewall extends CMSPlugin
{
	protected $config;
	protected $ip;
	protected $referer;
	protected $agent = '';
	protected $reason = '';
	protected $files;
	protected $logger;
	protected $option;

	public function __construct($subject, $config = array()) {
		parent::__construct($subject, $config);

		// try to load the configuration class
		if ($this->_autoLoadClass('RSFirewallConfig')) {
			try
			{
				$this->config = RSFirewallConfig::getInstance();
			}
			catch (Exception $e)
			{
				$this->config = false;
				Factory::getApplication()->enqueueMessage($e->getMessage(), 'error');
				return;
			}
		}

		Table::addIncludePath(JPATH_ADMINISTRATOR.'/components/com_rsfirewall/tables');

		$input = Factory::getApplication()->input;

		$this->agent   = $input->server->get('HTTP_USER_AGENT', '', 'raw'); // user agent
		$this->referer = $input->server->get('HTTP_REFERER', '', 'raw');
		$this->ip 	   = $this->getIP(); // ip address

		if ($this->_autoLoadClass('RSFirewallLogger')) {
			$this->logger = RSFirewallLogger::getInstance();
		}
	}

	public function onAfterInitialise()
	{
		// load the languages
		Factory::getLanguage()->load('com_rsfirewall', JPATH_SITE);
		Factory::getLanguage()->load('com_rsfirewall', JPATH_ADMINISTRATOR);
	}

	protected function getOption() {
		return Factory::getApplication()->input->get('option');
	}

	protected function _autoLoadClass($class) {
		// class already has been loaded
		if (class_exists($class)) {
			return true;
		}

		$path = JPATH_ADMINISTRATOR.'/components/com_rsfirewall/helpers';

		// try to load
		switch ($class) {
			case 'RSFirewallConfig':
				if (file_exists($path.'/config.php')) {
					require_once $path.'/config.php';
					return true;
				}
				return false;
				break;

			case 'RSFirewallLogger':
				if (file_exists($path.'/log.php')) {
					require_once $path.'/log.php';
					return true;
				}
				return false;
				break;

			case 'RSFirewallXSSHelper':
				if (file_exists($path.'/xss.php')) {
					require_once $path.'/xss.php';
					return true;
				}
				return false;
				break;

			case 'RSFirewallSnapshot':
				if (file_exists($path.'/snapshot.php')) {
					require_once $path.'/snapshot.php';
					return true;
				}
				return false;
				break;

			case 'RSFirewallReplacer':
				if (file_exists($path.'/replacer.php')) {
					require_once $path.'/replacer.php';
					return true;
				}
				return false;
				break;

			case 'RSFirewallCaptcha':
				if (file_exists($path.'/captcha.php')) {
					require_once $path.'/captcha.php';
					return true;
				}
				return false;
				break;

			case 'RSFirewallProtection':
				if (file_exists($path.'/protection.php')) {
					require_once $path.'/protection.php';
					return true;
				}
				return false;
				break;
		}

		return false;
	}

	protected function getIP() {
		require_once JPATH_ADMINISTRATOR.'/components/com_rsfirewall/helpers/ip/ip.php';

		return RSFirewallIP::get();
	}

	protected function isBlacklisted() {
		$result = $this->isListed(0);

		if ($result && $this->config->get('enable_extra_logging')) {
			$this->logger->add('low', 'IP_BLOCKED');
		}

		return $result;
	}

	protected function isWhitelisted() {
		return $this->isListed(1);
	}

	protected function isListed($type) {
		static $cache = array();

		if (!isset($cache[$type])) {
			$db	 	= Factory::getDbo();
			$query 	= $db->getQuery(true);

			// defaults to false
			$cache[$type] = false;

			$query->select($db->qn('ip'))
				->select($db->qn('reason'))
				->from('#__rsfirewall_lists')
				->where('('.$db->qn('ip').'='.$db->q($this->ip).' OR '.$db->qn('ip').' LIKE '.$db->q('%*%').' OR '.$db->qn('ip').' LIKE '.$db->q('%/%').' OR '.$db->qn('ip').' LIKE '.$db->q('%-%').')')
				->where($db->qn('type').'='.$db->q($type))
				->where($db->qn('published').'='.$db->q(1));
			$db->setQuery($query);

			if ($results = $db->loadObjectList()) {
				try {
					$class = new RSFirewallIP($this->ip);
					foreach ($results as $result) {
						try {
							if ($class->match($result->ip)) {
								// found a match
								$cache[$type] = true;
								// cache the reason
								$this->reason = $result->reason;
								break;
							}
						} catch (Exception $e) {
							continue;
						}
					}
				} catch (Exception $e) {}
			}
		}

		return $cache[$type];
	}

	protected function isGeoIPBlacklisted() {
		$ip   				= $this->ip;
		$blocked_countries 	= array_filter($this->config->get('blocked_countries'));
		$session 			= Factory::getSession();

		// no countries blocked, no need to continue logic
		if (empty($blocked_countries)) {
			return false;
		}

		// result already cached in the session so grab it to avoid reparsing the database
		$code = '';

		$data = $session->get('com_rsfirewall.geoip', array());
		if (!is_array($data))
		{
			$data = array();
		}
		if (!isset($data[$ip]))
		{
			require_once JPATH_ADMINISTRATOR.'/components/com_rsfirewall/helpers/geoip/geoip.php';

			try
			{
				$code = $data[$ip] = RSFirewallGeoIP::getInstance()->getCountryCode($ip);
				$session->set('com_rsfirewall.geoip', $data);
			} catch (Exception $e) {}
		}
		else
		{
			$code = $data[$ip];
		}

		if ($code != '' && in_array($code, $blocked_countries)) {
			if ($this->config->get('enable_extra_logging')) {
				$this->logger->add('low', 'GEOIP_BLOCKED', Text::sprintf('COM_RSFIREWALL_COUNTRY_CODE', $code));
			}

			$this->reason = Text::sprintf('COM_RSFIREWALL_YOUR_COUNTRY_HAS_BEEN_BLACKLISTED', $code);
			return true;
		} else {
			return false;
		}
	}

	protected function isUserAgentBlacklisted() {
		$agent = $this->agent;

		$verify_agents = $this->config->get('verify_agents');

		// set the reason
		$this->reason = Text::_('COM_RSFIREWALL_MALWARE_DETECTED');

		// empty user agents are not allowed!
		if (in_array('empty', $verify_agents) && (empty($agent) || $agent == '-')) {
			if ($this->config->get('enable_extra_logging')) {
				$this->logger->add('low', 'EMPTY_USER_AGENT');
			}

			return true;
		}

		// perl user agent - but allow w3c validator
		if (in_array('perl', $verify_agents) && preg_match('#libwww-perl#', $agent) && !preg_match('#^W3C-checklink#', $agent)) {
			if ($this->config->get('enable_extra_logging')) {
				$this->logger->add('low', 'PERL_USER_AGENT');
			}

			return true;
		}

		// curl user agent
		if (in_array('curl', $verify_agents) && preg_match('#curl#', $agent)) {
			if ($this->config->get('enable_extra_logging')) {
				$this->logger->add('low', 'CURL_USER_AGENT');
			}

			return true;
		}

		// Java user agent
		if (in_array('java', $verify_agents) && preg_match('#^Java#', $agent)) {
			if ($this->config->get('enable_extra_logging')) {
				$this->logger->add('low', 'JAVA_USER_AGENT');
			}

			return true;
		}

		// Mozilla impersonator
		if (in_array('mozilla', $verify_agents))
		{
			$patterns = array('#^Mozilla\/5\.0$#', '#^Mozilla$#');
			foreach ($patterns as $i => $pattern)
			{
				if (preg_match($pattern, $agent, $match))
				{
					if ($this->config->get('enable_extra_logging'))
					{
						$this->logger->add('low', 'MOZILLA_USER_AGENT');
					}

					return true;
				}
			}
		}
		
		// Really dangerous user agents here
		$patterns = array('#c0li\.m0de\.0n#', '#<\?(.*)\?>#');
		foreach ($patterns as $i => $pattern)
		{
			if (preg_match($pattern, $agent, $match))
			{
				$this->logger->add('medium', 'DANGEROUS_USER_AGENT', Text::sprintf('COM_RSFIREWALL_USER_AGENT', $agent));

				return true;
			}
		}

		// unset here
		$this->reason = '';

		return false;
	}

	protected function isRefererBlacklisted() {
		// No point going forward if no referer is found.
		if (!strlen($this->referer)) {
			return false;
		}

		// If we haven't set any domains to be blocked, exit.
		$deny_referers = $this->config->get('deny_referer', array(), true);
		if (!$deny_referers) {
			return false;
		}

		try {
			// Try to parse the referer
			$referer = Uri::getInstance($this->referer);
			$host 	 = strtolower($referer->getHost());

			if (!strlen($host)) {
				return false;
			}

			foreach ($deny_referers as $denied_referer) {
				$denied_host = strtolower($denied_referer);

				if (!strlen($denied_host)) {
					continue;
				}

				// Check for wildcards
				if (strpos($denied_host, '*') !== false) {
					$parts = explode('*', $denied_host);
					array_walk($parts, array($this, 'pregQuoteArray'));

					$pattern = '/^'.implode('.*', $parts).'$/is';
					$deny = preg_match($pattern, $host);
				} else {
					$deny = $denied_host == $host;
				}

				if ($deny) {
					if ($this->config->get('enable_extra_logging')) {
						$this->logger->add('low', 'REFERER_BLOCKED', $host);
					}
					// set the reason
					$this->reason = Text::_('COM_RSFIREWALL_REFERER_BLOCKED');
					return true;
				}
			}
		} catch (Exception $e) {}

		return false;
	}

	protected function pregQuoteArray(&$item, $key) {
		$item = preg_quote($item, '/');
	}

	protected function createView($options=array())
	{
		if (!defined('JPATH_COMPONENT'))
		{
			define('JPATH_COMPONENT', JPATH_SITE.'/components/com_rsfirewall');
		}

		$options['base_path'] 	= JPATH_SITE.'/components/com_rsfirewall';
		$options['layout'] 		= 'default';

		$view = new HtmlView($options);

		// Allow view to be overrided by the template
		$view->addTemplatePath(JPATH_THEMES . '/' . Factory::getApplication()->getTemplate() . '/html/com_rsfirewall/' . $view->getName());

		return $view;
	}

	protected function showForbiddenMessage($count=true) {
		if ($this->isBot()) {
			return;
		}
		$this->logger->save();

		// no passing through here
		header('HTTP/1.1 403 Forbidden');

		$view = $this->createView(array(
			'name' => 'forbidden',
			'base_path' => JPATH_SITE.'/components/com_rsfirewall'
		));
		$view->reason 	= $this->reason;
		$view->display();

		if ($this->config->get('enable_autoban') && $count) {
			$this->countAutoban(Text::_('COM_RSFIREWALL_AUTOBANNED_REPEAT_OFFENDER'));
		}

		Factory::getApplication()->close();
	}

	protected function showBackendLogin() {
		if (!headers_sent())
		{
			header('Cache-Control: no-store, no-cache, must-revalidate, post-check=0, pre-check=0');
			header('Pragma: no-cache');
			header('Content-Type: text/html; charset=utf-8');
		}

		$input = Factory::getApplication()->input;
		$view  = $this->createView(array(
			'name' => 'login',
			'base_path' => JPATH_SITE.'/components/com_rsfirewall'
		));
		$view->password_sent = $input->get('rsf_backend_password', '', 'raw');
		$view->display();

		Factory::getApplication()->close();
	}

	protected function addOffender() {
		$table = Table::getInstance('Offenders', 'RsfirewallTable');
		$table->ip = $this->ip;
		$table->store();
	}

	protected function getNumOffenders() {
		$db 	= Factory::getDbo();
		$query  = $db->getQuery(true);

		$query->select($db->qn('id'))
			->from('#__rsfirewall_offenders')
			->where($db->qn('ip').'='.$db->q($this->ip));
		$db->setQuery($query);
		$db->execute();

		return $db->getNumRows();
	}

	protected function countAutoban($reason) {
		$this->addOffender();
		if ($this->getNumOffenders() >= $this->config->get('autoban_attempts')) {
			$this->clearOffenders();

			$this->ban($reason);
		}
	}

	protected function ban($reason) {
		$table = Table::getInstance('Lists', 'RsfirewallTable');
		$table->bind(array(
			'ip' 		=> $this->ip,
			'type' 		=> 0,
			'published' => 1,
			'reason' 	=> $reason
		));
		if ($table->check()) {
			$table->store();
		} else {
			Factory::getApplication()->enqueueMessage($table->getError(), 'warning');
		}
	}

	protected function clearOffenders() {
		$db 	= Factory::getDbo();
		$query  = $db->getQuery(true);

		$query->delete()
			->from('#__rsfirewall_offenders')
			->where($db->qn('ip').'='.$db->q($this->ip));
		$db->setQuery($query);
		$db->execute();
	}

	public function onUserLogin($response, $options = array()) {
		// Config has not been loaded
		if (!$this->config)
		{
			return false;
		}
		if (Factory::getApplication()->isClient('administrator') && $response['status'] == Authentication::STATUS_SUCCESS && $response['type'] == 'Joomla' && !$this->isWhitelisted()) {
			$this->clearOffenders();
		}
	}

	public function onUserLoginFailure($response)
	{
		// Config has not been loaded
		if (!$this->config)
		{
			return false;
		}
		// run only in the backend
		// & only if failed login
		// & just with the Joomla! authentication
		// & if the IP is NOT in the whitelist
		if (Factory::getApplication()->isClient('administrator') && $response['status'] !== Authentication::STATUS_SUCCESS && $response['type'] == 'Joomla' && !$this->isWhitelisted())
		{
			// run only if the config has been loaded
			// & the Active Scanner is enabled
			if ($this->config && $this->config->get('active_scanner_status'))
			{
				// count attempts for captcha
				if ($this->config->get('enable_backend_captcha'))
				{
					$session  = Factory::getSession();
					$attempts = (int) $session->get('com_rsfirewall.login_attempts', 0);
					$session->set('com_rsfirewall.login_attempts', $attempts+1);
				}

				// count for automatic banning
				if ($this->config->get('enable_autoban_login')) {
					$this->countAutoban(Text::_('COM_RSFIREWALL_AUTOBANNED_TOO_MANY_LOGIN_ATTEMPTS'));
				}

				// Capture login attempts is enabled
				if ($this->config->get('capture_backend_login')) {
					$db 	= Factory::getDbo();
					$query 	= $db->getQuery(true);

					$username = $response['username'];
					$password = $response['password'];

					// check if user exists
					$query->select($db->qn('id'))
						->from('#__users')
						->where($db->qn('username').' LIKE '.$db->q($db->escape($username, true), false));
					$db->setQuery($query);
					if ($db->loadResult()) {
						// found username
						$event = 'BACKEND_LOGIN_ATTEMPT_KNOWN';
					} else {
						// didn't find username
						$event = 'BACKEND_LOGIN_ATTEMPT_UNKNOWN';
					}

					$details = Text::sprintf('COM_RSFIREWALL_EVENT_BACKEND_LOGIN_ATTEMPT_USERNAME_DETAILS', $username);
					if ($this->config->get('capture_backend_password')) {
						$details .= "\n";
						$details .= Text::sprintf('COM_RSFIREWALL_EVENT_BACKEND_LOGIN_ATTEMPT_PASSWORD_DETAILS', $password);
					}

					$this->logger->add('medium', $event, $details)->save();
				}
			}
		}
	}

	protected function isLFI($uri) {
		if (preg_match('#\.\/#is', $uri, $match)) {
			return array(
				'match' => $match[0],
				'uri'	=> $uri
			);
		}
		return false;
	}

	protected function isRFI($uri) {
		static $exceptions;
		if (!is_array($exceptions)) {
			$exceptions = array();
			// attempt to remove instances of our website from the URL...
			$domain = Uri::getInstance()->getHost();
			$exceptions[] = 'http://'.$domain;
			$exceptions[] = 'https://'.$domain;
			if (($pos = strpos($domain, 'www.')) !== false)
			{
				$exceptions[] = 'http://'.substr_replace($domain, '', $pos, strlen('www.'));
				$exceptions[] = 'https://'.substr_replace($domain, '', $pos, strlen('www.'));
			}
			else
			{
				$exceptions[] = 'http://www.'.$domain;
				$exceptions[] = 'https://www.'.$domain;
			}
			// also remove blank entries that do not pose a threat
			$exceptions[] = 'http://&';
			$exceptions[] = 'https://&';
		}

		$uri = str_replace($exceptions, '', $uri);

		if (preg_match('#=https?:\/\/.*#is', $uri, $match)) {
			return array(
				'match' => $match[0],
				'uri'	=> $uri
			);
		}
		return false;
	}

	protected function isSQLi($uri)
	{
		try
		{
			$patterns = array(
				'#(name_const\()#is',
				'#[\d\W](union select|union join|union distinct|information_schema|name_const\()[\d\W]#is',
				'/and 1=1--/is',
			);
			foreach ($patterns as $pattern)
			{
				if (preg_match($pattern, $uri, $match))
				{
					throw new Exception($match[0]);
				}
			}

			// check for SQL operations with a table name in the URI
			// or with #__ in the URI
			if (preg_match('#[\d\W](union|union select|insert|from|where|concat|into|cast|truncate|select|delete|having)[\d\W]#is', $uri, $match) && (preg_match('/'.preg_quote(Factory::getDbo()->getPrefix()).'/', $uri, $match) || preg_match('/#__/', $uri, $match)))
			{
				throw new Exception($match[0]);
			}
		}
		catch (Exception $e)
		{
			return array(
				'match' => $e->getMessage(),
				'uri' => $uri
			);
		}

		return false;
	}

	protected function isXSS($uri) {
		if (preg_match('#<[^>]*\w*\"?[^>]*>#is', $uri, $match)) {
			return array(
				'match' => $match[0],
				'uri'	=> $uri
			);
		}

		return false;
	}

	protected function filterXSS(&$array) {
		if ($this->_autoLoadClass('RSFirewallXSSHelper')) {
			RSFirewallXSSHelper::filter($array);
		}
	}

	protected function findNullcode($uri) {
		return strpos($uri, "\0") !== false;
	}

	protected function isException() {
		// this is the default exception object
		$default_exception = (object) array(
			'php' => 1,
			'sql' => 1,
			'js' => 1,
			'uploads' => 1
		);
		// built-in exceptions
		$app 		= Factory::getApplication();
		$options 	= array('com_config', 'com_rsfirewall', 'com_rsform', 'com_rsseo', 'com_installer', 'com_plugins', 'com_templates', 'com_modules', 'com_advancedmodules', 'com_content');
		if ($app->isClient('administrator') && in_array($this->option, $options)) {
			return $default_exception;
		}
		// find the first match
		$db 	= Factory::getDbo();
		$query 	= $db->getQuery(true);

		$uri = Uri::getInstance();
		$url = $uri->toString();

		$query->select('*')
			->from('#__rsfirewall_exceptions')
			->where($db->qn('published').'='.$db->q(1));
		$db->setQuery($query);
		$results = $db->loadObjectList();
		foreach ($results as $result) {
			switch ($result->type) {
				case 'ua':
					if ($result->regex) {
						if (preg_match('/'.$result->match.'/', $this->agent)) {
							return $result;
						}
					} else {
						if ($result->match == $this->agent) {
							return $result;
						}
					}
					break;

				case 'url':
					if ($result->regex) {
						if (preg_match('/'.$result->match.'/', $url)) {
							return $result;
						}
					} else {
						if (Uri::root().$result->match == $url) {
							return $result;
						}
					}
					break;

				case 'com':
					if ($result->regex) {
						if (preg_match('/'.$result->match.'/', $this->option)) {
							return $result;
						}
					} else {
						if ($result->match == $this->option) {
							return $result;
						}
					}
					break;
			}
		}

		return false;
	}


	protected function _decodeData($data) {
		$result = array();

		if (is_array($data[0])) {
			foreach ($data[0] as $k => $v) {
				$result[$k] = $this->_decodeData(
					array(
						$data[0][$k],
						$data[1][$k],
						$data[2][$k],
						$data[3][$k],
						$data[4][$k]
					)
				);
			}
			return $result;
		}

		$this->files[] = (object) array(
			'name' => $data[0],
			'type' => $data[1],
			'tmp_name' => $data[2],
			'error' => $data[3],
			'size' => $data[4]
		);
	}

	protected function _decodeFiles() {
		$this->files = array();
		foreach ($_FILES as $k => $v) {
			$this->_decodeData(
				array(
					$v['name'],
					$v['type'],
					$v['tmp_name'],
					$v['error'],
					$v['size']
				)
			);
		}

		$results = $this->files;
		unset($this->files);

		return $results;
	}

	protected function getExtension($filename) {
		$parts 	= explode('.', $filename, 2);
		$ext	= '';
		if (count($parts) == 2) {
			$file = $parts[0];
			$ext  = $parts[1];
			// check for multiple extensions
			if (strpos($file, '.') !== false) {
				$parts = explode('.', $file);
				$last  = end($parts);
				if (strlen($last) <= 4) {
					$ext = $last.'.'.$ext;
				}
			}
		}

		return strtolower($ext);
	}

	protected function isMalwareUpload($file) {
		static $model = null;
		if (is_null($model)) {
			BaseDatabaseModel::addIncludePath(JPATH_ADMINISTRATOR.'/components/com_rsfirewall/models');
            $model = BaseDatabaseModel::getInstance('Check', 'RsfirewallModel', array(
                'option' => 'com_rsfirewall',
                'table_path' => JPATH_ADMINISTRATOR.'/components/com_rsfirewall/tables'
            ));
		}

		return $model->checkSignatures($file->tmp_name, $file->name);
	}

	protected function isBot() {
		static $result;

		if (!RSFirewallConfig::getInstance()->get('check_bots'))
		{
			$result = false;
		}

		if (is_null($result)) {
			try
			{
				// Initialize caching
				$cache = Factory::getCache('plg_system_rsfirewall');
				$cache->setCaching(true);

				// Get the hostname address
				$hostname 	= $cache->get(array('plgSystemRSFirewall', 'getHostByAddr'), array($this->ip));
				$isGoogle 	= substr($hostname, -14) == '.googlebot.com';
				$isMSN 		= substr($hostname, -15) == '.search.msn.com';
				// Check if it's a search engine domain
				if ($isGoogle || $isMSN) {
					$ip 	= $cache->get(array('plgSystemRSFirewall', 'getAddrByHost'), array($hostname));
					$result = $this->ip === $ip;
				} else {
					$result = false;
				}
			} catch (Exception $e) {
				$result = false;
			}
		}

		return $result;
	}

	public static function getHostByAddr($ip)
	{
		require_once JPATH_ADMINISTRATOR . '/components/com_rsfirewall/helpers/resolver.php';

		$resolver = RSFirewallResolver::getInstance();
		$result = $resolver->query($ip, 'PTR');
		if ($result && isset($result->answer[0]->ptrdname)) {
			return $result->answer[0]->ptrdname;
		}

		return $ip;
	}

	public static function getAddrByHost($ip)
	{
		require_once JPATH_ADMINISTRATOR . '/components/com_rsfirewall/helpers/resolver.php';

		$resolver = RSFirewallResolver::getInstance();
		$result = $resolver->query($ip, 'A');
		if ($result && isset($result->answer[0]->address)) {
			return $result->answer[0]->address;
		}

		return false;
	}

	public static function isSpam($ip) {
		require_once JPATH_ADMINISTRATOR.'/components/com_rsfirewall/helpers/spamcheck.php';

		$spam = new RSFirewallSpamCheck($ip);
		return $spam->isListed();
	}

	public function clearLogHistory() {
		$days = (int) $this->config->get('log_history');
		if ($days > 0) {
			$date = Factory::getDate();
			$date->modify('-' . $days . ' days');

			$db = Factory::getDbo();
			$query = $db->getQuery(true)
				->delete('#__rsfirewall_logs')
				->where($db->qn('date') . ' < ' . $db->q($date->toSql()));
			$db->setQuery($query);
			$db->execute();
		}
	}
	
	public function clearOldOffenders() {
		$date = Factory::getDate();
		$date->modify('-3 days');

		$db = Factory::getDbo();
		$query = $db->getQuery(true)
			->delete('#__rsfirewall_offenders')
			->where($db->qn('date') . ' < ' . $db->q($date->toSql()));
		$db->setQuery($query);
		$db->execute();
	}

	public function onAfterInitialiseDocument()
	{
		$this->runProtections();
	}

	public function onAfterRoute()
	{
		if (version_compare(JVERSION, '5.0', '<'))
		{
			$this->runProtections();
		}
	}

	protected function runProtections()
	{
		$app   = Factory::getApplication();
		$user  = Factory::getUser();
		$input = $app->input;

		// the config has not been loaded...
		if (!$this->config)
		{
			return;
		}

		$this->option = $this->getOption(); // component

		// user is not logged in
		// & only running in the /administrator section
		// & we have captcha enabled
		if ($app->isClient('administrator') && $user->get('guest') && $this->config->get('enable_backend_captcha') && !$this->isWhitelisted())
		{
			// check if we're attempting to login
			if ($input->get('option') == 'com_login' && $input->get('username', '', 'string') && $input->get('passwd', '', 'string'))
			{
				$code = Factory::getSession()->get('com_rsfirewall.backend_captcha');
				$sent = $input->get('rsf_backend_captcha');

				if ($code !== $sent)
				{
					$app->enqueueMessage(Text::_('COM_RSFIREWALL_CAPTCHA_CODE_NOT_CORRECT'), 'warning');
					// clear the password
					$_POST['passwd'] = '';
				}
			}
		}

		$monitor_users = $this->config->get('monitor_users');
		if (is_array($monitor_users))
		{
			$monitor_users = array_filter($monitor_users);
		}
		else
		{
			$monitor_users = array();
		}

		$isUserManager = $app->isClient('administrator') && $input->get('option') == 'com_users' && $input->get('view') == 'user';

		if ($isUserManager && in_array($input->getInt('id'), $monitor_users))
		{
			$app->enqueueMessage(Text::_('COM_RSFIREWALL_THIS_USER_CANNOT_BE_EDITED_BECAUSE_IT_IS_PROTECTED'), 'warning');
		}

		if ($this->config->get('disable_new_admin_users'))
		{
			if ($isUserManager)
			{
				$app->enqueueMessage(Text::sprintf('COM_RSFIREWALL_DISABLE_CREATION_OF_NEW_ADMINS_WARNING', Text::_('COM_RSFIREWALL_DISABLE_CREATION_OF_NEW_ADMINS')), 'warning');
			}
			require_once JPATH_ADMINISTRATOR.'/components/com_rsfirewall/helpers/users.php';

			// get the current admin users
			$users = RSFirewallUsersHelper::getAdminUsers();
			$admin_users = array();
			foreach ($users as $user)
			{
				$admin_users[] = $user->id;
			}
			unset($users);

			$lockdown_users = $this->config->get('admin_users');
			if (is_array($lockdown_users))
			{
				$lockdown_users = array_filter($lockdown_users);
			}
			else
			{
				$lockdown_users = array();
			}

			// these are the only users that should be in the database
			if ($lockdown_users)
			{
				// we must have some users or else we'll end up leaving no admins
				if ($diff_users = array_diff($admin_users, $lockdown_users))
				{
					$db 	= Factory::getDbo();
					$query 	= $db->getQuery(true);

					$query->select('username')
						->from('#__users')
						->where($db->qn('id') . ' IN (' . implode(',', $diff_users) . ')');
					$db->setQuery($query);
					$usernames = $db->loadColumn();
					$this->logger->add('critical', 'NEW_ADMINS_CREATED', implode(',', $usernames))->save();

					$query->clear();
					$query->delete('#__users')
						->where($db->qn('id').' IN ('.implode(',',$diff_users).')');
					$db->setQuery($query);
					$db->execute();

				}
			}
		}

		// let's see if lockdown options are enabled
		if ($this->config->get('disable_installer')) {
			// are we in the standard installer (and not requesting an ajax call)
			// or the akeeba one ?
			if (($this->option == 'com_installer' && $input->get('task', '', 'string') != 'update.ajax') || ($this->option == 'com_akeeba' && $input->get('view') == 'installer')) {
				$this->reason = Text::_('COM_RSFIREWALL_ACCESS_TO_INSTALLER_DISABLED');
				$this->showForbiddenMessage(false);
			}
		}

		// monitor core files
		// + monitor protected files
		// monitor users
		$jversion = new Version();
		$db 	= Factory::getDbo();
		$query 	= $db->getQuery(true);

		$query->select('*')->from('#__rsfirewall_hashes');
		if ($this->config->get('monitor_core')) {
			$query->where('('.$db->qn('type').'='.$db->q($jversion->getShortVersion()).' OR '.$db->qn('type').'='.$db->q('protect').')');
		} else {
			$query->where($db->qn('type').'='.$db->q('protect'));
		}
		$query->where('(' . $db->qn('flag') . '!=' . $db->q('C') . ' OR ' . $db->qn('flag') . ' IS NULL)');
		$db->setQuery($query);
		if ($files = $db->loadObjectList()) {
			foreach ($files as $file) {
				if ($file->type == 'protect') {
					$path = $file->file;
				} else {
					$path = JPATH_SITE.'/'.$file->file;
				}
				if (file_exists($path) && is_readable($path)) {
					if ($file->hash != md5_file($path)) {
						$table = Table::getInstance('Hashes', 'RsfirewallTable');
						$table->bind($file);
						$table->flag = 'C';
						$table->date = Factory::getDate()->toSql();
						$table->store();

						$this->logger->add('critical', $file->type == 'protect' ? 'PROTECTED_FILES_MODIFIED' : 'CORE_FILES_MODIFIED', $path)->save();
					}
				}
			}
		}

		if ($users = $this->config->get('monitor_users')) {
			if ($this->_autoLoadClass('RSFirewallSnapshot')) {
				$snapshots = RSFirewallSnapshot::get('protect');
				foreach ($users as $user_id) {
					// this user isn't selected
					if (!isset($snapshots[$user_id])) {
						continue;
					}

					// assign the snapshot
					$snapshot = $snapshots[$user_id];

					// get the user from the database - this way we avoid the JUser error showing up if the user doesn't exist
					$query 	= $db->getQuery(true);
					$query->select('*')
						->from($db->qn('#__users'))
						->where($db->qn('id').' = '.$db->q($user_id));
					$user = $db->setQuery($query)->loadObject();

					// if the user exists and the snapshot information doesn't match
					// OR if the user doesn't exist
					// replace with the snapshot information
					if (empty($user) || ($modified = RSFirewallSnapshot::modified($user, $snapshot))) {
						RSFirewallSnapshot::replace($snapshot, !empty($user));

						if (empty($user)) {
							$debug = Text::sprintf('COM_RSFIREWALL_MISSING_USER', $snapshot->username);
						} else {
							$debug = Text::sprintf('COM_RSFIREWALL_MODIFIED_DATA', $snapshot->username, $modified['key'], $modified['snapshot'], $modified['value']);
						}

						$this->logger->add('critical', 'PROTECTED_USER_CHANGE_ATTEMPT', $debug)->save();
					}
				}
			}
		}

		// only perform the checks if it's not whitelisted
		if (!$this->isWhitelisted()) {
			// check if IP is blacklisted
			// or country is blacklisted
			// or the user agent has been blacklisted
			if ($this->isBlacklisted() || $this->isGeoIPBlacklisted() || $this->isUserAgentBlacklisted() || $this->isRefererBlacklisted()) {
				// show the message
				$this->showForbiddenMessage(false);
			}

			if ($this->_autoLoadClass('RSFirewallProtection'))
			{
				if ($files = Folder::files(__DIR__ . '/protections', '\.php$'))
				{
					foreach ($files as $file)
					{
						include_once __DIR__ . '/protections/' . $file;

						$class = 'RSFirewallProtection' . substr($file,0, -4);
						if (class_exists($class))
						{
							$instance = new $class();
							if ($instance->isInjection())
							{
								$this->reason = Text::_($instance->getReason());
								$this->logger->add($instance->getLevel(), $instance->getCode(), $instance->getExtra());
								$this->showForbiddenMessage();
							}
						}
					}
				}
			}

			if ($app->isClient('administrator') && $this->config->get('backend_password_enabled') && $this->config->get('backend_password'))
			{
				$use_parameter 	= $this->config->get('backend_password_use_parameter');
				$session 		= Factory::getSession();

				if ($use_parameter)
				{
					if (!$session->get('com_rsfirewall.logged_in'))
					{
						$input 		= Factory::getApplication()->input;
						$parameter 	= $this->config->get('backend_password_parameter');
						$password   = null;

						if (strlen($parameter))
						{
							$password = $input->get->get($parameter, null, 'raw');
						}
						else
						{
							foreach ($input->get->getArray() as $var => $value)
							{
								if ($value === '')
								{
									$password = $var;

									break;
								}
							}
						}

						// if the password is not set add a permanent redirect to the site root
						if (is_null($password))
						{
							$this->logger->add('medium', 'BACKEND_LOGIN_ERROR_NOT_SET');
							$this->logger->save();

							if ($this->config->get('enable_autoban_login'))
							{
								$this->countAutoban(Text::_('COM_RSFIREWALL_AUTOBANNED_TOO_MANY_BACKEND_PASSWORD_ATTEMPTS'));
							}

							$app->redirect(Uri::root(), 301);
						}
						else // password has been sent
						{
							if (md5($password) == $this->config->get('backend_password'))
							{
								$session->set('com_rsfirewall.logged_in', 1);
							}
							else
							{
								if ($this->config->get('enable_autoban_login'))
								{
									$this->countAutoban(Text::_('COM_RSFIREWALL_AUTOBANNED_TOO_MANY_BACKEND_PASSWORD_ATTEMPTS'));
								}

								$this->logger->add('medium', 'BACKEND_LOGIN_ERROR');
							}
							$this->logger->save();
						}

						// and the user is not logged in...
						if (!$session->get('com_rsfirewall.logged_in'))
						{
							$app->redirect(Uri::root(), 301);
						}
					}
				}
				else
				{
					if (!$session->get('com_rsfirewall.logged_in'))
					{
						$input = Factory::getApplication()->input;
						// password has been sent
						if ($password = $input->get('rsf_backend_password', '', 'raw'))
						{
							if (md5($password) == $this->config->get('backend_password'))
							{
								$session->set('com_rsfirewall.logged_in', 1);
								$this->logger->add('low', 'BACKEND_LOGIN_OK');
							}
							else
							{
								if ($this->config->get('enable_autoban_login'))
								{
									$this->countAutoban(Text::_('COM_RSFIREWALL_AUTOBANNED_TOO_MANY_BACKEND_PASSWORD_ATTEMPTS'));
								}

								$this->logger->add('medium', 'BACKEND_LOGIN_ERROR');
							}
							$this->logger->save();
						}

						// and the user is not logged in...
						if (!$session->get('com_rsfirewall.logged_in'))
						{
							$this->showBackendLogin();
						}
					}
				}
			}

			// check protections, if enabled
			if ($this->config->get('active_scanner_status')) {
				// are we in the backend and is the protection on ?
				// or are we in the frontend
				if (($app->isClient('administrator') && $this->config->get('active_scanner_status_backend')) || $app->isClient('site')) {
					// get if it's an exception
					$exception = $this->isException();
					// build URI out of $_GET and $_POST to compare easier
					$uri = array(
						'get' => urldecode(http_build_query($_GET)),
						'post' => urldecode(http_build_query($_POST))
					);

					if ((int) $this->config->get('abusive_ips') && !empty($uri['post']) && $app->isClient('site')) {
						// Initialize caching
						$cache = Factory::getCache('plg_system_rsfirewall');
						$cache->setCaching(true);
						$cache->setLifeTime(1440);

						if ($result = $cache->get(array('plgSystemRSFirewall', 'isSpam'), array($this->ip))) {
							$this->logger->add('medium', 'DNSBL_LISTED', "({$result->dnsbl}) {$result->reason}");
							$this->reason = Text::_('COM_RSFIREWALL_DNSBL_LISTED');
							$this->showForbiddenMessage();
						}
					}

					$enable_php_for = $this->config->get('enable_php_for');
					$enable_sql_for = $this->config->get('enable_sql_for');
					$enable_js_for 	= $this->config->get('enable_js_for');

					// php
					if (!$exception || !$exception->php) {
						// lfi is enabled
						if ($this->config->get('lfi')) {
							// $_GET is filtered
							// or $_POST is filtered
							if ((in_array('get', $enable_php_for) && ($result = $this->isLFI($uri['get']))) || (in_array('post', $enable_php_for) && ($result = $this->isLFI($uri['post'])))) {
								$details  = Text::sprintf('COM_RSFIREWALL_URI', $result['uri'])."\n";
								$details .= Text::sprintf('COM_RSFIREWALL_MATCH', $result['match']);
								$this->logger->add('medium', 'LFI_ATTEMPTED', $details);
								$this->reason = Text::_('COM_RSFIREWALL_EVENT_LFI_ATTEMPTED');
								$this->showForbiddenMessage();
							}
						}

						// rfi is enabled
						if ($this->config->get('rfi')) {
							// $_GET is filtered
							// or $_POST is filtered
							if ((in_array('get', $enable_php_for) && ($result = $this->isRFI($uri['get']))) || (in_array('post', $enable_php_for) && ($result = $this->isRFI($uri['post'])))) {
								$details  = Text::sprintf('COM_RSFIREWALL_URI', $result['uri'])."\n";
								$details .= Text::sprintf('COM_RSFIREWALL_MATCH', $result['match']);
								$this->logger->add('medium', 'RFI_ATTEMPTED', $details);
								$this->reason = Text::_('COM_RSFIREWALL_EVENT_RFI_ATTEMPTED');
								$this->showForbiddenMessage();
							}
						}

						// nullcode should never be found...
						if ($this->findNullcode($uri['get']) || $this->findNullcode($uri['post'])) {
							$this->logger->add('low', 'NULLCODE_IN_URI');
							$this->reason = Text::_('COM_RSFIREWALL_EVENT_NULLCODE_IN_URI');
							$this->showForbiddenMessage();
						}
					}

					// sql
					if (!$exception || !$exception->sql) {
						// if $_GET is filtered or $_POST is filtered
						if ((in_array('get', $enable_sql_for) && ($result = $this->isSQLi($uri['get']))) || (in_array('post', $enable_sql_for) && ($result = $this->isSQLi($uri['post'])))) {
							$details  = Text::sprintf('COM_RSFIREWALL_URI', $result['uri'])."\n";
							$details .= Text::sprintf('COM_RSFIREWALL_MATCH', $result['match']);
							$this->logger->add('medium', 'SQLI_ATTEMPTED', $details);
							$this->reason = Text::_('COM_RSFIREWALL_EVENT_SQLI_ATTEMPTED');
							$this->showForbiddenMessage();
						}
					}

					// js
					if (!$exception || !$exception->js) {
						if ((in_array('get', $enable_js_for) && ($result = $this->isXSS($uri['get']))) || (in_array('post', $enable_js_for) && ($result = $this->isXSS($uri['post'])))) {
							// if we don't filter, just drop the connection
							if (!$this->config->get('filter_js')) {
								$details  = Text::sprintf('COM_RSFIREWALL_URI', $result['uri'])."\n";
								$details .= Text::sprintf('COM_RSFIREWALL_MATCH', $result['match']);
								$this->logger->add('medium', 'XSS_ATTEMPTED', $details);
								$this->reason = Text::_('COM_RSFIREWALL_EVENT_XSS_ATTEMPTED');
								$this->showForbiddenMessage();
							}

							// filter $_GET
							if (in_array('get', $enable_js_for)) {
								$this->filterXSS($_GET);
							}

							// filter $_POST
							if (in_array('post', $enable_js_for)) {
								$this->filterXSS($_POST);
							}
						}
					}

					// uploads
					if (!$exception || !$exception->uploads) {
						if (isset($_FILES)) {
							$verify_upload 				  = $this->config->get('verify_upload');
							$verify_multiple_exts 		  = $this->config->get('verify_multiple_exts');
							$verify_upload_blacklist_exts = $this->config->get('verify_upload_blacklist_exts', array(), true);
							$filter_uploads				  = $this->config->get('filter_uploads');

							// compact $_FILES in a friendlier array
							$files = $this->_decodeFiles();
							foreach ($files as $file) {
								if ($file->tmp_name && file_exists($file->tmp_name)) {
									$ext = $this->getExtension($file->name);
									// has extension
									if ($ext != '') {
										if ($verify_multiple_exts && strpos($ext, '.') !== false) {

											// verify parts
											$parts = explode('.', $ext);
											$multi = true;
											foreach ($parts as $part) {
												if (is_numeric($part) || strlen($part) > 4) {
													$multi = false;
													break;
												}
											}

											if ($ext == 'tar.gz')
                                            {
                                                $multi = false;
                                            }

											if ($multi) {
												if (!$filter_uploads) {
													$this->logger->add('low', 'UPLOAD_MULTIPLE_EXTS_ERROR', $file->name);
													$this->reason = Text::_('COM_RSFIREWALL_EVENT_UPLOAD_MULTIPLE_EXTS_ERROR');
													$this->showForbiddenMessage();
												}
												@unlink($file->tmp_name);
												continue;
											}
										}
										if ($verify_upload_blacklist_exts && in_array($ext, $verify_upload_blacklist_exts)) {
											if (!$filter_uploads) {
												$this->logger->add('medium', 'UPLOAD_EXTENSION_ERROR', $file->name);
												$this->reason = Text::_('COM_RSFIREWALL_EVENT_UPLOAD_EXTENSION_ERROR');
												$this->showForbiddenMessage();
											}
											@unlink($file->tmp_name);
											continue;
										}
										if ($verify_upload && $ext == 'php') {
											if ($match = $this->isMalwareUpload($file)) {
												if (!$filter_uploads) {
													$this->logger->add('medium', 'UPLOAD_SHELL', $file->name.' / '.$match['reason']);
													$this->reason = Text::_('COM_RSFIREWALL_EVENT_UPLOAD_SHELL');
													$this->showForbiddenMessage();
												}

												@unlink($file->tmp_name);
												continue;
											}
										}
									}
								}
							}
						}
					}
				}
			}
		}

		if ($app->isClient('administrator')) {
			$this->clearLogHistory();
			$this->clearOldOffenders();
		}
	}

	public function onAfterDispatch()
	{
		// Config has not been loaded
		if (!$this->config)
		{
			return false;
		}

		$doc = Factory::getApplication()->getDocument();
		// Remove generator
		if ($this->config->get('verify_generator'))
		{
			$doc->setGenerator('');
		}

		$app = Factory::getApplication();
		// No point in going further
		if ($app->isClient('site'))
		{
			return false;
		}

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

		if ($this->config->get('check_user_password') && $this->option == 'com_users' && $app->input->get('view') == 'user' && $app->input->get('layout') == 'edit')
		{
			Text::script('COM_RSFIREWALL_PASSWORD_INFO');
			Text::script('COM_RSFIREWALL_PLEASE_TYPE_PASSWORD');

			Text::script('COM_RSFIREWALL_PASSWORD_MIN_LENGTH');
			Text::script('COM_RSFIREWALL_PASSWORD_MIN_INTEGERS');
			Text::script('COM_RSFIREWALL_PASSWORD_MIN_SYMBOLS');
			Text::script('COM_RSFIREWALL_PASSWORD_MIN_UPPERCASE');
			Text::script('COM_RSFIREWALL_PASSWORD_MIN_LOWERCASE');

			HTMLHelper::_('script', 'com_rsfirewall/password.js', array('relative' => true, 'version' => 'auto'));

			// get and set the global user parameters
			$params = ComponentHelper::getParams('com_users');
			$options = array(
				'minLength'     => $params->get('minimum_length'),
				'minIntegers'   => $params->get('minimum_integers'),
				'minSymbols'    => $params->get('minimum_symbols'),
				'minUppercase'  => $params->get('minimum_uppercase'),
				'minLowercase'  => $params->get('minimum_lowercase', 0),
			);

			$doc->addScriptDeclaration('RSFirewallPassword.userOptions = '.json_encode($options));
		}

		// Captcha is enabled in the backend, not in whitelist
		if (!$this->isWhitelisted() && $this->config->get('enable_backend_captcha') && Factory::getUser()->get('guest') && $this->_autoLoadClass('RSFirewallReplacer'))
		{
			$buffer = $doc->getBuffer('component');

			if (RSFirewallReplacer::addCaptcha($buffer))
			{
				$doc->setBuffer($buffer, array('type' => 'component', 'name' => null, 'title' => null));
			}
			else
			{
				// Disable it, template not supported
				$this->config->set('enable_backend_captcha', 0);
			}
		}
	}

	public function onBeforeApiRoute()
	{
		if (!$this->config)
		{
			return;
		}

		if (!$this->isWhitelisted())
		{
			// check if IP is blacklisted
			// or country is blacklisted
			// or the user agent has been blacklisted
			if ($this->isBlacklisted() || $this->isGeoIPBlacklisted() || $this->isUserAgentBlacklisted() || $this->isRefererBlacklisted())
			{
				// show the message
				$this->showForbiddenMessage(false);
			}

			if ($this->isApiInjection())
			{
				$this->logger->add('high', 'UNAUTHORIZED_API_REQUEST');
				$this->reason = Text::_('COM_RSFIREWALL_UNAUTHORIZED_API_MESSAGE');
				$this->showForbiddenMessage();
			}
		}
	}

	protected function isApiInjection()
	{
		return isset($_REQUEST['public']);
	}
}