e107_handlers/iphandler_class.php
<?php
/*
* e107 website system
*
* Copyright 2008-2013 e107 Inc (e107.org)
* Released under the terms and conditions of the
* GNU General Public License (http://www.gnu.org/licenses/gpl.txt)
*
* IP Address related routines, including banning-related code
*
* $URL$
* $Revision$
* $Id$
*
*/
/**
* @package e107
* @subpackage e107_handlers
* @version $Id$;
*
* Routines to manage IP addresses and banning.
*/
/**
* Class to handle ban-related checks, and provide some utility functions related to IP addresses
* There are two parts to the class:
*
* Part 1
* ------
* This part intentionally does NO database access, and requires an absolute minimum of file paths to be set up
* (this is to minimise processing load in the event of an access from a banned IP address)
* It works only with the user's IP address, and potentially browser 'signature'
* The objective of this part is to do only those things which can be done without the database open, and without complicating things later on
* (If DB access is required to handle a ban, it should only need to be done occasionally)
*
* Part 2
* ------
* This part handles those functions which require DB access.
* The intention is that Part 1 will catch most existing bans, to reduce the incidence of abortive DB opens
* If part 1 signals that a ban has expired, part 2 removes it from the database
*
* Elsewhere
* ---------
* if ban retriggering is enabled, cron task needs to scan the ban log periodically to update the expiry times. (Can't do on every access, since it would
* eliminate the benefits of this handler - a DB access would be needed on every access from a banned IP address).
* @todo Implement the ban retriggering cron job (elsewhere)
* - do we have a separate text file for the accesses in need of retriggering? Could then delete it once actioned; keeps it small
* @todo Implement flood bans - needs db access - maybe leave to the second part of this file or the online handler
*
* All IP addresses are stored in 'normal' form - a fixed length IPV6 format with separator colons.
*
* To use:
* include this file, early on (before DB accesses started), and instantiate class ipHandler.
*
*/
class eIPHandler
{
/**
* IPV6 string for localhost - as stored in DB
*/
// const LOCALHOST_IP = '0000:0000:0000:0000:0000:ffff:7f00:0001';
const BAN_REASON_COUNT = 7; // Update as more ban reasons added (max 10 supported)
const BAN_TYPE_LEGACY = 0; // Shouldn't get these unless update process not run
const BAN_TYPE_MANUAL = -1; /// Manually entered bans
const BAN_TYPE_FLOOD = -2; /// Flood ban
const BAN_TYPE_HITS = -3;
const BAN_TYPE_LOGINS = -4;
const BAN_TYPE_IMPORTED = -5; /// Imported bans
const BAN_TYPE_USER = -6; /// User is banned
// Spare value
const BAN_TYPE_UNKNOWN = -8;
const BAN_TYPE_TEMPORARY = -9; /// Used during CSV import - giving it this value highlights problems
const BAN_TYPE_WHITELIST = 100; /// Entry for whitelist - actually not a ban at all! Keep at this value for BC
const BAN_FILE_DIRECTORY = 'cache/'; /// Directory containing the text files (within e_SYSTEM)
const BAN_LOG_DIRECTORY = 'logs/'; /// Directory containing the log file (within e_SYSTEM)
const BAN_FILE_LOG_NAME = 'banlog.log'; /// Logs bans etc
// Note for the following file names - the code appends the extension
const BAN_FILE_IP_NAME = 'banlist'; /// Saves list of banned and whitelisted IP addresses
const BAN_FILE_ACTION_NAME = 'banactions'; /// Details of actions for different ban types
const BAN_FILE_HTACCESS = 'banhtaccess'; /// File in format for direct paste into .htaccess
const BAN_FILE_CSV_NAME = 'banlistcsv'; /// Output file in CSV format
const BAN_FILE_RETRIGGER_NAME = 'banretrigger'; /// Any bans needing retriggering
const BAN_FILE_EXTENSION = '.php'; /// File extension to use
/**
* IP address of current user, in 'normal' form
*/
private $ourIP = '';
private $serverIP = '';
private $debug = false;
/**
* Host name of current user
* Initialised when requested
*/
private $_host_name_cache = array();
/**
* Token for current user, calculated from browser settings.
* Supplements IP address (Can be spoofed, but helps differentiate among honest users at the same IP address)
*/
private $accessID = '';
/**
* Path to directory containing current config file(s)
*/
private $ourConfigDir = '';
/**
* Current user's IP address status. Usually zero (neutral); may be one of the BAN_TYPE_xxx constants
*/
private $ipAddressStatus = 0;
/**
* Flag set to the IP address that triggered the match, if current IP has an expired ban to clear
*/
private $clearBan = FALSE;
/**
* IP Address from ban list file which matched (may have wildcards)
*/
private $matchAddress = '';
/**
* Number of entries read from banlist/whitelist
*/
private $actionCount = 0;
/**
* Constructor
*
* Only one instance of this class is ever loaded, very early on in the initialisation sequence
*
* @param string $configDir Path to the directory containing the files used by this class
* If not set, defaults to BAN_FILE_DIRECTORY constant
*
* On load it gets the user's IP address, and checks it against whitelist and blacklist files
* If the address is blacklisted, displays an appropriate message (as configured) and aborts
* Otherwise sets up
*/
public function __construct($configDir = '')
{
$configDir = trim((string) $configDir);
if ($configDir)
{
$this->ourConfigDir = realpath($configDir);
}
else
{
$this->ourConfigDir = e_SYSTEM.eIPHandler::BAN_FILE_DIRECTORY;
}
$this->ourIP = $this->ipEncode($this->getCurrentIP());
$this->serverIP = $this->ipEncode(isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : 'x.x.x.x');
$this->makeUserToken();
$ipStatus = $this->checkIP($this->ourIP);
if ($ipStatus != 0)
{
if ($ipStatus < 0)
{ // Blacklisted
$this->logBanItem($ipStatus, 'result --> '.$ipStatus); // only log blacklist
$this->banAction($ipStatus); // This will abort if appropriate
}
//elseif ($ipStatus > 0)
// { // Whitelisted - we may want to set a specific indicator
// }
}
// Continue here - user not banned (so far)
}
/**
* @param $ip
* @return void
*/
public function setIP($ip)
{
$this->ourIP = $this->ipEncode($ip);
}
/**
* @param $value
* @return void
*/
public function debug($value)
{
$this->debug = $value === true;
}
/**
* Add an entry to the banlist log file (which is a simple text file)
* A date/time string is prepended to the line
*
* @param int $reason - numeric reason code, usually in range -10..+10
* @param string $message - additional text as required (length not checked, but should be less than 100 characters or so
*
* @return void
*/
private function logBanItem($reason, $message)
{
if ($tmp = fopen(e_SYSTEM.eIPHandler::BAN_LOG_DIRECTORY.eIPHandler::BAN_FILE_LOG_NAME, 'a'))
{
$logLine = time().' '.$this->ourIP.' '.$reason.' '.$message."\n";
fwrite($tmp,$logLine);
fclose($tmp);
}
}
/**
* Generate relatively unique user token from browser info
* (but don't believe that the browser info is accurate - can readily be spoofed)
*
* This supplements use of the IP address in some places; both to improve user identification, and to help deal with dynamic IP allocations
*
* May be replaced by a 'global' e107 token at some point
*/
private function makeUserToken()
{
$tmpStr = '';
foreach (array('HTTP_USER_AGENT', 'HTTP_ACCEPT', 'HTTP_ACCEPT_CHARSET', 'HTTP_ACCEPT_LANGUAGE', 'HTTP_ACCEPT_ENCODING') as $v)
{
if (isset($_SERVER[$v]))
{
$tmpStr .= $_SERVER[$v];
}
else
{
$tmpStr .= 'dummy'.$v;
}
}
$this->accessID = md5($tmpStr);
}
/**
* Return browser-characteristics token
*/
public function getUserToken()
{
return $this->accessID; // Should always be defined at this point
}
/**
* Check whether an IP address is routable
*
* @param string $ip - IPV4 or IPV6 numeric address.
*
* @return boolean TRUE if routable, FALSE if not
@todo handle IPV6 fully
*/
public function isAddressRoutable($ip)
{
$ignore = array(
'0\..*' , '^127\..*' , // Local loopbacks
'192\.168\..*' , // RFC1918 - Private Network
'172\.(?:1[6789]|2\d|3[01])\..*' , // RFC1918 - Private network
'10\..*' , // RFC1918 - Private Network
'169\.254\..*' , // RFC3330 - Link-local, auto-DHCP
'2(?:2[456789]|[345][0-9])\..*' // Single check for Class D and Class E
);
$pattern = '#^('.implode('|',$ignore).')#';
if(preg_match($pattern,$ip))
{
return false;
}
/* XXX preg_match doesn't accept arrays.
if (preg_match(array(
'#^0\..*#' , '#^127\..*#' , // Local loopbacks
'#^192\.168\..*#' , // RFC1918 - Private Network
'#^172\.(?:1[6789]|2\d|3[01])\..*#' , // RFC1918 - Private network
'#^10\..*#' , // RFC1918 - Private Network
'#^169\.254\..*#' , // RFC3330 - Link-local, auto-DHCP
'#^2(?:2[456789]|[345][0-9])\..*#' // Single check for Class D and Class E
), $ip))
{
return FALSE;
}
*/
if (strpos(':', $ip) === FALSE) return TRUE;
// Must be an IPV6 address here
// @todo need to handle IPV4 addresses in IPV6 format
$ip = strtolower($ip);
if ($ip == 'ff02::1') return FALSE; // link-local all nodes multicast group
if ($ip == 'ff02:0000:0000:0000:0000:0000:0000:0001') return FALSE;
if ($ip == '::1') return FALSE; // localhost
if ($ip == '0000:0000:0000:0000:0000:0000:0000:0001') return FALSE;
if (strpos($ip, 'fc00:') === 0) return FALSE; // local addresses
// @todo add:
// ::0 (all zero) - invalid
// ff02::1:ff00:0/104 - Solicited-Node multicast addresses - add?
// 2001:0000::/29 through 2001:01f8::/29 - special purpose addresses
// 2001:db8::/32 - used in documentation
return TRUE;
}
/**
* Get current user's IP address in 'normal' form.
* Likely to be very similar to existing e107::getIP() function
* May log X-FORWARDED-FOR cases - or could generate a special IPV6 address, maybe?
*/
private function getCurrentIP()
{
if(!$this->ourIP)
{
$ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : 'x.x.x.x';
if ($ip4 = getenv('HTTP_X_FORWARDED_FOR'))
{
if (!$this->isAddressRoutable($ip))
{
$ip3 = explode(',', $ip4); // May only be one address; could be several, comma separated, if multiple proxies used
$ip = trim($ip3[count($ip3) - 1]); // If IP address is unroutable, replace with any forwarded_for address
$this->logBanItem(0, 'X_Forward '.$ip4.' --> '.$ip); // Just log for interest ATM
}
}
$this->ourIP = $this->ipEncode($ip); // Normalise for storage
}
return $this->ourIP;
}
/**
* Return the user's IP address, in normal or display-friendly form as requested
*
* @param boolean $forDisplay - TRUE for minimum-length display-friendly format. FALSE for 'normal' form (to be used when storing into DB etc)
*
* @return string IP address
*
* Note: if we define USER_IP (and maybe USER_DISPLAY_IP) constant, this function is strictly unnecessary. But we still need a format conversion routine
*/
public function getIP($forDisplay = FALSE)
{
if ($forDisplay == FALSE) return $this->ourIP;
return $this->ipDecode($this->ourIP);
}
/**
* Takes appropriate action for a blacklisted IP address
*
* @param int $code - integer value < 0 specifying the ban reason.
*
* @return void (may not even return)
*
* Looks up the reason code, and extracts the corresponding text.
* If this text begins with 'http://' or 'https://', assumed to be a link to a web page, and redirects.
* Otherwise displays an error message to the user (if configured) then aborts.
*/
private function banAction($code)
{
$search = '['.$code.']';
$fileName = $this->ourConfigDir.eIPHandler::BAN_FILE_ACTION_NAME.eIPHandler::BAN_FILE_EXTENSION;
if(!is_readable($fileName)) // Note readable, but the IP is still banned, so half further script execution.
{
if($this->debug === true || defset('e_DEBUG') === true)
{
echo "Your IP is banned!";
}
die();
// return; //
}
$vals = file($fileName);
if ($vals === FALSE || count($vals) == 0) return;
if (strpos($vals[0], '<?php') !== 0)
{
echo 'Invalid message file';
die();
}
unset($vals[0]);
foreach ($vals as $line)
{
if (strpos($line, ';') === 0) continue;
if (strpos($line, $search) === 0)
{ // Found the action line
if (e107::getPref('ban_retrigger'))
{
if ($tmp = fopen($this->ourConfigDir.eIPHandler::BAN_FILE_RETRIGGER_NAME.eIPHandler::BAN_FILE_EXTENSION, 'a'))
{
$logLine = time().' '.$this->matchAddress.' '.$code.' Retrigger: '.$this->ourIP."\n"; // Same format as log entries - can share routines
fwrite($tmp,$logLine);
fclose($tmp);
}
}
$line = trim(substr($line, strlen($search)));
if ((strpos($line, 'http://') === 0) || (strpos($line, 'https://') === 0))
{ // Display a specific web page
if (strpos($line, '?') === FALSE)
{
$line .= '?'.$search; // Add on the ban reason - may be useful in the page
}
e107::redirect($line);
exit();
}
// Otherwise just display any message and die
if($this->debug)
{
print_a("User Banned");
}
echo $line;
die();
}
}
$this->logBanItem($code, 'Unmatched action: '.$search.' - no block implemented');
}
/**
* Get whitelist and blacklist
*
* @return array - each element is an array with elements 'ip', 'action, and 'time_limit'
*
* Note: Intentionally a single call, so the two lists can be split across files as convenient
*
* At present the list is a single file, one entry per line, whitelist entries first. Most precisely defined addresses before larger subnets
*
* Format of each line is:
* IP_address action expiry_time additional_parameters
*
* where action is: >0 = whitelisted, <0 blacklisted, value is 'reason code'
* expiry_time is zero for an indefinite ban, time stamp for a limited ban
* additional_parameters may be required for certain actions in the future
*/
private function getWhiteBlackList()
{
$ret = array();
$fileName = $this->ourConfigDir.eIPHandler::BAN_FILE_IP_NAME.eIPHandler::BAN_FILE_EXTENSION;
if (!is_readable($fileName)) return $ret;
$vals = file($fileName);
if ($vals === FALSE || count($vals) == 0) return $ret;
if (strpos($vals[0], '<?php') !== 0)
{
echo 'Invalid list file';
die(); // Debatable, because admins can't get in if this fails. But can manually delete the file.
}
unset($vals[0]);
foreach ($vals as $line)
{
if (strpos($line, ';') === 0) continue;
if (trim($line))
{
$tmp = explode(' ',$line);
if (count($tmp) >= 2)
{
$ret[] = array('ip' => $tmp[0], 'action' => $tmp[1], 'time_limit' => intval(varset($tmp[2], 0)));
}
}
}
$this->actionCount = count($ret); // Note how many entries in list
return $ret;
}
/**
* Checks whether IP address is in the whitelist or blacklist.
*
* @param string $addr - IP address in 'normal' form
*
* @return int - >0 = whitelisted, 0 = not listed (= 'OK'), <0 is 'reason code' for ban
*
* note: Could maybe combine this with getWhiteBlackList() for efficiency, but makes it less general
*/
private function checkIP($addr)
{
$now = time();
$checkLists = $this->getWhiteBlackList();
if($this->debug)
{
echo "<h4>Banlist.php</h4>";
print_a($checkLists);
print_a("Now: ".$now. " ".date('r',$now));
}
foreach ($checkLists as $val)
{
if (strpos($addr, $val['ip']) === 0) // See if our address begins with an entry - handles wildcards
{ // Match found
if($this->debug)
{
print_a("Found ".$addr." in file. TimeLimit: ".date('r',$val['time_limit']));
}
if (($val['time_limit'] == 0) || ($val['time_limit'] > $now))
{ // Indefinite ban, or timed ban (not expired) or whitelist entry
if ($val['action']== eIPHandler::BAN_TYPE_LEGACY) return eIPHandler::BAN_TYPE_MANUAL; // Precautionary
$this->matchAddress = $val['ip'];
return $val['action']; // OK to just return - PHP should release the memory used by $checkLists
}
// Time limit expired
$this->clearBan = $val['ip']; // Note what triggered the match - it could be a wildcard (although timed ban unlikely!)
return 0; // Can just return - shouldn't be another entry
}
}
return 0;
}
/**
* Encode an IPv4 address into IPv6
* Similar functionality to ipEncode
*
* @param $ip
* @param bool $wildCards
* @param string $div
* @return string - the 'ip4' bit of an IPv6 address (i.e. last 32 bits)
*/
private function ip4Encode($ip, $wildCards = FALSE, $div = ':')
{
$ipa = explode('.', $ip);
$temp = '';
for ($s = 0; $s < 4; $s++)
{
if (!isset($ipa[$s])) $ipa[$s] = '*';
if ((($ipa[$s] == '*') || (strpos($ipa[$s], 'x') !== FALSE)) && $wildCards)
{
$temp .= 'xx';
}
else
{ // Put a zero in if wildcards not allowed
$temp .= sprintf('%02x', $ipa[$s]);
}
if ($s == 1) $temp .= $div;
}
return $temp;
}
/**
* Encode an IP address to internal representation. Returns string if successful; FALSE on error
* Default separates fields with ':'; set $div='' to produce a 32-char packed hex string
*
* @param string $ip - 'raw' IP address. May be IPv4, IPv6
* @param boolean $wildCards - if TRUE, wildcard characters allowed at the end of an address:
* '*' replaces 2 hex characters (primarily for 8-bit subnets of IPv4 addresses)
* 'x' replaces a single hex character
* @param string $div separator between 4-character blocks of the IPv6 address
*
* @return bool|string encoded IP. Always exactly 32 characters plus separators if conversion successful
* FALSE if conversion unsuccessful
*/
public function ipEncode($ip, $wildCards = FALSE, $div = ':')
{
$ret = '';
$divider = '';
if(strpos($ip, ':')!==FALSE)
{ // Its IPV6 (could have an IP4 'tail')
if(strpos($ip, '.')!==FALSE)
{ // IPV4 'tail' to deal with
$temp = strrpos($ip, ':')+1;
$ip = substr($ip, 0, $temp).$this->ip4Encode(substr($ip, $temp), $wildCards, $div);
}
// Now 'normalise' the address
$temp = explode(':', $ip);
$s = 8-count($temp); // One element will of course be the blank
foreach($temp as $f)
{
if($f=='')
{
$ret .= $divider.'0000'; // Always put in one set of zeros for the blank
$divider = $div;
if($s>0)
{
$ret .= str_repeat($div.'0000', $s);
$s = 0;
}
}
else
{
$ret .= $divider.sprintf('%04x', hexdec($f));
$divider = $div;
}
}
return $ret;
}
if(strpos($ip, '.')!==FALSE)
{ // Its IPV4
return str_repeat('0000'.$div, 5).'ffff'.$div.$this->ip4Encode($ip, $wildCards, $div);
}
return FALSE; // Unknown
}
/**
* Given a potentially truncated IPV6 address as used in the ban list files, adds 'x' characters etc to create
* a normalised IPV6 address as stored in the DB. Returned length is exactly 39 characters
* @param $address
* @return string
*/
public function ip6AddWildcards($address)
{
while (($togo = (39 - strlen($address))) > 0)
{
if (($togo % 5) == 0)
{
$address .= ':';
}
else
{
$address .= 'x';
}
}
return $address;
}
/**
* Takes an encoded IP address - returns a displayable one
* Set $IP4Legacy TRUE to display 'old' (IPv4) addresses in the familiar dotted format,
* FALSE to display in standard IPV6 format
* Should handle most things that can be thrown at it.
* If wildcard characters ('x' found, incorporated 'as is'
*
* @param string $ip encoded IP
* @param boolean $IP4Legacy
* @return string decoded IP
*/
public function ipDecode($ip, $IP4Legacy = TRUE)
{
if (strpos($ip, '.') !== false)
{
if ($IP4Legacy) return $ip; // Assume its unencoded IPV4
$ipa = explode('.', $ip);
$ip = '0:0:0:0:0:ffff:'.sprintf('%02x%02x:%02x%02x', $ipa[0], $ipa[1], $ipa[2], $ipa[3]);
$ip = str_repeat('0000'.':', 5).'ffff:'.$this->ip4Encode($ip, TRUE, ':');
}
if (strpos($ip, '::') !== false) return $ip; // Assume its a compressed IPV6 address already
if ((strlen($ip) == 8) && strpos($ip, ':') === false)
{ // Assume a 'legacy' IPV4 encoding
$ip = '0:0:0:0:0:ffff:'.implode(':',str_split($ip,4)); // Turn it into standard IPV6
}
elseif ((strlen($ip) == 32) && strpos($ip, ':') === false)
{ // Assume a compressed hex IPV6
$ip = implode(':',str_split($ip,4));
}
if (strpos($ip, ':') === false) return FALSE; // Return on problem - no ':'!
$temp = explode(':',$ip);
$z = 0; // State of the 'zero manager' - 0 = not started, 1 = running, 2 = done
$ret = '';
$zc = 0; // Count zero fields (not always required)
foreach ($temp as $t)
{
$v = hexdec($t);
if (($v != 0) || ($z == 2) || (strpos($t, 'x') !== FALSE))
{
if ($z == 1)
{ // Just finished a run of zeros
$z++;
$ret .= ':';
}
if ($ret) $ret .= ':';
if (strpos($t, 'x') !== FALSE)
{
$ret .= $t;
}
else
{
$ret .= sprintf('%x',$v); // Drop leading zeros
}
}
else
{ // Zero field
$z = 1;
$zc++;
}
}
if ($z == 1)
{ // Need to add trailing zeros, or double colon
if ($zc > 1) $ret .= '::'; else $ret .= ':0';
}
if ($IP4Legacy && (strpos($ret, '::ffff:') === 0))
{
$temp = str_replace(':', '', substr($ip,-9, 9));
$tmp = str_split($temp, 2); // Four 2-character hex values
$z = array();
foreach ($tmp as $t)
{
if ($t == 'xx')
{
$z[] = '*';
}
else
{
$z[] = hexdec($t);
}
}
$ret = implode('.',$z);
}
return $ret;
}
/**
* Given a string which may be IP address, email address etc, tries to work out what it is
* Uses a fairly simplistic (but quick) approach - does NOT check formatting etc
*
* @param string $string
* @return string ip|email|url|ftp|unknown
*/
public function whatIsThis($string)
{
$string = trim($string);
if (strpos($string, '@') !== FALSE) return 'email'; // Email address
if (strpos($string, 'http://') === 0) return 'url';
if (strpos($string, 'https://') === 0) return 'url';
if (strpos($string, 'ftp://') === 0) return 'ftp';
if (strpos($string, ':') !== FALSE) return 'ip'; // Identify ipv6
$string = strtolower($string);
if (str_replace(' ', '', strtr($string,'0123456789abcdef.*', ' ')) == '') // Delete all characters found in ipv4 addresses, plus wildcards
{
return 'ip';
}
return 'unknown';
}
/**
* Retrieve & cache host name
*
* @param string $ip_address
* @return string host name
*/
public function get_host_name($ip_address)
{
if(!isset($this->_host_name_cache[$ip_address]))
{
$this->_host_name_cache[$ip_address] = gethostbyaddr($ip_address);
}
return $this->_host_name_cache[$ip_address];
}
/**
* Generate DB query for domain name-related checks
*
* If an email address is passed, discards the individual's name
*
* @param string $email - an email address or domain name string
* @param string $fieldName
* @return array|bool false if invalid domain name format
* false if invalid domain name format
* array of values to compare
* @internal param string $fieldname - if non-empty, each array entry is a comparison with this field
*
*/
function makeDomainQuery($email, $fieldName = 'banlist_ip')
{
$tp = e107::getParser();
if (($tv = strrpos('@', $email)) !== FALSE)
{
$email = substr($email, $tv+1);
}
$tmp = strtolower($tp -> toDB(trim($email)));
if ($tmp == '') return FALSE;
if (strpos($tmp,'.') === FALSE) return FALSE;
$em = array_reverse(explode('.',$tmp));
$line = '';
$out = array('*@'.$tmp); // First element looks for domain as email address
foreach ($em as $e)
{
$line = '.'.$e.$line;
$out[] = '*'.$line;
}
if ($fieldName)
{
foreach ($out as $k => $v)
{
$out[$k] = '(`'.$fieldName."`='".$v."')";
}
}
return $out;
}
/**
* Split up an email address to check for banned domains.
* @param string $email - email address to process
* @param string $fieldname - name of field being searched in DB
*
* @return bool|string false if invalid address. Otherwise returns a set of values to check
* (Moved in from user_handler.php)
*/
public function makeEmailQuery($email, $fieldname = 'banlist_ip')
{
$tp = e107::getParser();
$tmp = strtolower($tp -> toDB(trim(substr($email, strrpos($email, "@")+1)))); // Pull out the domain name
if ($tmp == '') return FALSE;
if (strpos($tmp,'.') === FALSE) return FALSE;
$em = array_reverse(explode('.',$tmp));
$line = '';
$out = array($fieldname."='*@{$tmp}'"); // First element looks for domain as email address
foreach ($em as $e)
{
$line = '.'.$e.$line;
$out[] = '`'.$fieldname."`='*{$line}'";
}
return implode(' OR ',$out);
}
/**
* Routines beyond here are to handle banlist-related tasks which involve the DB
* note: Most of these routines already existed; moved in from e107_class.php
*/
/**
* Check if current user is banned
*
* This is called soon after the DB is opened, to do checks which require it.
* Previous checks have already done IP-based bans.
*
* Starts by removing expired bans if $this->clearBan is set
*
* Generates the queries to interrogate the ban list, then calls $this->check_ban().
* If the user is banned, $check_ban() never returns - so a return from this routine indicates a non-banned user.
*
* @return void
*
* @todo should be possible to simplify, since IP addresses already checked earlier
*/
public function ban()
{
$sql = e107::getDb();
if ($this->clearBan !== FALSE)
{ // Expired ban to clear - match exactly the address which triggered this action - could be a wildcard
$clearAddress = $this->ip6AddWildcards($this->clearBan);
if ($sql->delete('banlist',"`banlist_ip`='{$clearAddress}'"))
{
$this->actionCount--; // One less item on list
$this->logBanItem(0,'Ban cleared: '.$clearAddress);
// Now regenerate the text files - so no further triggers from this entry
$this->regenerateFiles();
}
}
// do other checks - main IP check is in _construct()
if($this->actionCount)
{
$ip = $this->getIP(); // This will be in normalised IPV6 form
if ($ip !== e107::LOCALHOST_IP && ($ip !== e107::LOCALHOST_IP2) && ($ip !== $this->serverIP)) // Check host name, user email to see if banned
{
$vals = array();
if (e107::getPref('enable_rdns'))
{
$vals = array_merge($vals, $this->makeDomainQuery($this->get_host_name($ip), ''));
}
if ((defined('USEREMAIL') && USEREMAIL))
{
// @todo is there point to this? Usually avoid a complete query if we skip it
$vals = array_merge($vals, $this->makeDomainQuery(USEREMAIL, ''));
}
if (count($vals))
{
$vals = array_unique($vals); // Could get identical values from domain name check and email check
if($this->debug)
{
print_a($vals);
}
$match = "`banlist_ip`='".implode("' OR `banlist_ip`='", $vals)."'";
$this->checkBan($match);
}
}
elseif($this->debug)
{
print_a("IP is LocalHost - skipping ban-check");
}
}
}
/**
* Check the banlist table. $query is used to determine the match.
* If $do_return, will always return with ban status - TRUE for OK, FALSE for banned.
* If return permitted, will never display a message for a banned user; otherwise will display any message then exit
* @todo consider whether can be simplified
*
* @param string $query - the 'WHERE' part of the DB query to be executed
* @param boolean $show_error - if true, adds a '403 Forbidden' header for a banned user
* @param boolean $do_return - if TRUE, returns regardless without displaying anything. if FALSE, for a banned user displays any message and exits
* @return boolean TRUE for OK, FALSE for banned.
*/
public function checkBan($query, $show_error = true, $do_return = false)
{
$sql = e107::getDb();
$pref = e107::getPref();
$tp = e107::getParser();
$admin_log = e107::getLog();
//$admin_log->addEvent(4,__FILE__."|".__FUNCTION__."@".__LINE__,"DBG","Check for Ban",$query,FALSE,LOG_TO_ROLLING);
if ($sql->select('banlist', '*', $query.' ORDER BY `banlist_bantype` DESC'))
{
// Any whitelist entries will be first, because they are positive numbers - so we can answer based on the first DB record read
$row = $sql->fetch();
if($row['banlist_bantype'] >= eIPHandler::BAN_TYPE_WHITELIST)
{
//$admin_log->addEvent(4,__FILE__."|".__FUNCTION__."@".__LINE__,"DBG","Whitelist hit",$query,FALSE,LOG_TO_ROLLING);
return true; // Whitelisted entry
}
// Found banlist entry in table here
if(($row['banlist_banexpires'] > 0) && ($row['banlist_banexpires'] < time()))
{ // Ban has expired - delete from DB
$sql->delete('banlist', $query);
$this->regenerateFiles();
return true;
}
// User is banned hereafter - just need to sort out the details.
// May need to retrigger ban period
if (!empty($pref['ban_retrigger']) && !empty($pref['ban_durations'][$row['banlist_bantype']]))
{
$dur = (int) $pref['ban_durations'][$row['banlist_bantype']];
$updateQry = array(
'banlist_banexpires' => (time() + ($dur * 60 * 60)),
'WHERE' => "banlist_ip ='".$row['banlist_ip']."'"
);
$sql->update('banlist', $updateQry);
$this->regenerateFiles();
//$admin_log->addEvent(4,__FILE__."|".__FUNCTION__."@".__LINE__,"DBG","Retrigger Ban",$row['banlist_ip'],FALSE,LOG_TO_ROLLING);
}
//$admin_log->addEvent(4,__FILE__."|".__FUNCTION__."@".__LINE__,"DBG","Active Ban",$query,FALSE,LOG_TO_ROLLING);
if ($show_error)
{
header('HTTP/1.1 403 Forbidden', true);
}
// May want to display a message
if (!empty($pref['ban_messages']))
{
// Ban still current here
if($do_return)
{
return false;
}
echo $tp->toHTML(varset($pref['ban_messages'][$row['banlist_bantype']])); // Show message if one set
}
//$admin_log->addEvent(4, __FILE__."|".__FUNCTION__."@".__LINE__, 'BAN_03', 'LAN_AUDIT_LOG_003', $query, FALSE, LOG_TO_ROLLING);
if($this->debug)
{
echo "<pre>query: ".$query;
echo "\nBanned</pre>";
}
// added missing if clause
if ($do_return)
{
return false;
}
exit();
}
if($this->debug)
{
echo "query: ".$query;
echo "<br />Not Banned ";
}
//$admin_log->addEvent(4,__FILE__."|".__FUNCTION__."@".__LINE__,"DBG","No ban found",$query,FALSE,LOG_TO_ROLLING);
return true; // Email address OK
}
/**
* Add an entry to the banlist. $bantype = 1 for manual, 2 for flooding, 4 for multiple logins
* Returns TRUE if ban accepted.
* Returns FALSE if ban not accepted (e.g. because on whitelist, or invalid IP specified)
*
* @param integer $bantype - either one of the BAN_TYPE_xxx constants, or a legacy value as above
* @param string $ban_message
* @param string $ban_ip
* @param integer $ban_user
* @param string $ban_notes
*
* @return boolean|integer check result - FALSE if ban rejected. TRUE if ban added. 1 if IP address already banned
*/
public function add_ban($bantype, $ban_message = '', $ban_ip = '', $ban_user = 0, $ban_notes = '')
{
if ($ban_ip == e107::LOCALHOST_IP || $ban_ip == e107::LOCALHOST_IP2)
{
return false;
}
$sql = e107::getDb();
$pref = e107::getPref();
switch ($bantype) // Convert from 'internal' ban types to those used in the DB
{
case 1 : $bantype = eIPHandler::BAN_TYPE_MANUAL; break;
case 2 : $bantype = eIPHandler::BAN_TYPE_FLOOD; break;
case 4 : $bantype = eIPHandler::BAN_TYPE_LOGINS; break;
}
if (!$ban_message)
{
$ban_message = 'No explanation given';
}
if (!$ban_ip)
{
$ban_ip = $this->getIP();
}
$ban_ip = preg_replace('/[^\w@\.:]*/', '', urldecode($ban_ip)); // Make sure no special characters
if (!$ban_ip)
{
return FALSE;
}
// See if address already in the banlist
if ($sql->select('banlist', '`banlist_bantype`', "`banlist_ip`='{$ban_ip}'"))
{
list($banType) = $sql->fetch();
if ($banType >= eIPHandler::BAN_TYPE_WHITELIST)
{ // Got a whitelist entry for this
//$admin_log->addEvent(4, __FILE__."|".__FUNCTION__."@".__LINE__, "BANLIST_11", 'LAN_AL_BANLIST_11', $ban_ip, FALSE, LOG_TO_ROLLING);
return FALSE;
}
return 1; // Already in ban list
}
/*
// See if the address is in the whitelist
if ($sql->select('banlist', '*', "`banlist_ip`='{$ban_ip}' AND `banlist_bantype` >= ".eIPHandler::BAN_TYPE_WHITELIST))
{ // Got a whitelist entry for this
//$admin_log->addEvent(4, __FILE__."|".__FUNCTION__."@".__LINE__, "BANLIST_11", 'LAN_AL_BANLIST_11', $ban_ip, FALSE, LOG_TO_ROLLING);
return FALSE;
} */
if(!empty($pref['enable_rdns_on_ban']))
{
$ban_message .= 'Host: '.$this->get_host_name($ban_ip);
}
// Add using an array - handles DB changes better
$sql->insert('banlist',
array(
'banlist_id' => 0,
'banlist_ip' => $ban_ip ,
'banlist_bantype' => $bantype ,
'banlist_datestamp' => time() ,
'banlist_banexpires' => (vartrue($pref['ban_durations'][$bantype]) ? time()+($pref['ban_durations'][$bantype]*60*60) : 0) ,
'banlist_admin' => $ban_user ,
'banlist_reason' => $ban_message ,
'banlist_notes' => $ban_notes
));
$this->regenerateFiles();
return TRUE;
}
/**
* Regenerate the text-based banlist files (called after a banlist table mod)
*/
public function regenerateFiles()
{
// Now regenerate the text files - so accesses of this IP address don't use the DB
$ipAdministrator = new banlistManager;
$ipAdministrator->writeBanListFiles('ip,htaccess');
}
/**
* @return false|string
*/
public function getConfigDir()
{
return $this->ourConfigDir;
}
/**
* Routine checks whether a file or directory has sufficient permissions
*
* ********** @todo this is in the wrong place! Move it to a more appropriate class! *************
*
* @param string $name - file with path (if ends in anything other than '/' or '\') or directory (if ends in '/' or '\')
* @param string(?) $perms - required permissions as standard *nix 3-digit string
* @param boolean $message - if TRUE, and insufficient rights, a message is output (in 0.8, to the message handler)
*
* @return boolean TRUE if sufficient permissions, FALSE if not (or error)
*
* For each mode character:
* 1 - execute
* 2 - writable
* 4 - readable
*/
public function checkFilePerms($name, $perms, $message = TRUE)
{
$isDir = ((substr($name, -1,1) == '\\') || (substr($name, -1,1) == '/'));
$result = FALSE;
$msg = '';
$dest = $isDir ? 'Directory' : 'File';
$reqPerms = intval('0'.$perms) & 511; // We want an integer value to match the return from fileperms()
if (!file_exists($name))
{
$msg = $dest.': '.$name.' does not exist';
}
if ($msg == '')
{
$realPerms = fileperms($name);
$mgs = $name.' is not a '.$dest; // Assume an error to start; clear messsage if all OK
switch ($realPerms & 0xf000)
{
case 0x8000 :
if (!$isDir)
{
$msg = '';
}
break;
case 0x4000 :
if ($isDir)
{
$msg = '';
}
break;
}
}
if ($msg == '')
{
if (($reqPerms & $realPerms) == $reqPerms)
{
$result = TRUE;
}
else
{
$msg = $name.': Insufficient permissions. Required: '.$this->permsToString($reqPerms).' Actual: '.$this->permsToString($realPerms);
}
}
//if ($message && $msg)
// { // Do something with the error message
// }
return $result;
}
/**
* Decode file/directory permissions into human-readable characters
*
* @param int $val representing permissions (LS 9 bits used)
*
* @return string exactly 9 characters, with blocks of 3 representing user, group and world permissions
*/
public function permsToString($val)
{
$perms = 'rwxrwxrwx';
$mask = 0x100;
for ($i = 0; $i < 9; $i++)
{
if (($mask & $val) == 0) $perms[$i] = '-';
$mask = $mask >> 1;
}
return $perms;
}
/**
* Function to see whether a user is already logged as being online
*
* @todo - this is possibly in the wrong place!
*
* @param string $ip - in 'normalised' IPV6 form
* @param string $browser - browser token as logged
*
* @return boolean|array FALSE if DB error or not found. Best match table row if found
*/
public function isUserLogged($ip, $browser)
{
$ourDB = e107::getDb('olcheckDB'); // @todo is this OK, or should an existing one be used?
$result = $ourDB->select('online', '*', "`user_ip` = '{$ip}' OR `user_token` = '{$browser}'");
if ($result === FALSE) return FALSE;
$gotIP = FALSE;
$gotBrowser = FALSE;
$bestRow = FALSE;
while (FALSE !== ($row = $ourDB->fetch()))
{
if ($row['user_token'] == $browser)
{
if ($row['user_ip'] == $ip)
{ // Perfect match
return $row;
}
// Just browser token match here
if ($bestRow === FALSE)
{
$bestRow = $row;
$gotBrowser = TRUE;
}
// else
// { // Problem - two or more rows with same browser token. What to do?
// }
}
elseif ($row['user_ip'] == $ip)
{ // Just IP match here
if ($bestRow === FALSE)
{
$bestRow = $row;
$gotIP = TRUE;
}
//else
//{ // Problem - two or more rows with same IP address. Hopefully better offer later!
//}
}
}
return $bestRow;
}
}
/**
* Routines involved with the management of the ban list and associated files
*/
class banlistManager
{
private $ourConfigDir = '';
public $banTypes = array();
public function __construct()
{
e107_include_once(e_LANGUAGEDIR.e_LANGUAGE."/admin/lan_banlist.php");
$this->ourConfigDir = e107::getIPHandler()->getConfigDir();
$this->banTypes = array( // Used in Admin-ui.
'-1' => BANLAN_101, // manual
'-2' => BANLAN_102, // Flood
'-3' => BANLAN_103, // Hits
'-4' => BANLAN_104, // Logins
'-5' => BANLAN_105, // Imported
'-6' => BANLAN_106, // Users
'-8' => BANLAN_107, // Imported
'100' => BANLAN_120 // Whitelist
);
}
/**
* Return an array of valid ban types (for use as indices into array, generally)
*/
public static function getValidReasonList()
{
return array(
eIPHandler::BAN_TYPE_LEGACY,
eIPHandler::BAN_TYPE_MANUAL,
eIPHandler::BAN_TYPE_FLOOD,
eIPHandler::BAN_TYPE_HITS,
eIPHandler::BAN_TYPE_LOGINS,
eIPHandler::BAN_TYPE_IMPORTED,
eIPHandler::BAN_TYPE_USER,
// Spare value
eIPHandler::BAN_TYPE_UNKNOWN
);
}
/**
* Create banlist-related text files as requested:
* List of whitelisted and blacklisted IP addresses
* file for easy import into .htaccess file (allow from...., deny from....)
* Generic CSV-format export file
*
* @param string $options {ip|htaccess|csv} - comma separated list (no spaces) to select which files to write
* @param string $typeList - optional comma-separated list of ban types required (default is all)
* Uses constants:
* BAN_FILE_IP_NAME Saves list of banned and whitelisted IP addresses
* BAN_FILE_ACTION_NAME Details of actions for different ban types
* BAN_FILE_HTACCESS File in format for direct paste into .htaccess
* BAN_FILE_CSV_NAME
* BAN_FILE_EXTENSION File extension to append
*
*/
public function writeBanListFiles($options = 'ip', $typeList = '')
{
e107::getMessage()->addDebug("Writing new Banlist files.");
$sql = e107::getDb();
$ipManager = e107::getIPHandler();
$optList = explode(',',$options);
$fileList = array(); // Array of file handles once we start
$fileNameList = array('ip' => eIPHandler::BAN_FILE_IP_NAME, 'htaccess' => eIPHandler::BAN_FILE_HTACCESS, 'csv' => eIPHandler::BAN_FILE_CSV_NAME);
$qry = 'SELECT * FROM `#banlist` ';
if ($typeList != '') $qry .= " WHERE`banlist_bantype` IN ({$typeList})";
$qry .= ' ORDER BY `banlist_bantype` DESC'; // Order ensures whitelisted addresses appear first
// Create a temporary file for each type as demanded. Vet the options array on this pass, as well
foreach($optList as $k => $opt)
{
if (isset($fileNameList[$opt]))
{
if ($tmp = fopen($this->ourConfigDir.$fileNameList[$opt].'_tmp'.eIPHandler::BAN_FILE_EXTENSION, 'w'))
{
$fileList[$opt] = $tmp; // Save file handle
fwrite($fileList[$opt], "<?php\n; die();\n");
//echo "Open File for write: ".$this->ourConfigDir.$fileNameList[$opt].'_tmp'.eIPHandler::BAN_FILE_EXTENSION.'<br />';
}
else
{
unset($optList[$k]);
/// @todo - flag error?
}
}
else
{
unset($optList[$k]);
}
}
if ($sql->gen($qry))
{
while ($row = $sql->fetch())
{
$row['banlist_ip'] = $this->trimWildcard($row['banlist_ip']);
if ($row['banlist_ip'] == '') continue; // Ignore empty IP addresses
if ($ipManager->whatIsThis($row['banlist_ip']) != 'ip') continue; // Ignore non-numeric IP Addresses
if ($row['banlist_bantype'] == eIPHandler::BAN_TYPE_LEGACY) $row['banlist_bantype'] = eIPHandler::BAN_TYPE_UNKNOWN; // Handle legacy bans
foreach ($optList as $opt)
{
$line = '';
switch ($opt)
{
case 'ip' :
// IP_address action expiry_time additional_parameters
$line = $row['banlist_ip'].' '.$row['banlist_bantype'].' '.$row['banlist_banexpires']."\n";
break;
case 'htaccess' :
$line = (($row['banlist_bantype'] > 0) ? 'allow from ' : 'deny from ').$row['banlist_ip']."\n";
break;
case 'csv' : /// @todo - when PHP5.1 is minimum, can use fputcsv() function
$line = $row['banlist_ip'].','.$this->dateFormat($row['banlist_datestamp']).','.$this->dateFormat($row['banlist_expires']).',';
$line .= $row['banlist_bantype'].',"'.$row['banlist_reason'].'","'.$row['banlist_notes'].'"'."\n";
break;
}
fwrite($fileList[$opt], $line);
}
}
}
// Now close each file
foreach ($optList as $opt)
{
fclose($fileList[$opt]);
}
// Finally, delete the working file, rename the temporary one
// Docs suggest that 'newname' is auto-deleted if it exists (as it usually should)
// - but didn't appear to work, hence copy then delete
foreach ($optList as $opt)
{
$oldName = $this->ourConfigDir.$fileNameList[$opt].'_tmp'.eIPHandler::BAN_FILE_EXTENSION;
$newName = $this->ourConfigDir.$fileNameList[$opt].eIPHandler::BAN_FILE_EXTENSION;
copy($oldName, $newName);
unlink($oldName);
}
}
/**
* Trim wildcards from IP addresses
*
* @param string $ip - IP address in any normal form
*
* Note - this removes all characters after (and including) the first '*' or 'x' found. So an '*' or 'x' in the middle of a string may
* cause unexpected results.
* @return string
*/
private function trimWildcard($ip)
{
$ip = trim($ip);
$temp = strpos($ip, 'x');
if ($temp !== FALSE)
{
return substr($ip, 0, $temp);
}
$temp = strpos($ip, '*');
if ($temp !== FALSE)
{
return substr($ip, 0, $temp);
}
return $ip;
}
/**
* Format date and time for export into a text file.
*
* @param int $date - standard Unix time stamp
*
* @return string. '0' if date is zero, else formatted in consistent way.
*/
private function dateFormat($date)
{
if ($date == 0) return '0';
return eShims::strftime('%Y%m%d_%H%M%S',$date);
}
/**
* Return string corresponding to a ban type
* @param int $banType - constant representing the ban type
* @param bool $forMouseover - if true, its the (usually longer) explanatory string for a mouseover
*
* @return string
*/
public function getBanTypeString($banType, $forMouseover = FALSE)
{
switch ($banType)
{
case eIPHandler::BAN_TYPE_LEGACY : $listOffset = 0; break;
case eIPHandler::BAN_TYPE_MANUAL : $listOffset = 1; break;
case eIPHandler::BAN_TYPE_FLOOD : $listOffset = 2; break;
case eIPHandler::BAN_TYPE_HITS : $listOffset = 3; break;
case eIPHandler::BAN_TYPE_LOGINS : $listOffset = 4; break;
case eIPHandler::BAN_TYPE_IMPORTED : $listOffset = 5; break;
case eIPHandler::BAN_TYPE_USER : $listOffset = 6; break;
case eIPHandler::BAN_TYPE_TEMPORARY : $listOffset = 9; break;
case eIPHandler::BAN_TYPE_WHITELIST :
return BANLAN_120; // Special case - may never occur
case eIPHandler::BAN_TYPE_UNKNOWN :
default :
if (($banType > 0) && ($banType < 9))
{
$listOffset = $banType; // BC conversions
}
else
{
$listOffset = 8;
}
}
if ($forMouseover) return constant('BANLAN_11'.$listOffset);
return constant('BANLAN_10'.$listOffset);
}
/**
* Write a text file containing the ban messages related to each ban reason
*/
public function writeBanMessageFile()
{
$pref['ban_messages'] = e107::getPref('ban_messages');
$oldName = $this->ourConfigDir.eIPHandler::BAN_FILE_ACTION_NAME.'_tmp'.eIPHandler::BAN_FILE_EXTENSION;
if ($tmp = fopen($oldName, 'w'))
{
fwrite($tmp, "<?php\n; die();\n");
foreach ($this->getValidReasonList() as $type)
{
fwrite($tmp,'['.$type.']'.$pref['ban_messages'][$type]."\n");
}
fclose($tmp);
$newName = $this->ourConfigDir.eIPHandler::BAN_FILE_ACTION_NAME.eIPHandler::BAN_FILE_EXTENSION;
copy($oldName, $newName);
unlink($oldName);
}
}
/**
* Check whether the message file (containing responses to ban types) exists
*
* @return boolean TRUE if exists, FALSE if doesn't exist
*/
public function doesMessageFileExist()
{
return is_readable($this->ourConfigDir.eIPHandler::BAN_FILE_ACTION_NAME.eIPHandler::BAN_FILE_EXTENSION);
}
/**
* Get entries from the ban action log
*
* @param int $start - offset into list (zero is first entry)
* @param int $count - number of entries to return - zero is a special case
* @param int $numEntry - filled in on return with the total number of entries in the log file
*
* @return array of strings; each string is a single log entry, newest first.
*
* Returns an empty array if an error occurs (or if no entries)
* If $count is zero, all entries are returned, in ascending order.
*/
public function getLogEntries($start, $count, &$numEntry)
{
$ret = array();
$numEntry = 0;
$fileName = e_SYSTEM.eIPHandler::BAN_LOG_DIRECTORY.eIPHandler::BAN_FILE_LOG_NAME;
if (!is_readable($fileName)) return $ret;
$vals = file($fileName);
if ($vals === FALSE) return $ret;
if (strpos($vals[0], '<?php') === 0)
{
unset($vals[0]);
}
if (strpos($vals[0], ';') === 0) unset($vals[0]);
$numEntry = count($vals);
if ($start > $numEntry) return $ret; // Empty return if beyond the end
if ($count == 0) return $vals; // Special case - return the lot in ascending date order
// Array is built up with newest last - but we want newest first. And we don't want to duplicate the array!
if (($start + $count) > $numEntry) $count = $numEntry - $start; // Last segment might not have enough entries
$ret = array_slice($vals, -$start - $count, $count);
return array_reverse($ret);
}
/**
* Converts one of the strings returned in a getLogEntries string into an array of values
*
* @param string $string - a text line, possibly including a 'newline' at the end
*
* @return array of up to $count entries
* ['banDate'] - time/date stamp
* ['banIP'] - IP address involved
* ['banReason'] - Numeric reason code for entry
* ['banNotes'] = any text appended
*/
public function splitLogEntry($string)
{
$temp = explode(' ',$string, 4);
while (count($temp) < 4) $temp[] = '';
$ret['banDate'] = $temp[0];
$ret['banIP'] = $temp[1];
$ret['banReason'] = $temp[2];
$ret['banNotes'] = str_replace("\n", '', $temp[3]);
return $ret;
}
/**
* Delete ban Log file
*
* @return boolean TRUE on success, FALSE on failure
*/
public function deleteLogFile()
{
$fileName = e_SYSTEM.eIPHandler::BAN_LOG_DIRECTORY.eIPHandler::BAN_FILE_LOG_NAME;
return unlink($fileName);
}
/**
* Update expiry time for IP addresses that have accessed the site while banned.
* Processes the entries in the 'ban retrigger' action file, and deletes the file
*
* Needs to be called from a cron job, at least once per hour, and ideally every few minutes. Otherwise banned users who access
* the site in the period since the last call to this routine may be able to get in because their ban has expired. (Unlikely to be
* an issue in practice)
*
* @return int number of IP addresses updated
*
* @todo - implement cron job and test
*/
public function banRetriggerAction()
{
//if (!e107::getPref('ban_retrigger')) return 0; // Should be checked earlier
$numEntry = 0; // Make sure this variable declared before passing it - total number of log entries.
$ipAction = array(); // Array of IP addresses to action
$fileName = $this->ourConfigDir.eIPHandler::BAN_FILE_RETRIGGER_NAME.eIPHandler::BAN_FILE_EXTENSION;
$entries = file($fileName);
if (!is_array($entries))
{
return 0; // Probably no retrigger actions
}
@unlink($fileName); // Delete the action file now we've read it in.
// Scan the list completely before doing any processing - this will ensure we only process the most recent entry for each IP address
while (count($entries) > 0)
{
$line = array_shift($entries);
$info = $this->splitLogEntry($line);
if ($info['banReason'] < 0)
{
$ipAction[$info['banIP']] = array('date' => $info['banDate'], 'reason' => $info['banReason']); // This will result in us gathering the most recent access from each IP address
}
}
if (count($ipAction) == 0) return 0; // Nothing more to do
// Now run through the database updating times
$numRet = 0;
$pref['ban_durations'] = e107::getPref('ban_durations');
$ourDb = e107::getDb(); // Should be able to use $sql, $sql2 at this point
$writeDb = e107::getDb('sql2');
foreach ($ipAction as $ipKey => $ipInfo)
{
if ($ourDb->select('banlist', '*', "`banlist_ip`='".$ipKey."'") === 1)
{
if ($row = $ourDb->fetch())
{
// @todo check next line
$writeDb->update('banlist',
'`banlist_banexpires` = '.intval($row['banlist_banexpires'] + $pref['ban_durations'][$row['banlist_banreason']]));
$numRet++;
}
}
}
if ($numRet)
{
$this->writeBanListFiles('ip'); // Just rewrite the ban list - the actual IP addresses won't have changed
}
return $numRet;
}
}