shell bypass 403
<?php /** * @package admintools * @copyright Copyright (c)2010-2024 Nicholas K. Dionysopoulos / Akeeba Ltd * @license GNU General Public License version 3, or later */ namespace Akeeba\Plugin\System\AdminTools\Utility; defined('_JEXEC') or die; use Akeeba\Component\AdminTools\Administrator\Helper\Storage; use Akeeba\Component\AdminTools\Administrator\Helper\TemplateEmails; use DateTimeZone; use Exception; use Joomla\Application\ApplicationInterface; use Joomla\CMS\Application\CMSApplication; use Joomla\CMS\Component\ComponentHelper; use Joomla\CMS\Factory; use Joomla\CMS\Language\Text; use Joomla\CMS\Uri\Uri; use Joomla\CMS\User\User; use Joomla\Database\DatabaseAwareInterface; use Joomla\Database\DatabaseAwareTrait; use Joomla\Database\DatabaseDriver; use Joomla\Database\ParameterType; use Joomla\Registry\Registry; class BlockedRequestHandler implements DatabaseAwareInterface { use DatabaseAwareTrait; /** * Plugin parameters * * @var Registry * @since 7.0.0 */ protected $pluginParams = null; /** * WAF parameters * * @var Storage * @since 7.0.0 */ protected $wafParams = null; /** * Component parameters * * @var Registry * @since 7.0.0 */ protected $cParams = null; private ApplicationInterface $application; public function __construct(Registry $pluginParams, Storage $wafParams, Registry $cParams) { $this->pluginParams = $pluginParams; $this->wafParams = $wafParams; $this->cParams = $cParams; } /** * @param string $templateKey The template key to send, e.g. 'com_admintools.blockedrequest' * @param User|null $user The user to send the email to. NULL for the currently logged in user. * @param array $data Associative array for tag/variable replacement in the email template. * * @return bool */ public function sendEmail(string $templateKey, ?User $user = null, array $data = []): bool { // Do not send emails in the Core version if (!defined('ADMINTOOLS_PRO') || !ADMINTOOLS_PRO) { return true; } $app = $this->getApplication(); $user = $user ?: $app->getIdentity(); $data = $this->getEmailVariables( array_merge( [ 'USERNAME' => $user->username, 'FULLNAME' => $user->name, ], $data ) ); try { TemplateEmails::updateTemplate($templateKey); return TemplateEmails::sendMail($templateKey, $data, $user); } catch (Exception $e) { return false; } } /** * Logs security exceptions and processes the IP auto-ban for this IP but does NOT block the request. * * This is used when the request needs to be redirected (e.g. admin secret URL parameter), or when we are only * logging potential problems (e.g. failed login). * * @param string $reason Block reason code * @param string $extraLogInformation Extra information to be written to the text log file * @param string $extraLogTableInformation Extra information to be written to the extradata field of the log * * @return bool */ public function logWithoutBlocking($reason, $extraLogInformation = '', $extraLogTableInformation = '') { if ($this->wafParams->getValue('tsrenable', 0)) { $this->processAutoBan($reason); } return $this->logBlockedRequest($reason, $extraLogInformation, $extraLogTableInformation); } /** * Blocks the request, logs it and processes the IP auto-ban. * * This is the full request blocking experience, triggered when we need to immediately abort the request in progress * to prevent a security issue from affecting the application. * * @param string $reason Block reason code * @param string $message The message to be shown to the user * @param string $extraLogInformation Extra information to be written to the text log file * @param string $extraLogTableInformation Extra information to be written to the extradata field of the log * table (useful for JSON format) * * @return void * @throws Exception */ public function blockRequest( $reason = 'other', $message = '', $extraLogInformation = '', $extraLogTableInformation = '' ) { if (empty($message)) { $customMessage = $this->wafParams->getValue('custom403msg', ''); $message = trim($customMessage) ?: 'PLG_ADMINTOOLS_MSG_BLOCKED'; } if (!$this->logWithoutBlocking($reason, $extraLogInformation, $extraLogTableInformation)) { return; } // Merge the default translation with the current translation /** @var CMSApplication $app */ $app = $this->getApplication(); if ((Text::_('PLG_ADMINTOOLS_MSG_BLOCKED') == 'PLG_ADMINTOOLS_MSG_BLOCKED') && ($message == 'PLG_ADMINTOOLS_MSG_BLOCKED')) { $message = "Access Denied"; } else { $message = Text::_($message); } $message = RescueUrl::processRescueInfoInMessage($message); // Show the 403 message $use403View = $this->wafParams->getValue('use403view', 0); $isFrontend = $app->isClient('site'); $isApi = $app->isClient('api'); if ($isApi) { @ob_end_clean(); header('HTTP/1.1 403 Access Denied'); echo $message; $app->close(); } if (!$use403View || !$isFrontend) { // Using Joomla!'s error page $app->input->set('template', null); $app->input->set('layout', null); throw new Exception($message, 403); } // Using a view $session = $app->getSession(); if (!$session->get('com_admintools.block', false)) { // This is inside an if-block so that we don't end up in an infinite redirection loop $session->set('com_admintools.block', true); $session->set('com_admintools.message', $message); if ($app->isClient('site') || $app->isClient('administrator')) { $session->close(); } $app->redirect(Uri::base(), 307); } } /** * Checks if the Rescue URL is being accessed. * * This only applies when IP autoban is enabled and this is an administrator access. * * @return void */ public function checkRescueURL() { $autoban = $this->wafParams->getValue('tsrenable', 0); if (!$autoban) { return; } // If IP auto-ban is enabled we need to check for a Rescue URL RescueUrl::processRescueURL($this); } public function setApplication(ApplicationInterface $application): void { $this->application = $application; } private function getApplication(): ApplicationInterface { return $this->application; } /** * Sends a security exception email. Respecting the email throttling settings. * * @param string $reason The blocked request reason * @param User|null $recipient The recipient user. NULL for currently logged in user. * @param array $data Associative array for tag/variable replacement in the email template. * * @return bool True on succesfully sent email. */ private function sendSecurityExceptionEmail(string $reason, ?User $recipient = null, array $data = []) { if (!$this->isSendingAllowedByEmailThrottling()) { return false; } return $this->sendEmail( 'com_admintools.blockedrequest', $recipient, [ 'REASON' => $reason, ] ); } /** * Get the variables we can use in emails as an associative list (variable => value). * * @param array $customVariables An array of custom variables to add to the return. * * @return array */ private function getEmailVariables($customVariables = []) { $app = $this->getApplication(); $siteName = $app->get('sitename'); $cParams = ComponentHelper::getParams('com_admintools'); $email_timezone = $cParams->get('email_timezone', 'AKEEBA/DEFAULT'); $app = $this->getApplication(); $user_tz = $app->getIdentity()->get('timezone', $app->get('offset', 'GMT')); try { $timezone = new DateTimeZone($user_tz); } catch (Exception $e) { $timezone = null; } if (!empty($email_timezone) && ($email_timezone != 'AKEEBA/DEFAULT')) { try { $forcedTimezone = new DateTimeZone($email_timezone); $timezone = $forcedTimezone; } catch (Exception $e) { // Just in case someone puts an invalid timezone in there (you can never be too paranoid). } } $date = clone Factory::getDate(); $date->setTimezone($timezone ?: new DateTimeZone('GMT')); $ip = Filter::getIp(); if ((strpos($ip, '::') === 0) && (strstr($ip, '.') !== false)) { $ip = substr($ip, strrpos($ip, ':') + 1); } $currentUser = $app->getIdentity(); if ($currentUser->guest) { $currentUser = 'Guest'; } else { $currentUser = $currentUser->username . ' (' . $currentUser->name . ' <' . $currentUser->email . '>)'; } $ipLookupURL = 'https://' . $this->wafParams->getValue('iplookup', 'ip-lookup.net/index.php?ip={ip}'); $ipLookupURL = str_replace('{ip}', $ip, $ipLookupURL); $uri = Uri::getInstance(); $url = $uri->toString(['scheme', 'user', 'pass', 'host', 'port', 'path', 'query', 'fragment']); return array_merge( [ 'USER' => $currentUser, 'SITENAME' => $siteName, 'DATE' => ($date)->format('Y-m-d H:i:s T', true), 'IP' => $ip, 'URL' => $url, 'LOOKUP' => $ipLookupURL, 'UA' => $_SERVER['HTTP_USER_AGENT'], ], $customVariables ); } /** * Logs security exceptions * * @param string $reason Block reason code * @param string $extraLogInformation Extra information to be written to the text log file * @param string $extraLogTableInformation Extra information to be written to the extradata field of the log * table (useful for JSON format) * * @return bool */ private function logBlockedRequest($reason, $extraLogInformation = '', $extraLogTableInformation = '') { $ip = $this->getVisitorIPAddress(); // No point continuing if I cannot get the visitor's IP address if ($ip === false) { return false; } // Make sure this IP is not in the Administrator Exclusive Allow IP List if ($this->isIPInAdminWhitelist()) { return false; } // Make sure this IP is not in the Site IP Allow List if ($this->isIPInAllowList()) { return false; } // Make sure this IP is not in the "Do not block these IPs" list if ($this->isSafeIP()) { return true; } // Make sure this IP doesn't resolve to a whitelisted domain if ($this->isWhitelistedDomain($ip)) { return true; } // Get the human-readable blocking reason $txtReason = $this->getBlockingReasonHumanReadable($reason, $extraLogTableInformation); // Get the email tokens, also used for logging $tokens = $this->getEmailVariables( [ 'REASON' => $txtReason, ] ); // Log the security exception to file and the database, if necessary $this->logSecurityException($reason, $extraLogInformation, $extraLogTableInformation, $txtReason, $tokens); // Email the security exception, if necessary $this->emailSecurityException($reason, $tokens); return true; } /** * Checks if an IP address should be automatically banned for raising too many security exceptions over a predefined * time period. * * @param string $reason The reason of the ban * * @return void */ private function processAutoBan($reason = 'other') { // The Core version does not support auto-banning IP addresses if (!defined('ADMINTOOLS_PRO') || !ADMINTOOLS_PRO) { return; } // We need to be able to get our own IP, right? if (!function_exists('inet_pton')) { return; } // Get the IP $ip = Filter::getIp(); // No point continuing if we can't get an address, right? if (empty($ip) || ($ip == '0.0.0.0')) { return; } // Check for repeat offenses /** @var DatabaseDriver $db */ $db = $this->getDatabase(); $strikes = $this->wafParams->getValue('tsrstrikes', 3); $numfreq = $this->wafParams->getValue('tsrnumfreq', 1); $frequency = $this->wafParams->getValue('tsrfrequency', 'hour'); $mindatestamp = 0; switch ($frequency) { case 'second': break; case 'minute': $numfreq *= 60; break; case 'hour': $numfreq *= 3600; break; case 'day': $numfreq *= 86400; break; case 'ever': $mindatestamp = 946706400; // January 1st, 2000 break; } $jNow = clone Factory::getDate(); if ($mindatestamp == 0) { $mindatestamp = $jNow->toUnix() - $numfreq; } $jMinDate = clone Factory::getDate($mindatestamp); $minDate = $jMinDate->toSql(); $sql = $db->getQuery(true) ->select('COUNT(*)') ->from($db->qn('#__admintools_log')) ->where($db->qn('logdate') . ' >= ' . $db->q($minDate)) ->where($db->qn('ip') . ' = ' . $db->q($ip)); $db->setQuery($sql); try { $numOffenses = $db->loadResult(); } catch (Exception $e) { $numOffenses = 0; } if ($numOffenses < $strikes) { return; } // Block the IP $myIP = @inet_pton($ip); if ($myIP === false) { return; } $myIP = inet_ntop($myIP); $until = $jNow->toUnix(); $numfreq = $this->wafParams->getValue('tsrbannum', 1); $frequency = $this->wafParams->getValue('tsrbanfrequency', 'hour'); switch ($frequency) { case 'second': $until += $numfreq; break; case 'minute': $numfreq *= 60; $until += $numfreq; break; case 'hour': $numfreq *= 3600; $until += $numfreq; break; case 'day': $numfreq *= 86400; $until += $numfreq; break; case 'ever': $until = 2145938400; // January 1st, 2038 (mind you, UNIX epoch runs out on January 19, 2038!) break; } $jMinDate = clone Factory::getDate($until); $minDate = $jMinDate->toSql(); $record = (object) [ 'ip' => $myIP, 'reason' => $reason, 'until' => $minDate, ]; // If I'm here it means that we have to ban the user. Let's see if this is a simple autoban or // we have to issue a permaban as a result of several attacks if ($this->wafParams->getValue('permaban', 0)) { // Ok I have to check the number of autoban $query = $db->getQuery(true) ->select('COUNT(*)') ->from($db->qn('#__admintools_ipautobanhistory')) ->where($db->qn('ip') . ' = ' . $db->q($myIP)); try { $bans = $db->setQuery($query)->loadResult(); } catch (Exception $e) { $bans = 0; } $limit = (int) $this->wafParams->getValue('permabannum', 0); if ($limit && ($bans >= $limit)) { $block = (object) [ 'ip' => $myIP, 'description' => 'IP automatically blocked after being banned automatically ' . $bans . ' times', ]; try { $db->insertObject('#__admintools_ipblock', $block); Cache::resetCache('ipblock'); } catch (Exception $e) { // This should never happen, however let's prevent a white page if anything goes wrong } } } try { $db->insertObject('#__admintools_ipautoban', $record); Cache::resetCache('ipautoban'); } catch (Exception $e) { // If the IP was already blocked and I have to block it again, I'll have to update the current record $db->updateObject('#__admintools_ipautoban', $record, 'ip'); Cache::resetCache('ipautoban'); } // Send an optional email if ($this->wafParams->getValue('emailafteripautoban', '')) { // Load the component's administrator translation files $jlang = $this->getApplication()->getLanguage(); $jlang->load('com_admintools', JPATH_ADMINISTRATOR, 'en-GB', true); $jlang->load('com_admintools', JPATH_ADMINISTRATOR, $jlang->getDefault(), true); $jlang->load('com_admintools', JPATH_ADMINISTRATOR, null, true); $substitutions = $this->getEmailVariables( [ 'REASON' => $reason, 'UNTIL' => $minDate, ] ); // Send the email try { $recipients = explode(',', $this->wafParams->getValue('emailafteripautoban', '')); $recipients = array_map('trim', $recipients); foreach ($recipients as $recipient) { if (empty($recipient)) { continue; } $recipientUser = new User(); $recipientUser->username = $recipient; $recipientUser->name = $recipient; $recipientUser->email = $recipient; $data = array_merge(RescueUrl::getRescueInformation($recipient), $substitutions); $this->sendEmail('com_admintools.ipautoban', $recipientUser, $data); } } catch (Exception $e) { // Joomla! 3.5 and later throw an exception when crap happens instead of suppressing it and returning false } } } /** * Get the visitor IP address. Return false if we cannot get an IP address or if we get 0.0.0.0 (broken IP * forwarding). * * @return bool|string */ private function getVisitorIPAddress() { // Get our IP address $ip = Filter::getIp(); if ((strpos($ip, '::') === 0) && (strstr($ip, '.') !== false)) { $ip = substr($ip, strrpos($ip, ':') + 1); } // No point continuing if we can't get an address, right? if (empty($ip) || ($ip == '0.0.0.0')) { return false; } return $ip; } /** * Is the IP address in the "Never block these IPs" (safe IPs) list? * * @return bool */ private function isSafeIP() { $safeIPs = $this->wafParams->getValue('neverblockips', '') ?: []; if (is_string($safeIPs)) { $safeIPs = array_map('trim', explode(',', $safeIPs)); } $safeIPs = array_map( function ($x) { return is_array($x) ? $x[0] : $x; }, is_array($safeIPs) ? $safeIPs : [] ); return !empty($safeIPs) && Filter::IPinList($safeIPs) ? true : false; } /** * Is the IP address in the Administrator IP Whitelist? * * @return bool */ private function isIPInAdminWhitelist(): bool { if ($this->wafParams->getValue('ipwl', 0) != 1) { return false; } $ipTable = Cache::getCache('adminiplist'); if (!empty($ipTable) && Filter::IPinList($ipTable)) { return true; } return false; } /** * Is the IP address in the Site IP Allow List? * * @return bool * * @since 7.2.4 */ private function isIPInAllowList(): bool { $ipTable = Cache::getCache('ipallow'); if (!empty($ipTable) && Filter::IPinList($ipTable)) { return true; } return false; } /** * Does the IP address resolve to one of the whitelisted domain names? * * @param string $ip * * @return bool */ private function isWhitelistedDomain($ip) { static $whitelistDomains = null; if (is_null($whitelistDomains)) { $whitelistDomains = $this->wafParams->getValue('whitelist_domains', []); if (is_string($whitelistDomains)) { $whitelistDomains = array_map('trim', explode(',', $whitelistDomains)); } $whitelistDomains = array_map(function($x) { return is_array($x) ? $x[0] : $x; }, is_array($whitelistDomains) ? $whitelistDomains : []); } if (!empty($whitelistDomains)) { $remote_domain = @gethostbyaddr($ip); if (empty($remote_domain)) { return false; } foreach ($whitelistDomains as $domain) { $domain = trim($domain); if (strrpos($remote_domain, $domain) === strlen($remote_domain) - strlen($domain)) { return true; } } } return false; } /** * Get the blocking reason in a human readable format * * @param string $reason * @param string $extraLogTableInformation * * @return string */ private function getBlockingReasonHumanReadable($reason, $extraLogTableInformation) { // Load the component's administrator translation files $jlang = $this->getApplication()->getLanguage(); $jlang->load('com_admintools', JPATH_ADMINISTRATOR, 'en-GB', true); $jlang->load('com_admintools', JPATH_ADMINISTRATOR, $jlang->getDefault(), true); $jlang->load('com_admintools', JPATH_ADMINISTRATOR, null, true); // Get the reason in human readable format $txtReason = Text::_('COM_ADMINTOOLS_LOG_LBL_REASON_' . strtoupper($reason)); if (empty($extraLogTableInformation)) { return $txtReason; } // Get extra information [$logReason,] = explode('|', $extraLogTableInformation); return $txtReason . " ($logReason)"; } /** * Write a security exception to the log, as long as logging is enabled and the $reason is not one of the * $reasons_nolog ones * * @param string $reason * @param string $extraLogInformation * @param string $extraLogTableInformation * @param string $txtReason * @param array $tokens * * @return void */ private function logSecurityException($reason, $extraLogInformation, $extraLogTableInformation, $txtReason, $tokens) { $reasonsNoLog = $this->wafParams->getValue('reasons_nolog', []); // Handle legacy data if (is_string($reasonsNoLog)) { $reasonsNoLog = explode(',', $reasonsNoLog); } $reasonsNoLog = is_array($reasonsNoLog) ? $reasonsNoLog : []; // If it's a no-log reason let's get outta here if (!$this->wafParams->getValue('logbreaches', 0) || in_array($reason, $reasonsNoLog)) { return; } // Log to file $this->logSecurityExceptionToFile($reason, $extraLogInformation, $txtReason, $tokens); // Log to the database table $this->logSecurityExceptionToDatabase($reason, $extraLogTableInformation, $tokens); } /** * Log a security exception to our log file * * @param string $reason * @param string $extraLogInformation * @param string $txtReason * @param array $tokens */ private function logSecurityExceptionToFile($reason, $extraLogInformation, $txtReason, $tokens) { // Write to the log file only if we're told to if (!$this->wafParams->getValue('logfile', 0)) { return; } // Get the log filename $logpath = $this->getApplication()->get('log_path'); $fname = $logpath . DIRECTORY_SEPARATOR . 'admintools_blocked.php'; // -- Check the file size. If it's over 1Mb, archive and start a new log. if (@file_exists($fname)) { $fsize = filesize($fname); if ($fsize > 1048756) { $altFile = substr($fname, 0, -4) . '.1.php'; if (@file_exists($altFile)) { unlink($altFile); } @copy($fname, $altFile); @unlink($fname); } } // If the main log file does not exist yet create a new one. if (!file_exists($fname)) { $content = <<< END php /** * ===================================================================================================================== * Admin Tools debug log file * ===================================================================================================================== * * This file contains a dump of the requests which were blocked by Admin Tools. By definition, this file does contain * a lot of "hacking signatures" since this is what the Admin Tools component is designed to stop and this is the file * logging all these hacking attempts. * * You can disable the creation of this file by going to Components, Admin Tools, Web Application Firewall, Configure * WAF and setting the "Keep a debug log file" option to NO. This is the recommended setting. You should only set this * option to YES if you are troubleshooting an issue (Admin Tools is blocking access to your site). * * Some hosts will mistakenly report this file as suspicious or hacked. As a result they might issue an automated * warning and / or block access to your site. Should that happen please ask your host to look in this file and read * this header. This file is SAFE since the only executable statement is die() below which prevents the file from being * executed at all. If your host does not understand that this file is safe or does not know how to add an exception in * their automated scanner to exempt Joomla's log files (all files under this directory) from being flagged as hacked / * suspicious we strongly recommend going to a different host that understands how PHP works. It will be safer for you * as well. */ die(); END; $content = "?$content?"; $content .= ">\n\n"; file_put_contents($fname, '<' . $content); } // -- Log the exception $fp = @fopen($fname, 'a'); if ($fp === false) { return; } fwrite($fp, str_repeat('-', 79) . PHP_EOL); fwrite($fp, "Blocking reason: " . $reason . PHP_EOL . str_repeat('-', 79) . PHP_EOL); fwrite($fp, "Reason : " . $txtReason . PHP_EOL); fwrite($fp, 'Timestamp : ' . gmdate('Y-m-d H:i:s') . " GMT" . PHP_EOL); fwrite($fp, 'Local time : ' . $tokens['[DATE]'] . " " . PHP_EOL); fwrite($fp, 'URL : ' . $tokens['[URL]'] . PHP_EOL); fwrite($fp, 'User : ' . $tokens['[USER]'] . PHP_EOL); fwrite($fp, 'IP : ' . $tokens['[IP]'] . PHP_EOL); fwrite($fp, 'UA : ' . $tokens['[UA]'] . PHP_EOL); if (!empty($extraLogInformation)) { fwrite($fp, $extraLogInformation . PHP_EOL); } fwrite($fp, PHP_EOL . PHP_EOL); fclose($fp); } /** * Log a security exception to the database table * * @param string $reason * @param string $extraLogInformation * @param array $tokens * * * @since version */ private function logSecurityExceptionToDatabase($reason, $extraLogTableInformation, $tokens) { try { /** @var DatabaseDriver $db */ $db = $this->getDatabase(); $date = clone Factory::getDate(); $url = $tokens['URL']; if (strlen($url) > 10240) { $url = substr($url, 0, 10240); } $logEntry = (object) [ 'logdate' => $date->toSql(), 'ip' => $tokens['IP'], 'url' => $url, 'reason' => $reason, 'extradata' => $extraLogTableInformation, ]; $db->insertObject('#__admintools_log', $logEntry); } catch (Exception $e) { // Do nothing if the query fails } } /** * Sends information about the security exception by email * * @param string $reason * @param array $tokens * * @return bool */ private function emailSecurityException($reason, $tokens) { $emailOnException = $this->wafParams->getValue('emailbreaches', ''); $reasonsNoEmail = $this->wafParams->getValue('reasons_noemail', '') ?: []; $reasonsNoEmail = is_string($reasonsNoEmail) ? explode(',', $reasonsNoEmail) : $reasonsNoEmail; if (empty($emailOnException) || in_array($reason, $reasonsNoEmail)) { return true; } // Send the email try { $recipients = explode(',', $emailOnException); $recipients = array_map('trim', $recipients); foreach ($recipients as $recipient) { if (empty($recipient)) { continue; } if (empty($recipient)) { continue; } $recipientUser = new User(); $recipientUser->username = $recipient; $recipientUser->name = $recipient; $recipientUser->email = $recipient; $tokens = array_merge($tokens, RescueUrl::getRescueInformation($recipient)); $this->sendSecurityExceptionEmail($reason, $recipientUser, $tokens); } } catch (Exception $e) { } return true; } /** * Is sending an email allowed by the email throttling feature? * * @return bool * * @since 7.2.2 */ private function isSendingAllowedByEmailThrottling(): bool { $cParams = ComponentHelper::getParams('com_admintools'); // If the throttling feature is disabled allow sending the email. if ($cParams->get('email_throttle', 1) != 1) { return true; } // Get the frequency limit options $maxAllowedEmails = $cParams->get('email_num', 5); $timePeriod = $cParams->get('email_numfreq', 15); $timeUOM = $cParams->get('email_freq', 'minutes'); switch ($timeUOM) { case 'seconds': $earliestDate = Factory::getDate()->sub(new \DateInterval('PT' . $timePeriod . 'S')); break; case 'minutes': $earliestDate = Factory::getDate()->sub(new \DateInterval('PT' . $timePeriod . 'M')); break; case 'hours': $earliestDate = Factory::getDate()->sub(new \DateInterval('PT' . $timePeriod . 'H')); break; case 'days': $earliestDate = Factory::getDate()->sub(new \DateInterval('P' . $timePeriod . 'D')); break; case 'ever': default: $earliestDate = Factory::getDate('2000-01-01 00:00:00'); break; } $reasonsNoLog = $this->wafParams->getValue('reasons_nolog', []) ?: []; $reasonsNoLog = is_array($reasonsNoLog) ? $reasonsNoLog : array_map('trim', @explode(',', $reasonsNoLog)); /** @var DatabaseDriver $db */ $db = $this->getDatabase(); $logDate = $earliestDate->toSql(); $sql = $db->getQuery(true) ->select('COUNT(*)') ->from($db->qn('#__admintools_log')) ->where($db->qn('logdate') . ' >= :logDate') ->bind(':logDate', $logDate); // Apply the where clause only if we have excluded any reason from logging if (!empty($reasonsNoLog)) { $sql->whereNotIn($db->qn('reason'), $reasonsNoLog, ParameterType::STRING); } $db->setQuery($sql); try { $numOffenses = $db->loadResult() ?: 0; } catch (Exception $e) { $numOffenses = 0; } return $numOffenses <= $maxAllowedEmails; } }