You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
2063 lines
55 KiB
2063 lines
55 KiB
<?php |
|
require_once(dirname(__FILE__) . '/wfDB.php'); |
|
require_once(dirname(__FILE__) . '/wfUtils.php'); |
|
require_once(dirname(__FILE__) . '/wfBrowscap.php'); |
|
class wfLog { |
|
public $canLogHit = true; |
|
private $effectiveUserID = 0; |
|
private $hitsTable = ''; |
|
private $apiKey = ''; |
|
private $wp_version = ''; |
|
private $db = false; |
|
private $googlePattern = '/\.(?:googlebot\.com|google\.[a-z]{2,3}|google\.[a-z]{2}\.[a-z]{2}|1e100\.net)$/i'; |
|
private static $gbSafeCache = array(); |
|
|
|
/** |
|
* @var wfRequestModel |
|
*/ |
|
private $currentRequest; |
|
|
|
public static function shared() { |
|
static $_shared = null; |
|
if ($_shared === null) { |
|
$_shared = new wfLog(wfConfig::get('apiKey'), wfUtils::getWPVersion()); |
|
} |
|
return $_shared; |
|
} |
|
|
|
/** |
|
* Returns whether or not we have a cached record identifying the visitor as human. This is used both by certain |
|
* rate limiting features and by Live Traffic. |
|
* |
|
* @param bool|string $IP |
|
* @param bool|string $UA |
|
* @return bool |
|
*/ |
|
public static function isHumanRequest($IP = false, $UA = false) { |
|
global $wpdb; |
|
|
|
if ($IP === false) { |
|
$IP = wfUtils::getIP(); |
|
} |
|
|
|
if ($UA === false) { |
|
$UA = (isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''); |
|
} |
|
|
|
$table = wfDB::networkTable('wfLiveTrafficHuman'); |
|
if ($wpdb->get_var($wpdb->prepare("SELECT COUNT(*) FROM {$table} WHERE IP = %s AND identifier = %s AND expiration >= UNIX_TIMESTAMP()", wfUtils::inet_pton($IP), hash('sha256', $UA, true)))) { |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
/** |
|
* Creates a cache record for the requester to tag it as human. |
|
* |
|
* @param bool|string $IP |
|
* @param bool|string $UA |
|
* @return bool |
|
*/ |
|
public static function cacheHumanRequester($IP = false, $UA = false) { |
|
global $wpdb; |
|
|
|
if ($IP === false) { |
|
$IP = wfUtils::getIP(); |
|
} |
|
|
|
if ($UA === false) { |
|
$UA = (isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''); |
|
} |
|
|
|
$table = wfDB::networkTable('wfLiveTrafficHuman'); |
|
if ($wpdb->get_var($wpdb->prepare("INSERT IGNORE INTO {$table} (IP, identifier, expiration) VALUES (%s, %s, UNIX_TIMESTAMP() + 86400)", wfUtils::inet_pton($IP), hash('sha256', $UA, true)))) { |
|
return true; |
|
} |
|
} |
|
|
|
/** |
|
* Prunes any expired records from the human cache. |
|
*/ |
|
public static function trimHumanCache() { |
|
global $wpdb; |
|
$table = wfDB::networkTable('wfLiveTrafficHuman'); |
|
$wpdb->query("DELETE FROM {$table} WHERE `expiration` < UNIX_TIMESTAMP()"); |
|
} |
|
|
|
public function __construct($apiKey, $wp_version){ |
|
$this->apiKey = $apiKey; |
|
$this->wp_version = $wp_version; |
|
$this->hitsTable = wfDB::networkTable('wfHits'); |
|
$this->loginsTable = wfDB::networkTable('wfLogins'); |
|
$this->blocksTable = wfBlock::blocksTable(); |
|
$this->lockOutTable = wfDB::networkTable('wfLockedOut'); |
|
$this->throttleTable = wfDB::networkTable('wfThrottleLog'); |
|
$this->statusTable = wfDB::networkTable('wfStatus'); |
|
$this->ipRangesTable = wfDB::networkTable('wfBlocksAdv'); |
|
|
|
add_filter('determine_current_user', array($this, '_userIDDetermined'), 99, 1); |
|
} |
|
|
|
public function _userIDDetermined($userID) { |
|
//Needed because the REST API will clear the authenticated user if it fails a nonce check on the request |
|
$this->effectiveUserID = (int) $userID; |
|
return $userID; |
|
} |
|
|
|
public function initLogRequest() { |
|
if ($this->currentRequest === null) { |
|
$this->currentRequest = new wfRequestModel(); |
|
|
|
$this->currentRequest->ctime = sprintf('%.6f', microtime(true)); |
|
$this->currentRequest->statusCode = 200; |
|
$this->currentRequest->isGoogle = (wfCrawl::isGoogleCrawler() ? 1 : 0); |
|
$this->currentRequest->IP = wfUtils::inet_pton(wfUtils::getIP()); |
|
$this->currentRequest->userID = $this->getCurrentUserID(); |
|
$this->currentRequest->URL = wfUtils::getRequestedURL(); |
|
$this->currentRequest->referer = (isset($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : ''); |
|
$this->currentRequest->UA = (isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''); |
|
$this->currentRequest->jsRun = 0; |
|
|
|
add_action('wp_loaded', array($this, 'actionSetRequestJSEnabled')); |
|
add_action('init', array($this, 'actionSetRequestOnInit'), 9999); |
|
|
|
if (function_exists('register_shutdown_function')) { |
|
register_shutdown_function(array($this, 'logHit')); |
|
} |
|
} |
|
} |
|
|
|
public function actionSetRequestJSEnabled() { |
|
if (get_current_user_id() > 0) { |
|
$this->currentRequest->jsRun = true; |
|
return; |
|
} |
|
|
|
$IP = wfUtils::getIP(); |
|
$UA = $this->currentRequest->UA; |
|
$this->currentRequest->jsRun = wfLog::isHumanRequest($IP, $UA); |
|
} |
|
|
|
/** |
|
* CloudFlare's plugin changes $_SERVER['REMOTE_ADDR'] on init. |
|
*/ |
|
public function actionSetRequestOnInit() { |
|
$this->currentRequest->IP = wfUtils::inet_pton(wfUtils::getIP()); |
|
$this->currentRequest->userID = $this->getCurrentUserID(); |
|
} |
|
|
|
/** |
|
* @return wfRequestModel |
|
*/ |
|
public function getCurrentRequest() { |
|
return $this->currentRequest; |
|
} |
|
|
|
public function logLogin($action, $fail, $username){ |
|
if(! $username){ |
|
return; |
|
} |
|
$user = get_user_by('login', $username); |
|
$userID = 0; |
|
if($user){ |
|
$userID = $user->ID; |
|
if(! $userID){ |
|
return; |
|
} |
|
} |
|
else { |
|
$user = get_user_by('email', $username); |
|
if ($user) { |
|
$userID = $user->ID; |
|
if (!$userID) { |
|
return; |
|
} |
|
} |
|
} |
|
// change the action flag here if the user does not exist. |
|
if ($action == 'loginFailValidUsername' && $userID == 0) { |
|
$action = 'loginFailInvalidUsername'; |
|
} |
|
|
|
$hitID = 0; |
|
if ($this->currentRequest !== null) { |
|
$this->currentRequest->userID = $userID; |
|
$this->currentRequest->action = $action; |
|
$this->currentRequest->save(); |
|
$hitID = $this->currentRequest->getPrimaryKey(); |
|
} |
|
|
|
//Else userID stays 0 but we do log this even though the user doesn't exist. |
|
$this->getDB()->queryWrite("insert into " . $this->loginsTable . " (hitID, ctime, fail, action, username, userID, IP, UA) values (%d, %f, %d, '%s', '%s', %s, %s, '%s')", |
|
$hitID, |
|
sprintf('%.6f', microtime(true)), |
|
$fail, |
|
$action, |
|
$username, |
|
$userID, |
|
wfUtils::inet_pton(wfUtils::getIP()), |
|
(isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '') |
|
); |
|
} |
|
private function getCurrentUserID(){ |
|
if (!function_exists('get_current_user_id') || !defined('AUTH_COOKIE')) { //If pluggable.php is loaded early by some other plugin on a multisite installation, it leads to an error because AUTH_COOKIE is undefined and WP doesn't check for it first |
|
return 0; |
|
} |
|
$id = get_current_user_id(); |
|
return $id ? $id : 0; |
|
} |
|
public function logLeechAndBlock($type) { //404 or hit |
|
if (!wfRateLimit::mightRateLimit($type)) { |
|
return; |
|
} |
|
|
|
wfRateLimit::countHit($type, wfUtils::getIP()); |
|
|
|
if (wfRateLimit::globalRateLimit()->shouldEnforce($type)) { |
|
$this->takeBlockingAction('maxGlobalRequests', "Exceeded the maximum global requests per minute for crawlers or humans."); |
|
} |
|
else if (wfRateLimit::crawlerViewsRateLimit()->shouldEnforce($type)) { |
|
$this->takeBlockingAction('maxRequestsCrawlers', "Exceeded the maximum number of requests per minute for crawlers."); //may not exit |
|
} |
|
else if (wfRateLimit::crawler404sRateLimit()->shouldEnforce($type)) { |
|
$this->takeBlockingAction('max404Crawlers', "Exceeded the maximum number of page not found errors per minute for a crawler."); |
|
} |
|
else if (wfRateLimit::humanViewsRateLimit()->shouldEnforce($type)) { |
|
$this->takeBlockingAction('maxRequestsHumans', "Exceeded the maximum number of page requests per minute for humans."); |
|
} |
|
else if (wfRateLimit::human404sRateLimit()->shouldEnforce($type)) { |
|
$this->takeBlockingAction('max404Humans', "Exceeded the maximum number of page not found errors per minute for humans."); |
|
} |
|
} |
|
|
|
public function tagRequestForBlock($reason, $wfsn = false) { |
|
if ($this->currentRequest !== null) { |
|
$this->currentRequest->statusCode = 403; |
|
$this->currentRequest->action = 'blocked:' . ($wfsn ? 'wfsn' : 'wordfence'); |
|
$this->currentRequest->actionDescription = $reason; |
|
} |
|
} |
|
|
|
public function tagRequestForLockout($reason) { |
|
if ($this->currentRequest !== null) { |
|
$this->currentRequest->statusCode = 503; |
|
$this->currentRequest->action = 'lockedOut'; |
|
$this->currentRequest->actionDescription = $reason; |
|
} |
|
} |
|
|
|
/** |
|
* @return bool|int |
|
*/ |
|
public function logHit() { |
|
$liveTrafficEnabled = wfConfig::liveTrafficEnabled(); |
|
$action = $this->currentRequest->action; |
|
$logHitOK = $this->logHitOK(); |
|
if (!$logHitOK) { |
|
return false; |
|
} |
|
if (!$liveTrafficEnabled && !$action) { |
|
return false; |
|
} |
|
if ($this->currentRequest !== null) { |
|
if ($this->currentRequest->save()) { |
|
return $this->currentRequest->getPrimaryKey(); |
|
} |
|
} |
|
return false; |
|
} |
|
|
|
public function getHits($hitType /* 'hits' or 'logins' */, $type, $afterTime, $limit = 50, $IP = false){ |
|
global $wpdb; |
|
$IPSQL = ""; |
|
if($IP){ |
|
$IPSQL = " and IP=%s "; |
|
$sqlArgs = array($afterTime, wfUtils::inet_pton($IP), $limit); |
|
} else { |
|
$sqlArgs = array($afterTime, $limit); |
|
} |
|
if($hitType == 'hits'){ |
|
$securityOnly = !wfConfig::liveTrafficEnabled(); |
|
$delayedHumanBotFiltering = false; |
|
|
|
if($type == 'hit'){ |
|
$typeSQL = " "; |
|
} else if($type == 'crawler'){ |
|
if ($securityOnly) { |
|
$typeSQL = " "; |
|
$delayedHumanBotFiltering = true; |
|
} |
|
else { |
|
$now = time(); |
|
$typeSQL = " and jsRun = 0 and {$now} - ctime > 30 "; |
|
} |
|
} else if($type == 'gCrawler'){ |
|
$typeSQL = " and isGoogle = 1 "; |
|
} else if($type == '404'){ |
|
$typeSQL = " and statusCode = 404 "; |
|
} else if($type == 'human'){ |
|
if ($securityOnly) { |
|
$typeSQL = " "; |
|
$delayedHumanBotFiltering = true; |
|
} |
|
else { |
|
$typeSQL = " and jsRun = 1 "; |
|
} |
|
} else if($type == 'ruser'){ |
|
$typeSQL = " and userID > 0 "; |
|
} else { |
|
wordfence::status(1, 'error', "Invalid log type to wfLog: $type"); |
|
return false; |
|
} |
|
array_unshift($sqlArgs, "select h.*, u.display_name from {$this->hitsTable} h |
|
LEFT JOIN {$wpdb->users} u on h.userID = u.ID |
|
where ctime > %f $IPSQL $typeSQL order by ctime desc limit %d"); |
|
$results = call_user_func_array(array($this->getDB(), 'querySelect'), $sqlArgs); |
|
|
|
if ($delayedHumanBotFiltering) { |
|
$browscap = wfBrowscap::shared(); |
|
foreach ($results as $index => $res) { |
|
if ($res['UA']) { |
|
$b = $browscap->getBrowser($res['UA']); |
|
if ($b && $b['Parent'] != 'DefaultProperties') { |
|
$jsRun = wfUtils::truthyToBoolean($res['jsRun']); |
|
if (!wfConfig::liveTrafficEnabled() && !$jsRun) { |
|
$jsRun = !(isset($b['Crawler']) && $b['Crawler']); |
|
} |
|
|
|
if ($type == 'crawler' && $jsRun || $type == 'human' && !$jsRun) { |
|
unset($results[$index]); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
|
|
} else if($hitType == 'logins'){ |
|
array_unshift($sqlArgs, "select l.*, u.display_name from {$this->loginsTable} l |
|
LEFT JOIN {$wpdb->users} u on l.userID = u.ID |
|
where ctime > %f $IPSQL order by ctime desc limit %d"); |
|
$results = call_user_func_array(array($this->getDB(), 'querySelect'), $sqlArgs ); |
|
|
|
} else { |
|
wordfence::status(1, 'error', "getHits got invalid hitType: $hitType"); |
|
return false; |
|
} |
|
$this->processGetHitsResults($type, $results); |
|
return $results; |
|
} |
|
|
|
/** |
|
* @param string $type |
|
* @param array $results |
|
* @throws Exception |
|
*/ |
|
public function processGetHitsResults($type, &$results) { |
|
$serverTime = $this->getDB()->querySingle("select unix_timestamp()"); |
|
|
|
$this->resolveIPs($results); |
|
$ourURL = parse_url(site_url()); |
|
$ourHost = strtolower($ourURL['host']); |
|
$ourHost = preg_replace('/^www\./i', '', $ourHost); |
|
$browscap = wfBrowscap::shared(); |
|
|
|
$patternBlocks = wfBlock::patternBlocks(true); |
|
|
|
foreach($results as &$res){ |
|
$res['type'] = $type; |
|
$res['IP'] = wfUtils::inet_ntop($res['IP']); |
|
$res['timeAgo'] = wfUtils::makeTimeAgo($serverTime - $res['ctime']); |
|
$res['blocked'] = false; |
|
$res['rangeBlocked'] = false; |
|
$res['ipRangeID'] = -1; |
|
|
|
$ipBlock = wfBlock::findIPBlock($res['IP']); |
|
if ($ipBlock !== false) { |
|
$res['blocked'] = true; |
|
$res['blockID'] = $ipBlock->id; |
|
} |
|
|
|
foreach ($patternBlocks as $b) { |
|
if (empty($b->ipRange)) { continue; } |
|
$range = new wfUserIPRange($b->ipRange); |
|
if ($range->isIPInRange($res['IP'])) { |
|
$res['rangeBlocked'] = true; |
|
$res['ipRangeID'] = $b->id; |
|
break; |
|
} |
|
} |
|
|
|
$res['extReferer'] = false; |
|
if(isset( $res['referer'] ) && $res['referer']){ |
|
if(wfUtils::hasXSS($res['referer'] )){ //filtering out XSS |
|
$res['referer'] = ''; |
|
} |
|
} |
|
if( isset( $res['referer'] ) && $res['referer']){ |
|
$refURL = parse_url($res['referer']); |
|
if(is_array($refURL) && isset($refURL['host']) && $refURL['host']){ |
|
$refHost = strtolower(preg_replace('/^www\./i', '', $refURL['host'])); |
|
if($refHost != $ourHost){ |
|
$res['extReferer'] = true; |
|
//now extract search terms |
|
$q = false; |
|
if(preg_match('/(?:google|bing|alltheweb|aol|ask)\./i', $refURL['host'])){ |
|
$q = 'q'; |
|
} else if(stristr($refURL['host'], 'yahoo.')){ |
|
$q = 'p'; |
|
} else if(stristr($refURL['host'], 'baidu.')){ |
|
$q = 'wd'; |
|
} |
|
if($q){ |
|
$queryVars = array(); |
|
if( isset( $refURL['query'] ) ) { |
|
parse_str($refURL['query'], $queryVars); |
|
if(isset($queryVars[$q])){ |
|
$res['searchTerms'] = urlencode($queryVars[$q]); |
|
} |
|
} |
|
} |
|
} |
|
} |
|
if($res['extReferer']){ |
|
if ( isset( $referringPage ) && stristr( $referringPage['host'], 'google.' ) ) |
|
{ |
|
parse_str( $referringPage['query'], $queryVars ); |
|
// echo $queryVars['q']; // This is the search term used |
|
} |
|
} |
|
} |
|
$res['browser'] = false; |
|
if($res['UA']){ |
|
$b = $browscap->getBrowser($res['UA']); |
|
if($b && $b['Parent'] != 'DefaultProperties'){ |
|
$res['browser'] = array( |
|
'browser' => !empty($b['Browser']) ? $b['Browser'] : "", |
|
'version' => !empty($b['Version']) ? $b['Version'] : "", |
|
'platform' => !empty($b['Platform']) ? $b['Platform'] : "", |
|
'isMobile' => !empty($b['isMobileDevice']) ? $b['isMobileDevice'] : "", |
|
'isCrawler' => !empty($b['Crawler']) ? $b['Crawler'] : "", |
|
); |
|
|
|
if (isset($res['jsRun']) && !wfConfig::liveTrafficEnabled() && !wfUtils::truthyToBoolean($res['jsRun'])) { |
|
$res['jsRun'] = !(isset($b['Crawler']) && $b['Crawler']) ? '1' : '0'; |
|
} |
|
} |
|
else { |
|
$IP = wfUtils::getIP(); |
|
$res['browser'] = array( |
|
'isCrawler' => !wfLog::isHumanRequest($IP, $res['UA']) ? 'true' : '' |
|
); |
|
} |
|
} |
|
|
|
|
|
if($res['userID']){ |
|
$ud = get_userdata($res['userID']); |
|
if($ud){ |
|
$res['user'] = array( |
|
'editLink' => wfUtils::editUserLink($res['userID']), |
|
'display_name' => $res['display_name'], |
|
'ID' => $res['userID'] |
|
); |
|
} |
|
} else { |
|
$res['user'] = false; |
|
} |
|
} |
|
} |
|
|
|
public function resolveIPs(&$results){ |
|
if(sizeof($results) < 1){ return; } |
|
$IPs = array(); |
|
foreach($results as &$res){ |
|
if($res['IP']){ //Can also be zero in case of non IP events |
|
$IPs[] = $res['IP']; |
|
} |
|
} |
|
$IPLocs = wfUtils::getIPsGeo($IPs); //Creates an array with IP as key and data as value |
|
|
|
foreach($results as &$res){ |
|
$ip_printable = wfUtils::inet_ntop($res['IP']); |
|
if(isset($IPLocs[$ip_printable])){ |
|
$res['loc'] = $IPLocs[$ip_printable]; |
|
} else { |
|
$res['loc'] = false; |
|
} |
|
} |
|
} |
|
public function logHitOK(){ |
|
if (!$this->canLogHit) { |
|
return false; |
|
} |
|
if (is_admin()) { return false; } //Don't log admin pageviews |
|
if (isset($_SERVER['HTTP_USER_AGENT'])) { |
|
if (preg_match('/WordPress\/' . $this->wp_version . '/i', $_SERVER['HTTP_USER_AGENT'])) { return false; } //Ignore regular requests generated by WP UA. |
|
} |
|
$userID = get_current_user_id(); |
|
if (!$userID) { |
|
$userID = $this->effectiveUserID; |
|
} |
|
if ($userID) { |
|
$user = new WP_User($userID); |
|
if ($user && $user->exists()) { |
|
if (wfConfig::get('liveTraf_ignorePublishers') && ($user->has_cap('publish_posts') || $user->has_cap('publish_pages'))) { |
|
return false; |
|
} |
|
|
|
if (wfConfig::get('liveTraf_ignoreUsers')) { |
|
$ignored = explode(',', wfConfig::get('liveTraf_ignoreUsers')); |
|
foreach ($ignored as $entry) { |
|
if($user->user_login == $entry){ |
|
return false; |
|
} |
|
} |
|
} |
|
} |
|
} |
|
if(wfConfig::get('liveTraf_ignoreIPs')){ |
|
$IPs = explode(',', wfConfig::get('liveTraf_ignoreIPs')); |
|
$IP = wfUtils::getIP(); |
|
foreach($IPs as $ignoreIP){ |
|
if($ignoreIP == $IP){ |
|
return false; |
|
} |
|
} |
|
} |
|
if( isset($_SERVER['HTTP_USER_AGENT']) && wfConfig::get('liveTraf_ignoreUA') ){ |
|
if($_SERVER['HTTP_USER_AGENT'] == wfConfig::get('liveTraf_ignoreUA')){ |
|
return false; |
|
} |
|
} |
|
|
|
return true; |
|
} |
|
private function getDB(){ |
|
if(! $this->db){ |
|
$this->db = new wfDB(); |
|
} |
|
return $this->db; |
|
} |
|
public function firewallBadIPs() { |
|
$IP = wfUtils::getIP(); |
|
if (wfBlock::isWhitelisted($IP)) { |
|
return; |
|
} |
|
|
|
//Range and UA pattern blocking |
|
$patternBlocks = wfBlock::patternBlocks(true); |
|
$userAgent = !empty($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : ''; |
|
$referrer = !empty($_SERVER['HTTP_REFERER']) ? $_SERVER['HTTP_REFERER'] : ''; |
|
foreach ($patternBlocks as $b) { |
|
if ($b->matchRequest($IP, $userAgent, $referrer) !== wfBlock::MATCH_NONE) { |
|
$b->recordBlock(); |
|
wfActivityReport::logBlockedIP($IP, null, 'advanced'); |
|
$this->currentRequest->actionDescription = 'UA/Referrer/IP Range not allowed'; |
|
$this->do503(3600, "Advanced blocking in effect."); //exits |
|
} |
|
} |
|
|
|
// Country blocking |
|
$countryBlocks = wfBlock::countryBlocks(true); |
|
foreach ($countryBlocks as $b) { |
|
$match = $b->matchRequest($IP, false, false); |
|
if ($match === wfBlock::MATCH_COUNTRY_REDIR_BYPASS) { |
|
$bypassRedirDest = wfConfig::get('cbl_bypassRedirDest', ''); |
|
|
|
$this->initLogRequest(); |
|
$this->getCurrentRequest()->actionDescription = __('redirected to bypass URL', 'wordfence'); |
|
$this->getCurrentRequest()->statusCode = 302; |
|
$this->currentRequest->action = 'cbl:redirect'; |
|
$this->logHit(); |
|
|
|
wfUtils::doNotCache(); |
|
wp_redirect($bypassRedirDest, 302); |
|
exit(); |
|
} |
|
else if ($match === wfBlock::MATCH_COUNTRY_REDIR) { |
|
$b->recordBlock(); |
|
wfConfig::inc('totalCountryBlocked'); |
|
|
|
$this->initLogRequest(); |
|
$this->getCurrentRequest()->actionDescription = sprintf(__('blocked access via country blocking and redirected to URL (%s)', 'wordfence'), wfConfig::get('cbl_redirURL')); |
|
$this->getCurrentRequest()->statusCode = 503; |
|
if (!$this->getCurrentRequest()->action) { |
|
$this->currentRequest->action = 'blocked:wordfence'; |
|
} |
|
$this->logHit(); |
|
|
|
wfActivityReport::logBlockedIP($IP, null, 'country'); |
|
|
|
wfUtils::doNotCache(); |
|
wp_redirect(wfConfig::get('cbl_redirURL'), 302); |
|
exit(); |
|
} |
|
else if ($match !== wfBlock::MATCH_NONE) { |
|
$b->recordBlock(); |
|
$this->currentRequest->actionDescription = __('blocked access via country blocking', 'wordfence'); |
|
wfConfig::inc('totalCountryBlocked'); |
|
wfActivityReport::logBlockedIP($IP, null, 'country'); |
|
$this->do503(3600, __('Access from your area has been temporarily limited for security reasons', 'wordfence')); |
|
} |
|
} |
|
|
|
//Specific IP blocks |
|
$ipBlock = wfBlock::findIPBlock($IP); |
|
if ($ipBlock !== false) { |
|
$ipBlock->recordBlock(); |
|
$secsToGo = max(0, $ipBlock->expiration - time()); |
|
if (wfConfig::get('other_WFNet') && self::isAuthRequest()) { //It's an auth request and this IP has been blocked |
|
$this->getCurrentRequest()->action = 'blocked:wfsnrepeat'; |
|
wordfence::wfsnReportBlockedAttempt($IP, 'login'); |
|
} |
|
$reason = $ipBlock->reason; |
|
if ($ipBlock->type == wfBlock::TYPE_IP_MANUAL || $ipBlock->type == wfBlock::TYPE_IP_AUTOMATIC_PERMANENT) { |
|
$reason = __('Manual block by administrator', 'wordfence'); |
|
} |
|
$this->do503($secsToGo, $reason); //exits |
|
} |
|
} |
|
|
|
private function takeBlockingAction($configVar, $reason) { |
|
if ($this->googleSafetyCheckOK()) { |
|
$action = wfConfig::get($configVar . '_action'); |
|
if (!$action) { |
|
return; |
|
} |
|
|
|
$IP = wfUtils::getIP(); |
|
$secsToGo = 0; |
|
if ($action == 'block') { //Rate limited - block temporarily |
|
$secsToGo = wfBlock::blockDuration(); |
|
wfBlock::createRateBlock($reason, $IP, $secsToGo); |
|
wfActivityReport::logBlockedIP($IP, null, 'throttle'); |
|
$this->tagRequestForBlock($reason); |
|
|
|
$alertCallback = array(new wfBlockAlert($IP, $reason, $secsToGo), 'send'); |
|
|
|
do_action('wordfence_security_event', 'block', array( |
|
'ip' => $IP, |
|
'reason' => $reason, |
|
'duration' => $secsToGo, |
|
), $alertCallback); |
|
wordfence::status(2, 'info', sprintf(__('Blocking IP %s. %s', 'wordfence'), $IP, $reason)); |
|
} |
|
else if ($action == 'throttle') { //Rate limited - throttle |
|
$secsToGo = wfBlock::rateLimitThrottleDuration(); |
|
wfBlock::createRateThrottle($reason, $IP, $secsToGo); |
|
wfActivityReport::logBlockedIP($IP, null, 'throttle'); |
|
|
|
do_action('wordfence_security_event', 'throttle', array( |
|
'ip' => $IP, |
|
'reason' => $reason, |
|
'duration' => $secsToGo, |
|
)); |
|
wordfence::status(2, 'info', sprintf(__('Throttling IP %s. %s', 'wordfence'), $IP, $reason)); |
|
wfConfig::inc('totalIPsThrottled'); |
|
} |
|
$this->do503($secsToGo, $reason, false); |
|
} |
|
|
|
return; |
|
} |
|
|
|
/** |
|
* Test if the current request is for wp-login.php or xmlrpc.php |
|
* |
|
* @return boolean |
|
*/ |
|
private static function isAuthRequest() { |
|
if ((strpos($_SERVER['REQUEST_URI'], '/wp-login.php') !== false)) { |
|
return true; |
|
} |
|
return false; |
|
} |
|
|
|
public function do503($secsToGo, $reason, $sendEventToCentral = true){ |
|
$this->initLogRequest(); |
|
|
|
if ($sendEventToCentral) { |
|
do_action('wordfence_security_event', 'block', array( |
|
'ip' => wfUtils::inet_ntop($this->currentRequest->IP), |
|
'reason' => $this->currentRequest->actionDescription ? $this->currentRequest->actionDescription : $reason, |
|
'duration' => $secsToGo, |
|
)); |
|
} |
|
|
|
$this->currentRequest->statusCode = 503; |
|
if (!$this->currentRequest->action) { |
|
$this->currentRequest->action = 'blocked:wordfence'; |
|
} |
|
if (!$this->currentRequest->actionDescription) { |
|
$this->currentRequest->actionDescription = "blocked: " . $reason; |
|
} |
|
|
|
$this->logHit(); |
|
|
|
wfConfig::inc('total503s'); |
|
wfUtils::doNotCache(); |
|
header('HTTP/1.1 503 Service Temporarily Unavailable'); |
|
header('Status: 503 Service Temporarily Unavailable'); |
|
if($secsToGo){ |
|
header('Retry-After: ' . $secsToGo); |
|
} |
|
$customText = wpautop(wp_strip_all_tags(wfConfig::get('blockCustomText', ''))); |
|
require_once(dirname(__FILE__) . '/wf503.php'); |
|
exit(); |
|
} |
|
private function redirect($URL){ |
|
wfUtils::doNotCache(); |
|
wp_redirect($URL, 302); |
|
exit(); |
|
} |
|
private function googleSafetyCheckOK(){ //returns true if OK to block. Returns false if we must not block. |
|
$cacheKey = md5( (isset($_SERVER['HTTP_USER_AGENT']) ? $_SERVER['HTTP_USER_AGENT'] : '') . ' ' . wfUtils::getIP()); |
|
//Cache so we can call this multiple times in one request |
|
if(! isset(self::$gbSafeCache[$cacheKey])){ |
|
$nb = wfConfig::get('neverBlockBG'); |
|
if($nb == 'treatAsOtherCrawlers'){ |
|
self::$gbSafeCache[$cacheKey] = true; //OK to block because we're treating google like everyone else |
|
} else if($nb == 'neverBlockUA' || $nb == 'neverBlockVerified'){ |
|
if(wfCrawl::isGoogleCrawler()){ //Check the UA using regex |
|
if($nb == 'neverBlockVerified'){ |
|
if(wfCrawl::isVerifiedGoogleCrawler(wfUtils::getIP())){ //UA check passed, now verify using PTR if configured to |
|
self::$gbSafeCache[$cacheKey] = false; //This is a verified Google crawler, so no we can't block it |
|
} else { |
|
self::$gbSafeCache[$cacheKey] = true; //This is a crawler claiming to be Google but it did not verify |
|
} |
|
} else { //neverBlockUA |
|
self::$gbSafeCache[$cacheKey] = false; //User configured us to only do a UA check and this claims to be google so don't block |
|
} |
|
} else { |
|
self::$gbSafeCache[$cacheKey] = true; //This isn't a Google UA, so it's OK to block |
|
} |
|
} else { |
|
//error_log("Wordfence error: neverBlockBG option is not set."); |
|
self::$gbSafeCache[$cacheKey] = false; //Oops the config option is not set. This should never happen because it's set on install. So we return false to indicate it's not OK to block just for safety. |
|
} |
|
} |
|
if(! isset(self::$gbSafeCache[$cacheKey])){ |
|
//error_log("Wordfence assertion fail in googleSafetyCheckOK: cached value is not set."); |
|
return false; //for safety |
|
} |
|
return self::$gbSafeCache[$cacheKey]; //return cached value |
|
} |
|
public function addStatus($level, $type, $msg){ |
|
//$msg = '[' . sprintf('%.2f', memory_get_usage(true) / (1024 * 1024)) . '] ' . $msg; |
|
$this->getDB()->queryWrite("insert into " . $this->statusTable . " (ctime, level, type, msg) values (%s, %d, '%s', '%s')", sprintf('%.6f', microtime(true)), $level, $type, $msg); |
|
} |
|
public function getStatusEvents($lastCtime){ |
|
if($lastCtime < 1){ |
|
$lastCtime = $this->getDB()->querySingle("select ctime from " . $this->statusTable . " order by ctime desc limit 1000,1"); |
|
if(! $lastCtime){ |
|
$lastCtime = 0; |
|
} |
|
} |
|
$results = $this->getDB()->querySelect("select ctime, level, type, msg from " . $this->statusTable . " where ctime > %f order by ctime asc", $lastCtime); |
|
$timeOffset = 3600 * get_option('gmt_offset'); |
|
foreach($results as &$rec){ |
|
//$rec['timeAgo'] = wfUtils::makeTimeAgo(time() - $rec['ctime']); |
|
$rec['date'] = date('M d H:i:s', $rec['ctime'] + $timeOffset); |
|
$rec['msg'] = wp_kses_data( (string) $rec['msg']); |
|
} |
|
return $results; |
|
} |
|
public function getSummaryEvents(){ |
|
$results = $this->getDB()->querySelect("select ctime, level, type, msg from " . $this->statusTable . " where level = 10 order by ctime desc limit 100"); |
|
$timeOffset = 3600 * get_option('gmt_offset'); |
|
foreach($results as &$rec){ |
|
$rec['date'] = date('M d H:i:s', $rec['ctime'] + $timeOffset); |
|
if(strpos($rec['msg'], 'SUM_PREP:') === 0){ |
|
break; |
|
} |
|
} |
|
return array_reverse($results); |
|
} |
|
|
|
/** |
|
* @return string |
|
*/ |
|
public function getGooglePattern() { |
|
return $this->googlePattern; |
|
} |
|
|
|
} |
|
|
|
/** |
|
* |
|
*/ |
|
class wfUserIPRange { |
|
|
|
/** |
|
* @var string|null |
|
*/ |
|
private $ip_string; |
|
|
|
/** |
|
* @param string|null $ip_string |
|
*/ |
|
public function __construct($ip_string = null) { |
|
$this->setIPString($ip_string); |
|
} |
|
|
|
/** |
|
* Check if the supplied IP address is within the user supplied range. |
|
* |
|
* @param string $ip |
|
* @return bool |
|
*/ |
|
public function isIPInRange($ip) { |
|
$ip_string = $this->getIPString(); |
|
|
|
if (strpos($ip_string, '/') !== false) { //CIDR range -- 127.0.0.1/24 |
|
return wfUtils::subnetContainsIP($ip_string, $ip); |
|
} |
|
else if (strpos($ip_string, '[') !== false) //Bracketed range -- 127.0.0.[1-100] |
|
{ |
|
// IPv4 range |
|
if (strpos($ip_string, '.') !== false && strpos($ip, '.') !== false) { |
|
// IPv4-mapped-IPv6 |
|
if (preg_match('/:ffff:([^:]+)$/i', $ip_string, $matches)) { |
|
$ip_string = $matches[1]; |
|
} |
|
if (preg_match('/:ffff:([^:]+)$/i', $ip, $matches)) { |
|
$ip = $matches[1]; |
|
} |
|
|
|
// Range check |
|
if (preg_match('/\[\d+\-\d+\]/', $ip_string)) { |
|
$IPparts = explode('.', $ip); |
|
$whiteParts = explode('.', $ip_string); |
|
$mismatch = false; |
|
if (count($whiteParts) != 4 || count($IPparts) != 4) { |
|
return false; |
|
} |
|
|
|
for ($i = 0; $i <= 3; $i++) { |
|
if (preg_match('/^\[(\d+)\-(\d+)\]$/', $whiteParts[$i], $m)) { |
|
if ($IPparts[$i] < $m[1] || $IPparts[$i] > $m[2]) { |
|
$mismatch = true; |
|
} |
|
} |
|
else if ($whiteParts[$i] != $IPparts[$i]) { |
|
$mismatch = true; |
|
} |
|
} |
|
if ($mismatch === false) { |
|
return true; // Is whitelisted because we did not get a mismatch |
|
} |
|
} |
|
else if ($ip_string == $ip) { |
|
return true; |
|
} |
|
|
|
// IPv6 range |
|
} |
|
else if (strpos($ip_string, ':') !== false && strpos($ip, ':') !== false) { |
|
$ip = strtolower(wfUtils::expandIPv6Address($ip)); |
|
$ip_string = strtolower(self::expandIPv6Range($ip_string)); |
|
if (preg_match('/\[[a-f0-9]+\-[a-f0-9]+\]/i', $ip_string)) { |
|
$IPparts = explode(':', $ip); |
|
$whiteParts = explode(':', $ip_string); |
|
$mismatch = false; |
|
if (count($whiteParts) != 8 || count($IPparts) != 8) { |
|
return false; |
|
} |
|
|
|
for ($i = 0; $i <= 7; $i++) { |
|
if (preg_match('/^\[([a-f0-9]+)\-([a-f0-9]+)\]$/i', $whiteParts[$i], $m)) { |
|
$ip_group = hexdec($IPparts[$i]); |
|
$range_group_from = hexdec($m[1]); |
|
$range_group_to = hexdec($m[2]); |
|
if ($ip_group < $range_group_from || $ip_group > $range_group_to) { |
|
$mismatch = true; |
|
break; |
|
} |
|
} |
|
else if ($whiteParts[$i] != $IPparts[$i]) { |
|
$mismatch = true; |
|
break; |
|
} |
|
} |
|
if ($mismatch === false) { |
|
return true; // Is whitelisted because we did not get a mismatch |
|
} |
|
} |
|
else if ($ip_string == $ip) { |
|
return true; |
|
} |
|
} |
|
} |
|
else if (strpos($ip_string, '-') !== false) { //Linear range -- 127.0.0.1 - 127.0.1.100 |
|
list($ip1, $ip2) = explode('-', $ip_string); |
|
$ip1N = wfUtils::inet_pton($ip1); |
|
$ip2N = wfUtils::inet_pton($ip2); |
|
$ipN = wfUtils::inet_pton($ip); |
|
return (strcmp($ip1N, $ipN) <= 0 && strcmp($ip2N, $ipN) >= 0); |
|
} |
|
else { //Treat as a literal IP |
|
$ip1 = @wfUtils::inet_pton($ip_string); |
|
$ip2 = @wfUtils::inet_pton($ip); |
|
if ($ip1 !== false && $ip1 == $ip2) { |
|
return true; |
|
} |
|
} |
|
|
|
return false; |
|
} |
|
|
|
/** |
|
* Expand a compressed printable range representation of an IPv6 address. |
|
* |
|
* @todo Hook up exceptions for better error handling. |
|
* @todo Allow IPv4 mapped IPv6 addresses (::ffff:192.168.1.1). |
|
* @param string $ip_range |
|
* @return string |
|
*/ |
|
public static function expandIPv6Range($ip_range) { |
|
$colon_count = substr_count($ip_range, ':'); |
|
$dbl_colon_count = substr_count($ip_range, '::'); |
|
if ($dbl_colon_count > 1) { |
|
return false; |
|
} |
|
$dbl_colon_pos = strpos($ip_range, '::'); |
|
if ($dbl_colon_pos !== false) { |
|
$ip_range = str_replace('::', str_repeat(':0000', |
|
(($dbl_colon_pos === 0 || $dbl_colon_pos === strlen($ip_range) - 2) ? 9 : 8) - $colon_count) . ':', $ip_range); |
|
$ip_range = trim($ip_range, ':'); |
|
} |
|
$colon_count = substr_count($ip_range, ':'); |
|
if ($colon_count != 7) { |
|
return false; |
|
} |
|
|
|
$groups = explode(':', $ip_range); |
|
$expanded = ''; |
|
foreach ($groups as $group) { |
|
if (preg_match('/\[([a-f0-9]{1,4})\-([a-f0-9]{1,4})\]/i', $group, $matches)) { |
|
$expanded .= sprintf('[%s-%s]', str_pad(strtolower($matches[1]), 4, '0', STR_PAD_LEFT), str_pad(strtolower($matches[2]), 4, '0', STR_PAD_LEFT)) . ':'; |
|
} else if (preg_match('/[a-f0-9]{1,4}/i', $group)) { |
|
$expanded .= str_pad(strtolower($group), 4, '0', STR_PAD_LEFT) . ':'; |
|
} else { |
|
return false; |
|
} |
|
} |
|
return trim($expanded, ':'); |
|
} |
|
|
|
/** |
|
* @return bool |
|
*/ |
|
public function isValidRange() { |
|
return $this->isValidCIDRRange() || $this->isValidBracketedRange() || $this->isValidLinearRange() || wfUtils::isValidIP($this->getIPString()); |
|
} |
|
|
|
public function isValidCIDRRange() { //e.g., 192.0.2.1/24 |
|
$ip_string = $this->getIPString(); |
|
if (preg_match('/[^0-9a-f:\/\.]/i', $ip_string)) { return false; } |
|
return wfUtils::isValidCIDRRange($ip_string); |
|
} |
|
|
|
public function isValidBracketedRange() { //e.g., 192.0.2.[1-10] |
|
$ip_string = $this->getIPString(); |
|
if (preg_match('/[^0-9a-f:\.\[\]\-]/i', $ip_string)) { return false; } |
|
if (strpos($ip_string, '.') !== false) { //IPv4 |
|
if (preg_match_all('/(\d+)/', $ip_string, $matches) > 0) { |
|
foreach ($matches[1] as $match) { |
|
$group = (int) $match; |
|
if ($group > 255 || $group < 0) { |
|
return false; |
|
} |
|
} |
|
} |
|
|
|
$group_regex = '([0-9]{1,3}|\[[0-9]{1,3}\-[0-9]{1,3}\])'; |
|
return preg_match('/^' . str_repeat("{$group_regex}\\.", 3) . $group_regex . '$/i', $ip_string) > 0; |
|
} |
|
|
|
//IPv6 |
|
if (strpos($ip_string, '::') !== false) { |
|
$ip_string = self::expandIPv6Range($ip_string); |
|
} |
|
if (!$ip_string) { |
|
return false; |
|
} |
|
$group_regex = '([a-f0-9]{1,4}|\[[a-f0-9]{1,4}\-[a-f0-9]{1,4}\])'; |
|
return preg_match('/^' . str_repeat("$group_regex:", 7) . $group_regex . '$/i', $ip_string) > 0; |
|
} |
|
|
|
public function isValidLinearRange() { //e.g., 192.0.2.1-192.0.2.100 |
|
$ip_string = $this->getIPString(); |
|
if (preg_match('/[^0-9a-f:\.\-]/i', $ip_string)) { return false; } |
|
list($ip1, $ip2) = explode("-", $ip_string); |
|
$ip1N = @wfUtils::inet_pton($ip1); |
|
$ip2N = @wfUtils::inet_pton($ip2); |
|
|
|
if ($ip1N === false || !wfUtils::isValidIP($ip1) || $ip2N === false || !wfUtils::isValidIP($ip2)) { |
|
return false; |
|
} |
|
|
|
return strcmp($ip1N, $ip2N) <= 0; |
|
} |
|
|
|
public function isMixedRange() { //e.g., 192.0.2.1-2001:db8::ffff |
|
$ip_string = $this->getIPString(); |
|
if (preg_match('/[^0-9a-f:\.\-]/i', $ip_string)) { return false; } |
|
list($ip1, $ip2) = explode("-", $ip_string); |
|
|
|
$ipv4Count = 0; |
|
$ipv4Count += filter_var($ip1, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false ? 1 : 0; |
|
$ipv4Count += filter_var($ip2, FILTER_VALIDATE_IP, FILTER_FLAG_IPV4) !== false ? 1 : 0; |
|
|
|
$ipv6Count = 0; |
|
$ipv6Count += filter_var($ip1, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false ? 1 : 0; |
|
$ipv6Count += filter_var($ip2, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6) !== false ? 1 : 0; |
|
|
|
if ($ipv4Count != 2 && $ipv6Count != 2) { |
|
return true; |
|
} |
|
|
|
return false; |
|
} |
|
|
|
protected function _sanitizeIPRange($ip_string) { |
|
$ip_string = preg_replace('/\s/', '', $ip_string); //Strip whitespace |
|
$ip_string = preg_replace('/[\\x{2013}-\\x{2015}]/u', '-', $ip_string); //Non-hyphen dashes to hyphen |
|
$ip_string = strtolower($ip_string); |
|
|
|
if (preg_match('/^\d+-\d+$/', $ip_string)) { //v5 32 bit int style format |
|
list($start, $end) = explode('-', $ip_string); |
|
$start = long2ip($start); |
|
$end = long2ip($end); |
|
$ip_string = "{$start}-{$end}"; |
|
} |
|
|
|
return $ip_string; |
|
} |
|
|
|
/** |
|
* @return string|null |
|
*/ |
|
public function getIPString() { |
|
return $this->ip_string; |
|
} |
|
|
|
/** |
|
* @param string|null $ip_string |
|
*/ |
|
public function setIPString($ip_string) { |
|
$this->ip_string = $this->_sanitizeIPRange($ip_string); |
|
} |
|
} |
|
|
|
/** |
|
* The function of this class is to detect admin users created via direct access to the database (in other words, not |
|
* through WordPress). |
|
*/ |
|
class wfAdminUserMonitor { |
|
|
|
public function isEnabled() { |
|
$options = wfScanner::shared()->scanOptions(); |
|
$enabled = $options['scansEnabled_suspiciousAdminUsers']; |
|
if ($enabled && is_multisite()) { |
|
if (!function_exists('wp_is_large_network')) { |
|
require_once(ABSPATH . WPINC . '/ms-functions.php'); |
|
} |
|
$enabled = !wp_is_large_network('sites') && !wp_is_large_network('users'); |
|
} |
|
return $enabled; |
|
} |
|
|
|
/** |
|
* |
|
*/ |
|
public function createInitialList() { |
|
$admins = $this->getCurrentAdmins(); |
|
wfConfig::set_ser('adminUserList', $admins); |
|
} |
|
|
|
/** |
|
* @param int $userID |
|
*/ |
|
public function grantSuperAdmin($userID = null) { |
|
if ($userID) { |
|
$this->addAdmin($userID); |
|
} |
|
} |
|
|
|
/** |
|
* @param int $userID |
|
*/ |
|
public function revokeSuperAdmin($userID = null) { |
|
if ($userID) { |
|
$this->removeAdmin($userID); |
|
} |
|
} |
|
|
|
/** |
|
* @param int $ID |
|
* @param mixed $role |
|
* @param mixed $old_roles |
|
*/ |
|
public function updateToUserRole($ID = null, $role = null, $old_roles = null) { |
|
$admins = $this->getLoggedAdmins(); |
|
if ($role !== 'administrator' && array_key_exists($ID, $admins)) { |
|
$this->removeAdmin($ID); |
|
} else if ($role === 'administrator') { |
|
$this->addAdmin($ID); |
|
} |
|
} |
|
|
|
/** |
|
* @return array|bool |
|
*/ |
|
public function checkNewAdmins() { |
|
$loggedAdmins = $this->getLoggedAdmins(); |
|
$admins = $this->getCurrentAdmins(); |
|
$suspiciousAdmins = array(); |
|
foreach ($admins as $adminID => $v) { |
|
if (!array_key_exists($adminID, $loggedAdmins)) { |
|
$suspiciousAdmins[] = $adminID; |
|
} |
|
} |
|
return $suspiciousAdmins ? $suspiciousAdmins : false; |
|
} |
|
|
|
/** |
|
* Checks if the supplied user ID is suspicious. |
|
* |
|
* @param int $userID |
|
* @return bool |
|
*/ |
|
public function isAdminUserLogged($userID) { |
|
$loggedAdmins = $this->getLoggedAdmins(); |
|
return array_key_exists($userID, $loggedAdmins); |
|
} |
|
|
|
/** |
|
* @return array |
|
*/ |
|
public function getCurrentAdmins() { |
|
require_once(ABSPATH . WPINC . '/user.php'); |
|
if (is_multisite()) { |
|
if (function_exists("get_sites")) { |
|
$sites = get_sites(array( |
|
'network_id' => null, |
|
)); |
|
} |
|
else { |
|
$sites = wp_get_sites(array( |
|
'network_id' => null, |
|
)); |
|
} |
|
} else { |
|
$sites = array(array( |
|
'blog_id' => get_current_blog_id(), |
|
)); |
|
} |
|
|
|
// not very efficient, but the WordPress API doesn't provide a good way to do this. |
|
$admins = array(); |
|
foreach ($sites as $siteRow) { |
|
$siteRowArray = (array) $siteRow; |
|
$user_query = new WP_User_Query(array( |
|
'blog_id' => $siteRowArray['blog_id'], |
|
'role' => 'administrator', |
|
)); |
|
$users = $user_query->get_results(); |
|
if (is_array($users)) { |
|
/** @var WP_User $user */ |
|
foreach ($users as $user) { |
|
$admins[$user->ID] = 1; |
|
} |
|
} |
|
} |
|
|
|
// Add any super admins that aren't also admins on a network |
|
$superAdmins = get_super_admins(); |
|
foreach ($superAdmins as $userLogin) { |
|
$user = get_user_by('login', $userLogin); |
|
if ($user) { |
|
$admins[$user->ID] = 1; |
|
} |
|
} |
|
return $admins; |
|
} |
|
|
|
public function getLoggedAdmins() { |
|
$loggedAdmins = wfConfig::get_ser('adminUserList', false); |
|
if (!is_array($loggedAdmins)) { |
|
$this->createInitialList(); |
|
$loggedAdmins = wfConfig::get_ser('adminUserList', false); |
|
} |
|
if (!is_array($loggedAdmins)) { |
|
$loggedAdmins = array(); |
|
} |
|
return $loggedAdmins; |
|
} |
|
|
|
/** |
|
* @param int $userID |
|
*/ |
|
public function addAdmin($userID) { |
|
$loggedAdmins = $this->getLoggedAdmins(); |
|
if (!array_key_exists($userID, $loggedAdmins)) { |
|
$loggedAdmins[$userID] = 1; |
|
wfConfig::set_ser('adminUserList', $loggedAdmins); |
|
} |
|
} |
|
|
|
/** |
|
* @param int $userID |
|
*/ |
|
public function removeAdmin($userID) { |
|
$loggedAdmins = $this->getLoggedAdmins(); |
|
if (array_key_exists($userID, $loggedAdmins) && !array_key_exists($userID, $this->getCurrentAdmins())) { |
|
unset($loggedAdmins[$userID]); |
|
wfConfig::set_ser('adminUserList', $loggedAdmins); |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Represents a request record |
|
* |
|
* @property int $id |
|
* @property float $attackLogTime |
|
* @property float $ctime |
|
* @property string $IP |
|
* @property bool $jsRun |
|
* @property int $statusCode |
|
* @property bool $isGoogle |
|
* @property int $userID |
|
* @property string $URL |
|
* @property string $referer |
|
* @property string $UA |
|
* @property string $action |
|
* @property string $actionDescription |
|
* @property string $actionData |
|
*/ |
|
class wfRequestModel extends wfModel { |
|
|
|
private static $actionDataEncodedParams = array( |
|
'paramKey', |
|
'paramValue', |
|
'path', |
|
); |
|
|
|
/** |
|
* @param $actionData |
|
* @return mixed|string|void |
|
*/ |
|
public static function serializeActionData($actionData) { |
|
if (is_array($actionData)) { |
|
foreach (self::$actionDataEncodedParams as $key) { |
|
if (array_key_exists($key, $actionData)) { |
|
$actionData[$key] = base64_encode($actionData[$key]); |
|
} |
|
} |
|
} |
|
return json_encode($actionData); |
|
} |
|
|
|
/** |
|
* @param $actionDataJSON |
|
* @return mixed|string|void |
|
*/ |
|
public static function unserializeActionData($actionDataJSON) { |
|
$actionData = json_decode($actionDataJSON, true); |
|
if (is_array($actionData)) { |
|
foreach (self::$actionDataEncodedParams as $key) { |
|
if (array_key_exists($key, $actionData)) { |
|
$actionData[$key] = base64_decode($actionData[$key]); |
|
} |
|
} |
|
} |
|
return $actionData; |
|
} |
|
|
|
private $columns = array( |
|
'id', |
|
'attackLogTime', |
|
'ctime', |
|
'IP', |
|
'jsRun', |
|
'statusCode', |
|
'isGoogle', |
|
'userID', |
|
'URL', |
|
'referer', |
|
'UA', |
|
'action', |
|
'actionDescription', |
|
'actionData', |
|
); |
|
|
|
public function getIDColumn() { |
|
return 'id'; |
|
} |
|
|
|
public function getTable() { |
|
return wfDB::networkTable('wfHits'); |
|
} |
|
|
|
public function hasColumn($column) { |
|
return in_array($column, $this->columns); |
|
} |
|
|
|
public function save() { |
|
$sapi = @php_sapi_name(); |
|
if ($sapi == "cli") { |
|
return false; |
|
} |
|
|
|
return parent::save(); |
|
} |
|
} |
|
|
|
|
|
class wfLiveTrafficQuery { |
|
|
|
protected $validParams = array( |
|
'id' => 'h.id', |
|
'ctime' => 'h.ctime', |
|
'ip' => 'h.ip', |
|
'jsrun' => 'h.jsrun', |
|
'statuscode' => 'h.statuscode', |
|
'isgoogle' => 'h.isgoogle', |
|
'userid' => 'h.userid', |
|
'url' => 'h.url', |
|
'referer' => 'h.referer', |
|
'ua' => 'h.ua', |
|
'action' => 'h.action', |
|
'actiondescription' => 'h.actiondescription', |
|
'actiondata' => 'h.actiondata', |
|
|
|
// wfLogins |
|
'user_login' => 'u.user_login', |
|
'username' => 'l.username', |
|
); |
|
|
|
/** @var wfLiveTrafficQueryFilterCollection */ |
|
private $filters = array(); |
|
|
|
/** @var wfLiveTrafficQueryGroupBy */ |
|
private $groupBy; |
|
/** |
|
* @var float|null |
|
*/ |
|
private $startDate; |
|
/** |
|
* @var float|null |
|
*/ |
|
private $endDate; |
|
/** |
|
* @var int |
|
*/ |
|
private $limit; |
|
/** |
|
* @var int |
|
*/ |
|
private $offset; |
|
|
|
private $tableName; |
|
|
|
/** @var wfLog */ |
|
private $wfLog; |
|
|
|
/** |
|
* wfLiveTrafficQuery constructor. |
|
* |
|
* @param wfLog $wfLog |
|
* @param wfLiveTrafficQueryFilterCollection $filters |
|
* @param wfLiveTrafficQueryGroupBy $groupBy |
|
* @param float $startDate |
|
* @param float $endDate |
|
* @param int $limit |
|
* @param int $offset |
|
*/ |
|
public function __construct($wfLog, $filters = null, $groupBy = null, $startDate = null, $endDate = null, $limit = 20, $offset = 0) { |
|
$this->wfLog = $wfLog; |
|
$this->filters = $filters; |
|
$this->groupBy = $groupBy; |
|
$this->startDate = $startDate; |
|
$this->endDate = $endDate; |
|
$this->limit = $limit; |
|
$this->offset = $offset; |
|
} |
|
|
|
/** |
|
* @return array|null|object |
|
*/ |
|
public function execute() { |
|
global $wpdb; |
|
$delayedHumanBotFiltering = false; |
|
$humanOnly = false; |
|
$sql = $this->buildQuery($delayedHumanBotFiltering, $humanOnly); |
|
$results = $wpdb->get_results($sql, ARRAY_A); |
|
|
|
if ($delayedHumanBotFiltering) { |
|
$browscap = wfBrowscap::shared(); |
|
foreach ($results as $index => $res) { |
|
if ($res['UA']) { |
|
$b = $browscap->getBrowser($res['UA']); |
|
$jsRun = wfUtils::truthyToBoolean($res['jsRun']); |
|
if ($b && $b['Parent'] != 'DefaultProperties') { |
|
$jsRun = wfUtils::truthyToBoolean($res['jsRun']); |
|
if (!wfConfig::liveTrafficEnabled() && !$jsRun) { |
|
$jsRun = !(isset($b['Crawler']) && $b['Crawler']); |
|
} |
|
} |
|
|
|
if (!$humanOnly && $jsRun || $humanOnly && !$jsRun) { |
|
unset($results[$index]); |
|
} |
|
} |
|
} |
|
} |
|
|
|
$this->getWFLog()->processGetHitsResults('', $results); |
|
|
|
$verifyCrawlers = false; |
|
if ($this->filters !== null && count($this->filters->getFilters()) > 0) { |
|
$filters = $this->filters->getFilters(); |
|
foreach ($filters as $f) { |
|
if (strtolower($f->getParam()) == "isgoogle") { |
|
$verifyCrawlers = true; |
|
break; |
|
} |
|
} |
|
} |
|
|
|
foreach ($results as $key => &$row) { |
|
if ($row['isGoogle'] && $verifyCrawlers) { |
|
if (!wfCrawl::isVerifiedGoogleCrawler($row['IP'], $row['UA'])) { |
|
unset($results[$key]); //foreach copies $results and iterates on the copy, so it is safe to mutate $results within the loop |
|
continue; |
|
} |
|
} |
|
|
|
$row['actionData'] = (array) json_decode($row['actionData'], true); |
|
} |
|
return array_values($results); |
|
} |
|
|
|
/** |
|
* @param mixed $delayedHumanBotFiltering Whether or not human/bot filtering should be applied in PHP rather than SQL. |
|
* @param mixed $humanOnly When using delayed filtering, whether to show only humans or only bots. |
|
* |
|
* @return string |
|
* @throws wfLiveTrafficQueryException |
|
*/ |
|
public function buildQuery(&$delayedHumanBotFiltering = null, &$humanOnly) { |
|
global $wpdb; |
|
$filters = $this->getFilters(); |
|
$groupBy = $this->getGroupBy(); |
|
$startDate = $this->getStartDate(); |
|
$endDate = $this->getEndDate(); |
|
$limit = absint($this->getLimit()); |
|
$offset = absint($this->getOffset()); |
|
|
|
$wheres = array("h.action != 'logged:waf'", "h.action != 'scan:detectproxy'"); |
|
if ($startDate) { |
|
$wheres[] = $wpdb->prepare('h.ctime > %f', $startDate); |
|
} |
|
if ($endDate) { |
|
$wheres[] = $wpdb->prepare('h.ctime < %f', $endDate); |
|
} |
|
|
|
if ($filters instanceof wfLiveTrafficQueryFilterCollection) { |
|
if (!wfConfig::liveTrafficEnabled()) { |
|
$individualFilters = $filters->getFilters(); |
|
foreach ($individualFilters as $index => $f) { |
|
if ($f->getParam() == 'jsRun' && $delayedHumanBotFiltering !== null && $humanOnly !== null) { |
|
$humanOnly = wfUtils::truthyToBoolean($f->getValue()); |
|
if ($f->getOperator() == '!=') { |
|
$humanOnly = !$humanOnly; |
|
} |
|
$delayedHumanBotFiltering = true; |
|
unset($individualFilters[$index]); |
|
} |
|
} |
|
$filters->setFilters($individualFilters); |
|
} |
|
|
|
$filtersSQL = $filters->toSQL(); |
|
if ($filtersSQL) { |
|
$wheres[] = $filtersSQL; |
|
} |
|
} |
|
|
|
$orderBy = 'ORDER BY h.ctime DESC'; |
|
$select = ', l.username'; |
|
$groupBySQL = ''; |
|
if ($groupBy && $groupBy->validate()) { |
|
$groupBySQL = "GROUP BY {$groupBy->getParam()}"; |
|
$orderBy = 'ORDER BY hitCount DESC'; |
|
$select .= ', COUNT(h.id) as hitCount, MAX(h.ctime) AS lastHit, u.user_login AS username'; |
|
|
|
if ($groupBy->getParam() == 'user_login') { |
|
$wheres[] = 'user_login IS NOT NULL'; |
|
} |
|
else if ($groupBy->getParam() == 'action') { |
|
$wheres[] = '(statusCode = 403 OR statusCode = 503)'; |
|
} |
|
} |
|
|
|
$where = join(' AND ', $wheres); |
|
if ($where) { |
|
$where = 'WHERE ' . $where; |
|
} |
|
if (!$limit || $limit > 1000) { |
|
$limit = 20; |
|
} |
|
$limitSQL = $wpdb->prepare('LIMIT %d, %d', $offset, $limit); |
|
|
|
$table_wfLogins = wfDB::networkTable('wfLogins'); |
|
$sql = <<<SQL |
|
SELECT h.*, u.display_name{$select} FROM {$this->getTableName()} h |
|
LEFT JOIN {$wpdb->users} u on h.userID = u.ID |
|
LEFT JOIN {$table_wfLogins} l on h.id = l.hitID |
|
$where |
|
$groupBySQL |
|
$orderBy |
|
$limitSQL |
|
SQL; |
|
|
|
return $sql; |
|
} |
|
|
|
/** |
|
* @param $param |
|
* @return bool |
|
*/ |
|
public function isValidParam($param) { |
|
return array_key_exists(strtolower($param), $this->validParams); |
|
} |
|
|
|
/** |
|
* @param $getParam |
|
* @return bool|string |
|
*/ |
|
public function getColumnFromParam($getParam) { |
|
$getParam = strtolower($getParam); |
|
if (array_key_exists($getParam, $this->validParams)) { |
|
return $this->validParams[$getParam]; |
|
} |
|
return false; |
|
} |
|
|
|
/** |
|
* @return wfLiveTrafficQueryFilterCollection |
|
*/ |
|
public function getFilters() { |
|
return $this->filters; |
|
} |
|
|
|
/** |
|
* @param wfLiveTrafficQueryFilterCollection $filters |
|
*/ |
|
public function setFilters($filters) { |
|
$this->filters = $filters; |
|
} |
|
|
|
/** |
|
* @return float|null |
|
*/ |
|
public function getStartDate() { |
|
return $this->startDate; |
|
} |
|
|
|
/** |
|
* @param float|null $startDate |
|
*/ |
|
public function setStartDate($startDate) { |
|
$this->startDate = $startDate; |
|
} |
|
|
|
/** |
|
* @return float|null |
|
*/ |
|
public function getEndDate() { |
|
return $this->endDate; |
|
} |
|
|
|
/** |
|
* @param float|null $endDate |
|
*/ |
|
public function setEndDate($endDate) { |
|
$this->endDate = $endDate; |
|
} |
|
|
|
/** |
|
* @return wfLiveTrafficQueryGroupBy |
|
*/ |
|
public function getGroupBy() { |
|
return $this->groupBy; |
|
} |
|
|
|
/** |
|
* @param wfLiveTrafficQueryGroupBy $groupBy |
|
*/ |
|
public function setGroupBy($groupBy) { |
|
$this->groupBy = $groupBy; |
|
} |
|
|
|
/** |
|
* @return int |
|
*/ |
|
public function getLimit() { |
|
return $this->limit; |
|
} |
|
|
|
/** |
|
* @param int $limit |
|
*/ |
|
public function setLimit($limit) { |
|
$this->limit = $limit; |
|
} |
|
|
|
/** |
|
* @return int |
|
*/ |
|
public function getOffset() { |
|
return $this->offset; |
|
} |
|
|
|
/** |
|
* @param int $offset |
|
*/ |
|
public function setOffset($offset) { |
|
$this->offset = $offset; |
|
} |
|
|
|
/** |
|
* @return string |
|
*/ |
|
public function getTableName() { |
|
if ($this->tableName === null) { |
|
$this->tableName = wfDB::networkTable('wfHits'); |
|
} |
|
return $this->tableName; |
|
} |
|
|
|
/** |
|
* @param string $tableName |
|
*/ |
|
public function setTableName($tableName) { |
|
$this->tableName = $tableName; |
|
} |
|
|
|
/** |
|
* @return wfLog |
|
*/ |
|
public function getWFLog() { |
|
return $this->wfLog; |
|
} |
|
|
|
/** |
|
* @param wfLog $wfLog |
|
*/ |
|
public function setWFLog($wfLog) { |
|
$this->wfLog = $wfLog; |
|
} |
|
} |
|
|
|
class wfLiveTrafficQueryFilterCollection { |
|
|
|
private $filters = array(); |
|
|
|
/** |
|
* wfLiveTrafficQueryFilterCollection constructor. |
|
* |
|
* @param array $filters |
|
*/ |
|
public function __construct($filters = array()) { |
|
$this->filters = $filters; |
|
} |
|
|
|
public function toSQL() { |
|
$params = array(); |
|
$sql = ''; |
|
$filters = $this->getFilters(); |
|
if ($filters) { |
|
/** @var wfLiveTrafficQueryFilter $filter */ |
|
foreach ($filters as $filter) { |
|
$params[$filter->getParam()][] = $filter; |
|
} |
|
} |
|
|
|
foreach ($params as $param => $filters) { |
|
// $sql .= '('; |
|
$filtersSQL = ''; |
|
foreach ($filters as $filter) { |
|
$filterSQL = $filter->toSQL(); |
|
if ($filterSQL) { |
|
$filtersSQL .= $filterSQL . ' OR '; |
|
} |
|
} |
|
if ($filtersSQL) { |
|
$sql .= '(' . substr($filtersSQL, 0, -4) . ') AND '; |
|
} |
|
} |
|
if ($sql) { |
|
$sql = substr($sql, 0, -5); |
|
} |
|
return $sql; |
|
} |
|
|
|
public function addFilter($filter) { |
|
$this->filters[] = $filter; |
|
} |
|
|
|
/** |
|
* @return array |
|
*/ |
|
public function getFilters() { |
|
return $this->filters; |
|
} |
|
|
|
/** |
|
* @param array $filters |
|
*/ |
|
public function setFilters($filters) { |
|
$this->filters = $filters; |
|
} |
|
} |
|
|
|
class wfLiveTrafficQueryFilter { |
|
|
|
private $param; |
|
private $operator; |
|
private $value; |
|
|
|
protected $validOperators = array( |
|
'=', |
|
'!=', |
|
'contains', |
|
'match', |
|
'hregexp', |
|
'hnotregexp', |
|
); |
|
|
|
/** |
|
* @var wfLiveTrafficQuery |
|
*/ |
|
private $query; |
|
|
|
/** |
|
* wfLiveTrafficQueryFilter constructor. |
|
* |
|
* @param wfLiveTrafficQuery $query |
|
* @param string $param |
|
* @param string $operator |
|
* @param string $value |
|
*/ |
|
public function __construct($query, $param, $operator, $value) { |
|
$this->query = $query; |
|
$this->param = $param; |
|
$this->operator = $operator; |
|
$this->value = $value; |
|
} |
|
|
|
/** |
|
* @return string|void |
|
*/ |
|
public function toSQL() { |
|
$sql = ''; |
|
if ($this->validate()) { |
|
/** @var wpdb $wpdb */ |
|
global $wpdb; |
|
$operator = $this->getOperator(); |
|
$param = $this->getQuery()->getColumnFromParam($this->getParam()); |
|
if (!$param) { |
|
return $sql; |
|
} |
|
$value = $this->getValue(); |
|
switch ($operator) { |
|
case 'contains': |
|
$like = addcslashes($value, '_%\\'); |
|
$sql = $wpdb->prepare("$param LIKE %s", "%$like%"); |
|
break; |
|
|
|
case 'match': |
|
$sql = $wpdb->prepare("$param LIKE %s", $value); |
|
break; |
|
|
|
case 'hregexp': |
|
$sql = $wpdb->prepare("HEX($param) REGEXP %s", $value); |
|
break; |
|
|
|
case 'hnotregexp': |
|
$sql = $wpdb->prepare("HEX($param) NOT REGEXP %s", $value); |
|
break; |
|
|
|
default: |
|
$sql = $wpdb->prepare("$param $operator %s", $value); |
|
break; |
|
} |
|
} |
|
return $sql; |
|
} |
|
|
|
/** |
|
* @return bool |
|
*/ |
|
public function validate() { |
|
$valid = $this->isValidParam($this->getParam()) && $this->isValidOperator($this->getOperator()); |
|
if (defined('WP_DEBUG') && WP_DEBUG) { |
|
if (!$valid) { |
|
throw new wfLiveTrafficQueryException("Invalid param/operator [{$this->getParam()}]/[{$this->getOperator()}] passed to " . get_class($this)); |
|
} |
|
return true; |
|
} |
|
return $valid; |
|
} |
|
|
|
/** |
|
* @param string $param |
|
* @return bool |
|
*/ |
|
public function isValidParam($param) { |
|
return $this->getQuery() && $this->getQuery()->isValidParam($param); |
|
} |
|
|
|
/** |
|
* @param string $operator |
|
* @return bool |
|
*/ |
|
public function isValidOperator($operator) { |
|
return in_array($operator, $this->validOperators); |
|
} |
|
|
|
/** |
|
* @return mixed |
|
*/ |
|
public function getParam() { |
|
return $this->param; |
|
} |
|
|
|
/** |
|
* @param mixed $param |
|
*/ |
|
public function setParam($param) { |
|
$this->param = $param; |
|
} |
|
|
|
/** |
|
* @return mixed |
|
*/ |
|
public function getOperator() { |
|
return $this->operator; |
|
} |
|
|
|
/** |
|
* @param mixed $operator |
|
*/ |
|
public function setOperator($operator) { |
|
$this->operator = $operator; |
|
} |
|
|
|
/** |
|
* @return mixed |
|
*/ |
|
public function getValue() { |
|
return $this->value; |
|
} |
|
|
|
/** |
|
* @param mixed $value |
|
*/ |
|
public function setValue($value) { |
|
$this->value = $value; |
|
} |
|
|
|
/** |
|
* @return wfLiveTrafficQuery |
|
*/ |
|
public function getQuery() { |
|
return $this->query; |
|
} |
|
|
|
/** |
|
* @param wfLiveTrafficQuery $query |
|
*/ |
|
public function setQuery($query) { |
|
$this->query = $query; |
|
} |
|
} |
|
|
|
class wfLiveTrafficQueryGroupBy { |
|
|
|
private $param; |
|
|
|
/** |
|
* @var wfLiveTrafficQuery |
|
*/ |
|
private $query; |
|
|
|
/** |
|
* wfLiveTrafficQueryGroupBy constructor. |
|
* |
|
* @param wfLiveTrafficQuery $query |
|
* @param string $param |
|
*/ |
|
public function __construct($query, $param) { |
|
$this->query = $query; |
|
$this->param = $param; |
|
} |
|
|
|
/** |
|
* @return bool |
|
* @throws wfLiveTrafficQueryException |
|
*/ |
|
public function validate() { |
|
$valid = $this->isValidParam($this->getParam()); |
|
if (defined('WP_DEBUG') && WP_DEBUG) { |
|
if (!$valid) { |
|
throw new wfLiveTrafficQueryException("Invalid param [{$this->getParam()}] passed to " . get_class($this)); |
|
} |
|
return true; |
|
} |
|
return $valid; |
|
} |
|
|
|
/** |
|
* @param string $param |
|
* @return bool |
|
*/ |
|
public function isValidParam($param) { |
|
return $this->getQuery() && $this->getQuery()->isValidParam($param); |
|
} |
|
|
|
/** |
|
* @return wfLiveTrafficQuery |
|
*/ |
|
public function getQuery() { |
|
return $this->query; |
|
} |
|
|
|
/** |
|
* @param wfLiveTrafficQuery $query |
|
*/ |
|
public function setQuery($query) { |
|
$this->query = $query; |
|
} |
|
|
|
/** |
|
* @return mixed |
|
*/ |
|
public function getParam() { |
|
return $this->param; |
|
} |
|
|
|
/** |
|
* @param mixed $param |
|
*/ |
|
public function setParam($param) { |
|
$this->param = $param; |
|
} |
|
|
|
} |
|
|
|
|
|
class wfLiveTrafficQueryException extends Exception { |
|
|
|
} |
|
|
|
class wfErrorLogHandler { |
|
public static function getErrorLogs($deepSearch = false) { |
|
static $errorLogs = null; |
|
|
|
if ($errorLogs === null) { |
|
$searchPaths = array(ABSPATH, ABSPATH . 'wp-admin', ABSPATH . 'wp-content'); |
|
|
|
$homePath = get_home_path(); |
|
if (!in_array($homePath, $searchPaths)) { |
|
$searchPaths[] = $homePath; |
|
} |
|
|
|
$errorLogPath = ini_get('error_log'); |
|
if (!empty($errorLogPath) && !in_array($errorLogPath, $searchPaths)) { |
|
$searchPaths[] = $errorLogPath; |
|
} |
|
|
|
$errorLogs = array(); |
|
foreach ($searchPaths as $s) { |
|
$errorLogs = array_merge($errorLogs, self::_scanForLogs($s, $deepSearch)); |
|
} |
|
} |
|
return $errorLogs; |
|
} |
|
|
|
private static function _scanForLogs($path, $deepSearch = false) { |
|
static $processedFolders = array(); //Protection for endless loops caused by symlinks |
|
if (is_file($path)) { |
|
$file = basename($path); |
|
if (preg_match('#(?:error_log(\-\d+)?$|\.log$)#i', $file)) { |
|
return array($path => is_readable($path)); |
|
} |
|
return array(); |
|
} |
|
|
|
$path = untrailingslashit($path); |
|
$contents = @scandir($path); |
|
if (!is_array($contents)) { |
|
return array(); |
|
} |
|
|
|
$processedFolders[$path] = true; |
|
$errorLogs = array(); |
|
foreach ($contents as $name) { |
|
if ($name == '.' || $name == '..') { continue; } |
|
$testPath = $path . DIRECTORY_SEPARATOR . $name; |
|
if (!array_key_exists($testPath, $processedFolders)) { |
|
if ((is_dir($testPath) && $deepSearch) || !is_dir($testPath)) { |
|
$errorLogs = array_merge($errorLogs, self::_scanForLogs($testPath, $deepSearch)); |
|
} |
|
} |
|
} |
|
return $errorLogs; |
|
} |
|
|
|
public static function outputErrorLog($path) { |
|
$errorLogs = self::getErrorLogs(); |
|
if (!isset($errorLogs[$path])) { //Only allow error logs we've identified |
|
status_header(404); |
|
nocache_headers(); |
|
|
|
$template = get_404_template(); |
|
if ($template && file_exists($template)) { |
|
include($template); |
|
} |
|
exit; |
|
} |
|
|
|
$fh = @fopen($path, 'r'); |
|
if (!$fh) { |
|
status_header(503); |
|
nocache_headers(); |
|
echo "503 Service Unavailable"; |
|
exit; |
|
} |
|
|
|
$headersOutputted = false; |
|
while (!feof($fh)) { |
|
$data = fread($fh, 1 * 1024 * 1024); //read 1 megs max per chunk |
|
if ($data === false) { //Handle the error where the file was reported readable but we can't actually read it |
|
status_header(503); |
|
nocache_headers(); |
|
echo "503 Service Unavailable"; |
|
exit; |
|
} |
|
|
|
if (!$headersOutputted) { |
|
header('Content-Type: text/plain'); |
|
header('Content-Disposition: attachment; filename="' . basename($path)); |
|
$headersOutputted = true; |
|
} |
|
echo $data; |
|
} |
|
exit; |
|
} |
|
} |