1<?php
2/*
3* e107 website system
4*
5* Copyright 2008-2013 e107 Inc (e107.org)
6* Released under the terms and conditions of the
7* GNU General Public License (http://www.gnu.org/licenses/gpl.txt)
8*
9* IP Address related routines, including banning-related code
10*
11* $URL$
12* $Revision$
13* $Id$
14*
15*/
16
17
18/**
19* @package e107
20* @subpackage e107_handlers
21* @version $Id$;
22*
23* Routines to manage IP addresses and banning.
24*/
25
26
27
28/**
29 *	Class to handle ban-related checks, and provide some utility functions related to IP addresses
30 *	There are two parts to the class:
31 *
32 *	Part 1
33 *	------
34 *	This part intentionally does NO database access, and requires an absolute minimum of file paths to be set up
35 *	(this is to minimise processing load in the event of an access from a banned IP address)
36 *	It works only with the user's IP address, and potentially browser 'signature'
37 *	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
38 *	(If DB access is required to handle a ban, it should only need to be done occasionally)
39 *
40 *	Part 2
41 *	------
42 *	This part handles those functions which require DB access.
43 *	The intention is that Part 1 will catch most existing bans, to reduce the incidence of abortive DB opens
44 *	If part 1 signals that a ban has expired, part 2 removes it from the database
45 *
46 *	Elsewhere
47 *	---------
48 *	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
49 *		eliminate the benefits of this handler - a DB access would be needed on every access from a banned IP address).
50 *	@todo	Implement the ban retriggering cron job (elsewhere)
51 *				- do we have a separate text file for the accesses in need of retriggering? Could then delete it once actioned; keeps it small
52 *	@todo	Implement flood bans - needs db access - maybe leave to the second part of this file or the online handler
53 *
54 *	All IP addresses are stored in 'normal' form - a fixed length IPV6 format with separator colons.
55 *
56 *	To use:
57 *		include this file, early on (before DB accesses started), and instantiate class ipHandler.
58 *
59 */
60
61
62class eIPHandler
63{
64	/**
65	 * IPV6 string for localhost - as stored in DB
66	 */
67//	const LOCALHOST_IP = '0000:0000:0000:0000:0000:ffff:7f00:0001';
68
69
70	const BAN_REASON_COUNT =	7;				// Update as more ban reasons added (max 10 supported)
71
72	const BAN_TYPE_LEGACY = 	0;				// Shouldn't get these unless update process not run
73	const BAN_TYPE_MANUAL = 	-1;				/// Manually entered bans
74	const BAN_TYPE_FLOOD  = 	-2;				/// Flood ban
75	const BAN_TYPE_HITS = 		-3;
76	const BAN_TYPE_LOGINS = 	-4;
77	const BAN_TYPE_IMPORTED = 	-5;				/// Imported bans
78	const BAN_TYPE_USER = 		-6;				/// User is banned
79												// Spare value
80	const BAN_TYPE_UNKNOWN = 	-8;
81	const BAN_TYPE_TEMPORARY =	-9;				/// Used during CSV import - giving it this value highlights problems
82
83	const BAN_TYPE_WHITELIST = 	100;			/// Entry for whitelist - actually not a ban at all! Keep at this value for BC
84
85
86	const BAN_FILE_DIRECTORY 	= 'cache/';				/// Directory containing the text files (within e_SYSTEM)
87	const BAN_LOG_DIRECTORY 	= 'logs/';				/// Directory containing the log file (within e_SYSTEM)
88
89	const BAN_FILE_LOG_NAME 	= 'banlog.log';			/// Logs bans etc
90	// Note for the following file names - the code appends the extension
91	const BAN_FILE_IP_NAME 		= 'banlist';			/// Saves list of banned and whitelisted IP addresses
92	const BAN_FILE_ACTION_NAME	= 'banactions';			/// Details of actions for different ban types
93	const BAN_FILE_HTACCESS 	= 'banhtaccess';		/// File in format for direct paste into .htaccess
94	const BAN_FILE_CSV_NAME 	= 'banlistcsv';			/// Output file in CSV format
95	const BAN_FILE_RETRIGGER_NAME = 'banretrigger';		/// Any bans needing retriggering
96	const BAN_FILE_EXTENSION 	= '.php';				/// File extension to use
97
98	/**
99	 *	IP address of current user, in 'normal' form
100	 */
101	private $ourIP = '';
102
103	private $serverIP = '';
104
105	private $debug = false;
106	/**
107	 *	Host name of current user
108	 *	Initialised when requested
109	 */
110	private $_host_name_cache = array();
111
112
113	/**
114	 *	Token for current user, calculated from browser settings.
115	 *	Supplements IP address (Can be spoofed, but helps differentiate among honest users at the same IP address)
116	 */
117	private $accessID = '';
118
119	/**
120	 *	Path to directory containing current config file(s)
121	 */
122	private	$ourConfigDir = '';
123
124	/**
125	 *	Current user's IP address status. Usually zero (neutral); may be one of the BAN_TYPE_xxx constants
126	 */
127	private $ipAddressStatus = 0;
128
129
130	/**
131	 *	Flag set to the IP address that triggered the match, if current IP has an expired ban to clear
132	 */
133	private $clearBan = FALSE;
134
135
136	/**
137	 *	IP Address from ban list file which matched (may have wildcards)
138	 */
139	private $matchAddress = '';
140
141	/**
142	 *	Number of entries read from banlist/whitelist
143	 */
144	private $actionCount = 0;
145
146	/**
147	 *	Constructor
148	 *
149	 *	Only one instance of this class is ever loaded, very early on in the initialisation sequence
150	 *
151	 *	@param	string	$configDir	Path to the directory containing the files used by this class
152	 *								If not set, defaults to BAN_FILE_DIRECTORY constant
153	 *
154	 *	On load it gets the user's IP address, and checks it against whitelist and blacklist files
155	 *	If the address is blacklisted, displays an appropriate message (as configured) and aborts
156	 *	Otherwise sets up
157	 */
158	public function __construct($configDir = '')
159	{
160		$configDir = trim($configDir);
161
162		if ($configDir)
163		{
164			$this->ourConfigDir = realpath($configDir);
165		}
166		else
167		{
168			$this->ourConfigDir = e_SYSTEM.eIPHandler::BAN_FILE_DIRECTORY;
169		}
170
171
172		$this->ourIP = $this->ipEncode($this->getCurrentIP());
173
174		$this->serverIP = $this->ipEncode(isset($_SERVER['SERVER_ADDR']) ? $_SERVER['SERVER_ADDR'] : 'x.x.x.x');
175
176		$this->makeUserToken();
177		$ipStatus = $this->checkIP($this->ourIP);
178		if ($ipStatus != 0)
179		{
180			if ($ipStatus < 0)
181			{	// Blacklisted
182				$this->logBanItem($ipStatus, 'result --> '.$ipStatus); // only log blacklist
183				$this->banAction($ipStatus);		// This will abort if appropriate
184			}
185			elseif ($ipStatus > 0)
186			{	// Whitelisted - we may want to set a specific indicator
187			}
188		}
189		// Continue here - user not banned (so far)
190	}
191
192	public function setIP($ip)
193	{
194		$this->ourIP = $this->ipEncode($ip);
195
196	}
197
198
199	public function debug($value)
200	{
201		$this->debug = ($value === true) ? true: false;
202	}
203
204
205
206
207	/**
208	 *	Add an entry to the banlist log file (which is a simple text file)
209	 *	A date/time string is prepended to the line
210	 *
211	 *	@param int $reason - numeric reason code, usually in range -10..+10
212	 *	@param string $message - additional text as required (length not checked, but should be less than 100 characters or so
213	 *
214	 *	@return void
215	 */
216	private function logBanItem($reason, $message)
217	{
218		if ($tmp = fopen(e_SYSTEM.eIPHandler::BAN_LOG_DIRECTORY.eIPHandler::BAN_FILE_LOG_NAME, 'a'))
219		{
220			$logLine = time().' '.$this->ourIP.' '.$reason.' '.$message."\n";
221			fwrite($tmp,$logLine);
222			fclose($tmp);
223		}
224	}
225
226
227
228	/**
229	 *	Generate relatively unique user token from browser info
230	 *		(but don't believe that the browser info is accurate - can readily be spoofed)
231	 *
232	 *	This supplements use of the IP address in some places; both to improve user identification, and to help deal with dynamic IP allocations
233	 *
234	 *	May be replaced by a 'global' e107 token at some point
235	 */
236	private function makeUserToken()
237	{
238		$tmpStr = '';
239		foreach (array('HTTP_USER_AGENT', 'HTTP_ACCEPT', 'HTTP_ACCEPT_CHARSET', 'HTTP_ACCEPT_LANGUAGE', 'HTTP_ACCEPT_ENCODING') as $v)
240		{
241			if (isset($_SERVER[$v]))
242			{
243				$tmpStr .= $_SERVER[$v];
244			}
245			else
246			{
247				$tmpStr .= 'dummy'.$v;
248			}
249		}
250		$this->accessID = md5($tmpStr);
251	}
252
253
254
255	/**
256	 *	Return browser-characteristics token
257	 */
258	public function getUserToken()
259	{
260		return $this->accessID;				// Should always be defined at this point
261	}
262
263
264
265	/**
266	 *	Check whether an IP address is routable
267	 *
268	 *	@param string $ip - IPV4 or IPV6 numeric address.
269	 *
270	 *	@return boolean TRUE if routable, FALSE if not
271
272	 @todo handle IPV6 fully
273	 */
274	public function isAddressRoutable($ip)
275	{
276		$ignore = array(
277						'0\..*' , '^127\..*' , 			// Local loopbacks
278						'192\.168\..*' , 					// RFC1918 - Private Network
279						'172\.(?:1[6789]|2\d|3[01])\..*' ,	// RFC1918 - Private network
280						'10\..*' , 							// RFC1918 - Private Network
281						'169\.254\..*' , 					// RFC3330 - Link-local, auto-DHCP
282						'2(?:2[456789]|[345][0-9])\..*'		// Single check for Class D and Class E
283					);
284
285
286
287		$pattern = '#^('.implode('|',$ignore).')#';
288
289		if(preg_match($pattern,$ip))
290		{
291			return false;
292		}
293
294
295		/* XXX preg_match doesn't accept arrays.
296		if (preg_match(array(
297						'#^0\..*#' , '#^127\..*#' , 			// Local loopbacks
298						'#^192\.168\..*#' , 					// RFC1918 - Private Network
299						'#^172\.(?:1[6789]|2\d|3[01])\..*#' ,	// RFC1918 - Private network
300						'#^10\..*#' , 							// RFC1918 - Private Network
301						'#^169\.254\..*#' , 					// RFC3330 - Link-local, auto-DHCP
302						'#^2(?:2[456789]|[345][0-9])\..*#'		// Single check for Class D and Class E
303					), $ip))
304		{
305			return FALSE;
306		}
307		*/
308
309		if (strpos(':', $ip) === FALSE) return TRUE;
310		// Must be an IPV6 address here
311		// @todo need to handle IPV4 addresses in IPV6 format
312		$ip = strtolower($ip);
313		if ($ip == 'ff02::1') return FALSE; 			// link-local all nodes multicast group
314		if ($ip == 'ff02:0000:0000:0000:0000:0000:0000:0001') return FALSE;
315		if ($ip == '::1') return FALSE;											// localhost
316		if ($ip == '0000:0000:0000:0000:0000:0000:0000:0001') return FALSE;
317		if (substr($ip, 0, 5) == 'fc00:') return FALSE;							// local addresses
318		// @todo add:
319		// ::0 (all zero) - invalid
320		// ff02::1:ff00:0/104 - Solicited-Node multicast addresses - add?
321		// 2001:0000::/29 through 2001:01f8::/29 - special purpose addresses
322		// 2001:db8::/32 - used in documentation
323		return TRUE;
324	}
325
326
327
328	/**
329	 *	Get current user's IP address in 'normal' form.
330	 *	Likely to be very similar to existing e107::getIP() function
331	 *	May log X-FORWARDED-FOR cases - or could generate a special IPV6 address, maybe?
332	 */
333	private function getCurrentIP()
334	{
335		if(!$this->ourIP)
336		{
337			$ip = isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : 'x.x.x.x';
338			if ($ip4 = getenv('HTTP_X_FORWARDED_FOR'))
339			{
340				if (!$this->isAddressRoutable($ip))
341				{
342					$ip3 = explode(',', $ip4);				// May only be one address; could be several, comma separated, if multiple proxies used
343					$ip = trim($ip3[sizeof($ip3) - 1]);						// If IP address is unroutable, replace with any forwarded_for address
344					$this->logBanItem(0, 'X_Forward  '.$ip4.' --> '.$ip);		// Just log for interest ATM
345				}
346			}
347			$this->ourIP = $this->ipEncode($ip); 				// Normalise for storage
348		}
349		return $this->ourIP;
350	}
351
352
353
354	/**
355	 *	Return the user's IP address, in normal or display-friendly form as requested
356	 *
357	 *	@param boolean $forDisplay - TRUE for minimum-length display-friendly format. FALSE for 'normal' form (to be used when storing into DB etc)
358	 *
359	 *	@return string IP address
360	 *
361	 *	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
362	 */
363	public function getIP($forDisplay = FALSE)
364	{
365		if ($forDisplay == FALSE) return $this->ourIP;
366		return $this->ipDecode($this->ourIP);
367	}
368
369
370
371	/**
372	 *	Takes appropriate action for a blacklisted IP address
373	 *
374	 *	@param int $code - integer value < 0 specifying the ban reason.
375	 *
376	 *	@return void (may not even return)
377	 *
378	 *	Looks up the reason code, and extracts the corresponding text.
379	 *	If this text begins with 'http://' or 'https://', assumed to be a link to a web page, and redirects.
380	 *	Otherwise displays an error message to the user (if configured) then aborts.
381	 */
382	private function banAction($code)
383	{
384		$search = '['.$code.']';
385		$fileName = $this->ourConfigDir.eIPHandler::BAN_FILE_ACTION_NAME.eIPHandler::BAN_FILE_EXTENSION;
386
387		if(!is_readable($fileName)) // Note readable, but the IP is still banned, so half further script execution.
388		{
389			if($this->debug === true || e_DEBUG === true)
390			{
391				echo "Your IP is banned!";
392			}
393
394			die();
395		    // return;		//
396		}
397
398		$vals  = file($fileName);
399		if ($vals === FALSE || count($vals) == 0) return;
400		if (substr($vals[0], 0, 5) != '<?php')
401		{
402			echo 'Invalid message file';
403			die();
404		}
405		unset($vals[0]);
406		foreach ($vals as $line)
407		{
408			if (substr($line, 0, 1) == ';') continue;
409			if (strpos($line, $search) === 0)
410			{	// Found the action line
411				if (e107::getPref('ban_retrigger'))
412				{
413					if ($tmp = fopen($this->ourConfigDir.eIPHandler::BAN_FILE_RETRIGGER_NAME.eIPHandler::BAN_FILE_EXTENSION, 'a'))
414					{
415						$logLine = time().' '.$this->matchAddress.' '.$code.' Retrigger: '.$this->ourIP."\n";	// Same format as log entries - can share routines
416						fwrite($tmp,$logLine);
417						fclose($tmp);
418					}
419				}
420				$line = trim(substr($line, strlen($search)));
421				if ((strpos($line, 'http://') === 0) || (strpos($line, 'https://') === 0))
422				{	// Display a specific web page
423					if (strpos($line, '?') === FALSE)
424					{
425						$line .= '?'.$search;			// Add on the ban reason - may be useful in the page
426					}
427					e107::redirect($line);
428					exit();
429				}
430				// Otherwise just display any message and die
431				if($this->debug)
432				{
433					print_a("User Banned");
434				}
435
436				echo $line;
437
438				die();
439			}
440		}
441		$this->logBanItem($code, 'Unmatched action: '.$search.' - no block implemented');
442	}
443
444
445
446	/**
447	 *	Get whitelist and blacklist
448	 *
449	 *	@return array  - each element is an array with elements 'ip', 'action, and 'time_limit'
450	 *
451	 *	Note: Intentionally a single call, so the two lists can be split across files as convenient
452	 *
453	 *	At present the list is a single file, one entry per line, whitelist entries first. Most precisely defined addresses before larger subnets
454	 *
455	 *	Format of each line is:
456	 *		IP_address	action	expiry_time additional_parameters
457	 *
458	 *	where action is: >0 = whitelisted, <0 blacklisted, value is 'reason code'
459	 *		expiry_time is zero for an indefinite ban, time stamp for a limited ban
460	 *		additional_parameters may be required for certain actions in the future
461	 */
462	private function getWhiteBlackList()
463	{
464		$ret = array();
465		$fileName = $this->ourConfigDir.eIPHandler::BAN_FILE_IP_NAME.eIPHandler::BAN_FILE_EXTENSION;
466		if (!is_readable($fileName)) return $ret;
467
468		$vals  = file($fileName);
469		if ($vals === FALSE || count($vals) == 0) return $ret;
470		if (substr($vals[0], 0, 5) != '<?php')
471		{
472			echo 'Invalid list file';
473			die();			// Debatable, because admins can't get in if this fails. But can manually delete the file.
474		}
475		unset($vals[0]);
476		foreach ($vals as $line)
477		{
478			if (substr($line, 0, 1) == ';') continue;
479			if (trim($line))
480			{
481				$tmp = explode(' ',$line);
482				if (count($tmp) >= 2)
483				{
484					$ret[] = array('ip' => $tmp[0], 'action' => $tmp[1], 'time_limit' => intval(varset($tmp[2], 0)));
485				}
486			}
487		}
488		$this->actionCount = count($ret);		// Note how many entries in list
489		return $ret;
490	}
491
492
493
494	/**
495	 *	Checks whether IP address is in the whitelist or blacklist.
496	 *
497	 *	@param string $addr - IP address in 'normal' form
498	 *
499	 *	@return int - >0 = whitelisted, 0 = not listed (= 'OK'), <0 is 'reason code' for ban
500	 *
501	 *	note: Could maybe combine this with getWhiteBlackList() for efficiency, but makes it less general
502	 */
503	private function checkIP($addr)
504	{
505		$now = time();
506		$checkLists = $this->getWhiteBlackList();
507
508		if($this->debug)
509		{
510			echo "<h4>Banlist.php</h4>";
511			print_a($checkLists);
512			print_a("Now: ".$now. "   ".date('r',$now));
513		}
514
515
516		foreach ($checkLists as $val)
517		{
518			if (strpos($addr, $val['ip']) === 0)	// See if our address begins with an entry - handles wildcards
519			{	// Match found
520
521				if($this->debug)
522				{
523					print_a("Found ".$addr." in file.  TimeLimit: ".date('r',$val['time_limit']));
524				}
525
526				if (($val['time_limit'] == 0) || ($val['time_limit'] > $now))
527				{	// Indefinite ban, or timed ban (not expired) or whitelist entry
528					if ($val['action']== eIPHandler::BAN_TYPE_LEGACY) return eIPHandler::BAN_TYPE_MANUAL;		// Precautionary
529					$this->matchAddress = $val['ip'];
530					return $val['action'];			// OK to just return - PHP should release the memory used by $checkLists
531				}
532				// Time limit expired
533				$this->clearBan = $val['ip'];	// Note what triggered the match - it could be a wildcard (although timed ban unlikely!)
534				return 0;						// Can just return - shouldn't be another entry
535			}
536
537		}
538		return 0;
539	}
540
541
542	/**
543	 *    Encode an IPv4 address into IPv6
544	 *    Similar functionality to ipEncode
545	 *
546	 * @param $ip
547	 * @param bool $wildCards
548	 * @param string $div
549	 * @return string - the 'ip4' bit of an IPv6 address (i.e. last 32 bits)
550	 */
551	private function ip4Encode($ip, $wildCards = FALSE, $div = ':')
552	{
553		$ipa = explode('.', $ip);
554		$temp = '';
555		for ($s = 0; $s < 4; $s++)
556		{
557			if (!isset($ipa[$s])) $ipa[$s] = '*';
558			if ((($ipa[$s] == '*') || (strpos($ipa[$s], 'x') !== FALSE)) && $wildCards)
559			{
560				$temp .= 'xx';
561			}
562			else
563			{	// Put a zero in if wildcards not allowed
564				$temp .= sprintf('%02x', $ipa[$s]);
565			}
566			if ($s == 1) $temp .= $div;
567		}
568		return $temp;
569	}
570
571
572	/**
573	 * Encode an IP address to internal representation. Returns string if successful; FALSE on error
574	 * Default separates fields with ':'; set $div='' to produce a 32-char packed hex string
575	 *
576	 *	@param string $ip - 'raw' IP address. May be IPv4, IPv6
577	 *	@param boolean $wildCards - if TRUE, wildcard characters allowed at the end of an address:
578	 *				'*' replaces 2 hex characters (primarily for 8-bit subnets of IPv4 addresses)
579	 *				'x' replaces a single hex character
580	 *	@param string $div separator between 4-character blocks of the IPv6 address
581	 *
582	 * @return bool|string encoded IP. Always exactly 32 characters plus separators if conversion successful
583	 *				FALSE if conversion unsuccessful
584	 */
585	public function ipEncode($ip, $wildCards = FALSE, $div = ':')
586	{
587		$ret = '';
588		$divider = '';
589		if(strpos($ip, ':')!==FALSE)
590		{ // Its IPV6 (could have an IP4 'tail')
591			if(strpos($ip, '.')!==FALSE)
592			{ // IPV4 'tail' to deal with
593				$temp = strrpos($ip, ':')+1;
594				$ip = substr($ip, 0, $temp).$this->ip4Encode(substr($ip, $temp), $wildCards, $div);
595			}
596			// Now 'normalise' the address
597			$temp = explode(':', $ip);
598			$s = 8-count($temp); // One element will of course be the blank
599			foreach($temp as $f)
600			{
601				if($f=='')
602				{
603					$ret .= $divider.'0000'; // Always put in one set of zeros for the blank
604					$divider = $div;
605					if($s>0)
606					{
607						$ret .= str_repeat($div.'0000', $s);
608						$s = 0;
609					}
610				}
611				else
612				{
613					$ret .= $divider.sprintf('%04x', hexdec($f));
614					$divider = $div;
615				}
616			}
617			return $ret;
618		}
619		if(strpos($ip, '.')!==FALSE)
620		{ // Its IPV4
621			return str_repeat('0000'.$div, 5).'ffff'.$div.$this->ip4Encode($ip, $wildCards, $div);
622		}
623		return FALSE; // Unknown
624	}
625
626
627	/**
628	 *    Given a potentially truncated IPV6 address as used in the ban list files, adds 'x' characters etc to create
629	 *    a normalised IPV6 address as stored in the DB. Returned length is exactly 39 characters
630	 * @param $address
631	 * @return string
632	 */
633	public function ip6AddWildcards($address)
634	{
635		while (($togo = (39 - strlen($address))) > 0)
636		{
637			if (($togo % 5) == 0)
638			{
639				$address .= ':';
640			}
641			else
642			{
643				$address .= 'x';
644			}
645		}
646		return $address;
647	}
648
649
650	/**
651	 * Takes an encoded IP address - returns a displayable one
652	 * Set $IP4Legacy TRUE to display 'old' (IPv4) addresses in the familiar dotted format,
653	 * FALSE to display in standard IPV6 format
654	 * Should handle most things that can be thrown at it.
655	 *	If wildcard characters ('x' found, incorporated 'as is'
656	 *
657	 * @param string $ip encoded IP
658	 * @param boolean $IP4Legacy
659	 * @return string decoded IP
660	 */
661	public function ipDecode($ip, $IP4Legacy = TRUE)
662	{
663		if (strstr($ip,'.'))
664		{
665			if ($IP4Legacy) return $ip;			// Assume its unencoded IPV4
666			$ipa = explode('.', $ip);
667			$ip = '0:0:0:0:0:ffff:'.sprintf('%02x%02x:%02x%02x', $ipa[0], $ipa[1], $ipa[2], $ipa[3]);
668			$ip = str_repeat('0000'.':', 5).'ffff:'.$this->ip4Encode($ip, TRUE, ':');
669		}
670		if (strstr($ip,'::')) return $ip;			// Assume its a compressed IPV6 address already
671		if ((strlen($ip) == 8) && !strstr($ip,':'))
672		{	// Assume a 'legacy' IPV4 encoding
673			$ip = '0:0:0:0:0:ffff:'.implode(':',str_split($ip,4));		// Turn it into standard IPV6
674		}
675		elseif ((strlen($ip) == 32) && !strstr($ip,':'))
676		{  // Assume a compressed hex IPV6
677			$ip = implode(':',str_split($ip,4));
678		}
679		if (!strstr($ip,':')) return FALSE;			// Return on problem - no ':'!
680		$temp = explode(':',$ip);
681		$z = 0;		// State of the 'zero manager' - 0 = not started, 1 = running, 2 = done
682		$ret = '';
683		$zc = 0;			// Count zero fields (not always required)
684		foreach ($temp as $t)
685		{
686			$v = hexdec($t);
687			if (($v != 0) || ($z == 2) || (strpos($t, 'x') !== FALSE))
688			{
689				if ($z == 1)
690				{ // Just finished a run of zeros
691					$z++;
692					$ret .= ':';
693				}
694				if ($ret) $ret .= ':';
695				if (strpos($t, 'x') !== FALSE)
696				{
697					$ret .= $t;
698				}
699				else
700				{
701					$ret .= sprintf('%x',$v);				// Drop leading zeros
702				}
703			}
704			else
705			{  // Zero field
706				$z = 1;
707				$zc++;
708			}
709		}
710		if ($z == 1)
711		{  // Need to add trailing zeros, or double colon
712			if ($zc > 1) $ret .= '::'; else $ret .= ':0';
713		}
714		if ($IP4Legacy && (substr($ret,0,7) == '::ffff:'))
715		{
716			$temp = str_replace(':', '', substr($ip,-9, 9));
717			$tmp = str_split($temp, 2);			// Four 2-character hex values
718			$z = array();
719			foreach ($tmp as $t)
720			{
721				if ($t == 'xx')
722				{
723					$z[] = '*';
724				}
725				else
726				{
727					$z[] = hexdec($t);
728				}
729			}
730			$ret = implode('.',$z);
731		}
732		return $ret;
733	}
734
735
736
737	/**
738	 * Given a string which may be IP address, email address etc, tries to work out what it is
739	 * Uses a fairly simplistic (but quick) approach - does NOT check formatting etc
740	 *
741	 * @param string $string
742	 * @return string ip|email|url|ftp|unknown
743	 */
744	public function whatIsThis($string)
745	{
746		$string = trim($string);
747		if (strpos($string, '@') !== FALSE) return 'email';		// Email address
748		if (strpos($string, 'http://') === 0) return 'url';
749		if (strpos($string, 'https://') === 0) return 'url';
750		if (strpos($string, 'ftp://') === 0) return 'ftp';
751		if (strpos($string, ':') !== FALSE) return 'ip';	// Identify ipv6
752		$string = strtolower($string);
753		if (str_replace(' ', '', strtr($string,'0123456789abcdef.*', '                   ')) == '')	// Delete all characters found in ipv4 addresses, plus wildcards
754		{
755			return 'ip';
756		}
757		return 'unknown';
758	}
759
760
761	/**
762	 * Retrieve & cache host name
763	 *
764	 * @param string $ip_address
765	 * @return string host name
766	 */
767	public function get_host_name($ip_address)
768	{
769		if(!isset($this->_host_name_cache[$ip_address]))
770		{
771			$this->_host_name_cache[$ip_address] = gethostbyaddr($ip_address);
772		}
773		return $this->_host_name_cache[$ip_address];
774	}
775
776
777	/**
778	 *    Generate DB query for domain name-related checks
779	 *
780	 *    If an email address is passed, discards the individual's name
781	 *
782	 * @param string $email - an email address or domain name string
783	 * @param string $fieldName
784	 * @return array|bool false if invalid domain name format
785	 * false if invalid domain name format
786	 * array of values to compare
787	 * @internal param string $fieldname - if non-empty, each array entry is a comparison with this field
788	 *
789	 */
790	function makeDomainQuery($email, $fieldName = 'banlist_ip')
791	{
792		$tp = e107::getParser();
793		if (($tv = strrpos('@', $email)) !== FALSE)
794		{
795			$email = substr($email, $tv+1);
796		}
797		$tmp = strtolower($tp -> toDB(trim($email)));
798		if ($tmp == '') return FALSE;
799		if (strpos($tmp,'.') === FALSE) return FALSE;
800		$em = array_reverse(explode('.',$tmp));
801		$line = '';
802		$out = array('*@'.$tmp);		// First element looks for domain as email address
803		foreach ($em as $e)
804		{
805			$line = '.'.$e.$line;
806			$out[] = '*'.$line;
807		}
808		if ($fieldName)
809		{
810			foreach ($out as $k => $v)
811			{
812				$out[$k] = '(`'.$fieldName."`='".$v."')";
813			}
814		}
815		return $out;
816	}
817
818
819
820	/**
821	 *	Split up an email address to check for banned domains.
822	 *	@param string $email - email address to process
823	 *	@param string $fieldname - name of field being searched in DB
824	 *
825	 *	@return bool|string false if invalid address. Otherwise returns a set of values to check
826	 *	(Moved in from user_handler.php)
827	 */
828	public function makeEmailQuery($email, $fieldname = 'banlist_ip')
829	{
830		$tp = e107::getParser();
831		$tmp = strtolower($tp -> toDB(trim(substr($email, strrpos($email, "@")+1))));	// Pull out the domain name
832		if ($tmp == '') return FALSE;
833		if (strpos($tmp,'.') === FALSE) return FALSE;
834		$em = array_reverse(explode('.',$tmp));
835		$line = '';
836		$out = array($fieldname."='*@{$tmp}'");		// First element looks for domain as email address
837		foreach ($em as $e)
838		{
839			$line = '.'.$e.$line;
840			$out[] = '`'.$fieldname."`='*{$line}'";
841		}
842		return implode(' OR ',$out);
843	}
844
845
846
847/**
848 *	Routines beyond here are to handle banlist-related tasks which involve the DB
849 *	note: Most of these routines already existed; moved in from e107_class.php
850 */
851
852
853	/**
854	 * Check if current user is banned
855	 *
856	 *	This is called soon after the DB is opened, to do checks which require it.
857	 *	Previous checks have already done IP-based bans.
858	 *
859	 *	Starts by removing expired bans if $this->clearBan is set
860	 *
861	 * 	Generates the queries to interrogate the ban list, then calls $this->check_ban().
862	 *	If the user is banned, $check_ban() never returns - so a return from this routine indicates a non-banned user.
863	 *
864	 *	@return void
865	 *
866	 *	@todo should be possible to simplify, since IP addresses already checked earlier
867	 */
868	public function ban()
869	{
870		$sql = e107::getDb();
871
872		if ($this->clearBan !== FALSE)
873		{	// Expired ban to clear - match exactly the address which triggered this action - could be a wildcard
874			$clearAddress = $this->ip6AddWildcards($this->clearBan);
875			if ($sql->delete('banlist',"`banlist_ip`='{$clearAddress}'"))
876			{
877				$this->actionCount--;		// One less item on list
878				$this->logBanItem(0,'Ban cleared: '.$clearAddress);
879				// Now regenerate the text files - so no further triggers from this entry
880				$this->regenerateFiles();
881			}
882		}
883
884
885		// do other checks - main IP check is in _construct()
886		if($this->actionCount)
887		{
888			$ip = $this->getIP(); // This will be in normalised IPV6 form
889
890			if ($ip !== e107::LOCALHOST_IP && ($ip !== e107::LOCALHOST_IP2) && ($ip !== $this->serverIP)) // Check host name, user email to see if banned
891			{
892				$vals = array();
893				if (e107::getPref('enable_rdns'))
894				{
895					$vals = array_merge($vals, $this->makeDomainQuery($this->get_host_name($ip), ''));
896				}
897				if ((defined('USEREMAIL') && USEREMAIL))
898				{
899						// @todo is there point to this? Usually avoid a complete query if we skip it
900					$vals = array_merge($vals, $this->makeDomainQuery(USEREMAIL, ''));
901				}
902				if (count($vals))
903				{
904					$vals = array_unique($vals);			// Could get identical values from domain name check and email check
905
906					if($this->debug)
907					{
908						print_a($vals);
909					}
910
911
912					$match = "`banlist_ip`='".implode("' OR `banlist_ip`='", $vals)."'";
913					$this->checkBan($match);
914				}
915			}
916			elseif($this->debug)
917			{
918				print_a("IP is LocalHost -  skipping ban-check");
919			}
920		}
921	}
922
923
924
925	/**
926	 * Check the banlist table. $query is used to determine the match.
927	 * If $do_return, will always return with ban status - TRUE for OK, FALSE for banned.
928	 * If return permitted, will never display a message for a banned user; otherwise will display any message then exit
929	 * @todo consider whether can be simplified
930	 *
931	 * @param string $query - the 'WHERE' part of the DB query to be executed
932	 * @param boolean $show_error - if true, adds a '403 Forbidden' header for a banned user
933	 * @param boolean $do_return - if TRUE, returns regardless without displaying anything. if FALSE, for a banned user displays any message and exits
934	 * @return boolean TRUE for OK, FALSE for banned.
935	 */
936	public function checkBan($query, $show_error = true, $do_return = false)
937	{
938		$sql = e107::getDb();
939		$pref = e107::getPref();
940		$tp = e107::getParser();
941		$admin_log = e107::getAdminLog();
942
943		//$admin_log->e_log_event(4,__FILE__."|".__FUNCTION__."@".__LINE__,"DBG","Check for Ban",$query,FALSE,LOG_TO_ROLLING);
944		if ($sql->select('banlist', '*', $query.' ORDER BY `banlist_bantype` DESC'))
945		{
946			// Any whitelist entries will be first, because they are positive numbers - so we can answer based on the first DB record read
947			$row = $sql->fetch();
948			if($row['banlist_bantype'] >= eIPHandler::BAN_TYPE_WHITELIST)
949			{
950				//$admin_log->e_log_event(4,__FILE__."|".__FUNCTION__."@".__LINE__,"DBG","Whitelist hit",$query,FALSE,LOG_TO_ROLLING);
951				return true;        // Whitelisted entry
952			}
953
954			// Found banlist entry in table here
955			if(($row['banlist_banexpires'] > 0) && ($row['banlist_banexpires'] < time()))
956			{ // Ban has expired - delete from DB
957				$sql->delete('banlist', $query);
958				$this->regenerateFiles();
959
960				return true;
961			}
962
963			// User is banned hereafter - just need to sort out the details.
964			// May need to retrigger ban period
965			if (!empty($pref['ban_retrigger']) && !empty($pref['ban_durations'][$row['banlist_bantype']]))
966			{
967				$dur = (int) $pref['ban_durations'][$row['banlist_bantype']];
968				$updateQry = array(
969					'banlist_banexpires'    => (time() + ($dur * 60 * 60)),
970					'WHERE'                 => "banlist_ip ='".$row['banlist_ip']."'"
971				);
972
973				$sql->update('banlist', $updateQry);
974				$this->regenerateFiles();
975				//$admin_log->e_log_event(4,__FILE__."|".__FUNCTION__."@".__LINE__,"DBG","Retrigger Ban",$row['banlist_ip'],FALSE,LOG_TO_ROLLING);
976			}
977			//$admin_log->e_log_event(4,__FILE__."|".__FUNCTION__."@".__LINE__,"DBG","Active Ban",$query,FALSE,LOG_TO_ROLLING);
978			if ($show_error)
979			{
980				header('HTTP/1.1 403 Forbidden', true);
981			}
982			// May want to display a message
983			if (!empty($pref['ban_messages']))
984			{
985				// Ban still current here
986				if($do_return)
987				{
988					return false;
989				}
990
991				echo $tp->toHTML(varset($pref['ban_messages'][$row['banlist_bantype']])); 	// Show message if one set
992			}
993			//$admin_log->e_log_event(4, __FILE__."|".__FUNCTION__."@".__LINE__, 'BAN_03', 'LAN_AUDIT_LOG_003', $query, FALSE, LOG_TO_ROLLING);
994
995			if($this->debug)
996			{
997				echo "<pre>query: ".$query;
998				echo "\nBanned</pre>";
999			}
1000
1001			// added missing if clause
1002			if ($do_return)
1003			{
1004				return false;
1005			}
1006
1007			exit();
1008		}
1009
1010		if($this->debug)
1011		{
1012			echo "query: ".$query;
1013			echo "<br />Not Banned ";
1014		}
1015
1016
1017		//$admin_log->e_log_event(4,__FILE__."|".__FUNCTION__."@".__LINE__,"DBG","No ban found",$query,FALSE,LOG_TO_ROLLING);
1018		return true; 		// Email address OK
1019	}
1020
1021
1022
1023	/**
1024	 * Add an entry to the banlist. $bantype = 1 for manual, 2 for flooding, 4 for multiple logins
1025	 * Returns TRUE if ban accepted.
1026	 * Returns FALSE if ban not accepted (e.g. because on whitelist, or invalid IP specified)
1027	 *
1028	 * @param integer $bantype - either one of the BAN_TYPE_xxx constants, or a legacy value as above
1029	 * @param string $ban_message
1030	 * @param string $ban_ip
1031	 * @param integer $ban_user
1032	 * @param string $ban_notes
1033	 *
1034	 * @return boolean|integer check result - FALSE if ban rejected. TRUE if ban added. 1 if IP address already banned
1035	 */
1036	public function add_ban($bantype, $ban_message = '', $ban_ip = '', $ban_user = 0, $ban_notes = '')
1037	{
1038
1039		if ($ban_ip == e107::LOCALHOST_IP || $ban_ip == e107::LOCALHOST_IP2)
1040		{
1041			return false;
1042		}
1043
1044
1045		$sql = e107::getDb();
1046		$pref = e107::getPref();
1047
1048		switch ($bantype)		// Convert from 'internal' ban types to those used in the DB
1049		{
1050			case 1 : $bantype = eIPHandler::BAN_TYPE_MANUAL; break;
1051			case 2 : $bantype = eIPHandler::BAN_TYPE_FLOOD; break;
1052			case 4 : $bantype = eIPHandler::BAN_TYPE_LOGINS; break;
1053		}
1054		if (!$ban_message)
1055		{
1056			$ban_message = 'No explanation given';
1057		}
1058		if (!$ban_ip)
1059		{
1060			$ban_ip = $this->getIP();
1061		}
1062		$ban_ip = preg_replace('/[^\w@\.:]*/', '', urldecode($ban_ip)); // Make sure no special characters
1063		if (!$ban_ip)
1064		{
1065			return FALSE;
1066		}
1067		// See if address already in the banlist
1068		if ($sql->select('banlist', '`banlist_bantype`', "`banlist_ip`='{$ban_ip}'"))
1069		{
1070			list($banType) = $sql->fetch();
1071
1072			if ($banType >= eIPHandler::BAN_TYPE_WHITELIST)
1073			{ // Got a whitelist entry for this
1074				//$admin_log->e_log_event(4, __FILE__."|".__FUNCTION__."@".__LINE__, "BANLIST_11", 'LAN_AL_BANLIST_11', $ban_ip, FALSE, LOG_TO_ROLLING);
1075				return FALSE;
1076			}
1077			return 1;		// Already in ban list
1078		}
1079		/*
1080		// See if the address is in the whitelist
1081		if ($sql->db_Select('banlist', '*', "`banlist_ip`='{$ban_ip}' AND `banlist_bantype` >= ".eIPHandler::BAN_TYPE_WHITELIST))
1082		{ // Got a whitelist entry for this
1083			//$admin_log->e_log_event(4, __FILE__."|".__FUNCTION__."@".__LINE__, "BANLIST_11", 'LAN_AL_BANLIST_11', $ban_ip, FALSE, LOG_TO_ROLLING);
1084			return FALSE;
1085		} */
1086		if(vartrue($pref['enable_rdns_on_ban']))
1087		{
1088			$ban_message .= 'Host: '.$this->get_host_name($ban_ip);
1089		}
1090		// Add using an array - handles DB changes better
1091		$sql->insert('banlist',
1092			array(
1093				'banlist_id'			=> 0,
1094				'banlist_ip' 			=> $ban_ip ,
1095				'banlist_bantype' 		=> $bantype ,
1096				'banlist_datestamp' 	=> time() ,
1097				'banlist_banexpires' 	=> (vartrue($pref['ban_durations'][$bantype]) ? time()+($pref['ban_durations'][$bantype]*60*60) : 0) ,
1098				'banlist_admin' 		=> $ban_user ,
1099				'banlist_reason' 		=> $ban_message ,
1100				'banlist_notes' 		=> $ban_notes
1101			));
1102
1103		$this->regenerateFiles();
1104		return TRUE;
1105	}
1106
1107
1108	/**
1109	 *	Regenerate the text-based banlist files (called after a banlist table mod)
1110	 */
1111	public function regenerateFiles()
1112	{
1113		// Now regenerate the text files - so accesses of this IP address don't use the DB
1114		$ipAdministrator = new banlistManager;
1115		$ipAdministrator->writeBanListFiles('ip,htaccess');
1116	}
1117
1118
1119
1120	public function getConfigDir()
1121	{
1122		return $this->ourConfigDir;
1123	}
1124
1125
1126
1127	/**
1128	 *	Routine checks whether a file or directory has sufficient permissions
1129	 *
1130	 *	********** @todo this is in the wrong place! Move it to a more appropriate class! *************
1131	 *
1132	 *	@param string $name - file with path (if ends in anything other than '/' or '\') or directory (if ends in '/' or '\')
1133	 *	@param string(?) $perms - required permissions as standard *nix 3-digit string
1134	 *	@param boolean $message - if TRUE, and insufficient rights, a message is output (in 0.8, to the message handler)
1135	 *
1136	 *	@return boolean TRUE if sufficient permissions, FALSE if not (or error)
1137	 *
1138	 *	For each mode character:
1139	 *		1 - execute
1140	 *		2 - writable
1141	 *		4 - readable
1142	 */
1143	public function checkFilePerms($name, $perms, $message = TRUE)
1144	{
1145		$isDir = ((substr($name, -1,1) == '\\') || (substr($name, -1,1) == '/'));
1146		$result = FALSE;
1147		$msg = '';
1148		$dest = $isDir ? 'Directory' : 'File';
1149		$reqPerms = intval('0'.$perms) & 511;				// We want an integer value to match the return from fileperms()
1150		if (!file_exists($name))
1151		{
1152			$msg = $dest.': '.$name.' does not exist';
1153		}
1154		if ($msg == '')
1155		{
1156			$realPerms = fileperms($name);
1157			$mgs = $name.' is not a '.$dest;		// Assume an error to start; clear messsage if all OK
1158			switch ($realPerms & 0xf000)
1159			{
1160				case 0x8000 :
1161					if (!$isDir)
1162					{
1163						$msg = '';
1164					}
1165					break;
1166				case 0x4000 :
1167					if ($isDir)
1168					{
1169						$msg = '';
1170					}
1171					break;
1172			}
1173		}
1174		if ($msg == '')
1175		{
1176			if (($reqPerms & $realPerms) == $reqPerms)
1177			{
1178				$result = TRUE;
1179			}
1180			else
1181			{
1182				$msg = $name.': Insufficient permissions. Required: '.$this->permsToString($reqPerms).'  Actual: '.$this->permsToString($realPerms);
1183			}
1184		}
1185		if ($message && $msg)
1186		{	// Do something with the error message
1187		}
1188		return $result;
1189	}
1190
1191
1192	/**
1193	 *	Decode file/directory permissions into human-readable characters
1194	 *
1195	 *	@param int $val representing permissions (LS 9 bits used)
1196	 *
1197	 *	@return string exactly 9 characters, with blocks of 3 representing user, group and world permissions
1198	 */
1199	public function permsToString($val)
1200	{
1201		$perms = 'rwxrwxrwx';
1202		$mask = 0x100;
1203
1204		for ($i = 0; $i < 9; $i++)
1205		{
1206			if (($mask & $val) == 0) $perms[$i] = '-';
1207			$mask = $mask >> 1;
1208		}
1209		return $perms;
1210	}
1211
1212
1213	/**
1214	 *	Function to see whether a user is already logged as being online
1215	 *
1216	 *	@todo - this is possibly in the wrong place!
1217	 *
1218	 *	@param string $ip - in 'normalised' IPV6 form
1219	 *	@param string $browser - browser token as logged
1220	 *
1221	 *	@return boolean|array  FALSE if DB error or not found. Best match table row if found
1222	 */
1223	public function isUserLogged($ip, $browser)
1224	{
1225		$ourDB = e107::getDb('olcheckDB');			// @todo is this OK, or should an existing one be used?
1226
1227		$result = $ourDB->select('online', '*', "`user_ip` = '{$ip}' OR `user_token` = '{$browser}'");
1228		if ($result === FALSE) return FALSE;
1229		$gotIP = FALSE;
1230		$gotBrowser = FALSE;
1231		$bestRow = FALSE;
1232		while (FALSE !== ($row = $ourDB->fetch()))
1233		{
1234			if ($row['user_token'] == $browser)
1235			{
1236				if ($row['user_ip'] == $ip)
1237				{	// Perfect match
1238					return $row;
1239				}
1240				// Just browser token match here
1241				if ($bestRow === FALSE)
1242				{
1243					$bestRow = $row;
1244					$gotBrowser = TRUE;
1245				}
1246				else
1247				{	// Problem - two or more rows with same browser token. What to do?
1248				}
1249			}
1250			elseif ($row['user_ip'] == $ip)
1251				{	// Just IP match here
1252					if ($bestRow === FALSE)
1253					{
1254						$bestRow = $row;
1255						$gotIP = TRUE;
1256					}
1257					else
1258					{	// Problem - two or more rows with same IP address. Hopefully better offer later!
1259					}
1260				}
1261		}
1262		return $bestRow;
1263	}
1264}
1265
1266
1267
1268
1269
1270
1271/**
1272 *	Routines involved with the management of the ban list and associated files
1273 */
1274class banlistManager
1275{
1276	private $ourConfigDir = '';
1277	public $banTypes = array();
1278
1279	public function __construct()
1280	{
1281		e107_include_once(e_LANGUAGEDIR.e_LANGUAGE."/admin/lan_banlist.php");
1282		$this->ourConfigDir = e107::getIPHandler()->getConfigDir();
1283		$this->banTypes = array( // Used in Admin-ui.
1284			'-1' 				=> BANLAN_101, // manual
1285			'-2'				=> BANLAN_102, // Flood
1286			'-3'				=> BANLAN_103, // Hits
1287			'-4'				=> BANLAN_104, // Logins
1288			'-5'				=> BANLAN_105, // Imported
1289			'-6'				=> BANLAN_106, // Users
1290			'-8'				=> BANLAN_107, // Imported
1291			'100'				=> BANLAN_120 // Whitelist
1292		);
1293
1294
1295	}
1296
1297	/**
1298	 *	Return an array of valid ban types (for use as indices into array, generally)
1299	 */
1300	public function getValidReasonList()
1301	{
1302		return array(
1303			eIPHandler::BAN_TYPE_LEGACY,
1304			eIPHandler::BAN_TYPE_MANUAL,
1305			eIPHandler::BAN_TYPE_FLOOD,
1306			eIPHandler::BAN_TYPE_HITS,
1307			eIPHandler::BAN_TYPE_LOGINS,
1308			eIPHandler::BAN_TYPE_IMPORTED,
1309			eIPHandler::BAN_TYPE_USER,
1310														// Spare value
1311			eIPHandler::BAN_TYPE_UNKNOWN
1312			);
1313	}
1314
1315
1316	/**
1317	 *	Create banlist-related text files as requested:
1318	 *		List of whitelisted and blacklisted IP addresses
1319	 *		file for easy import into .htaccess file  (allow from...., deny from....)
1320	 *		Generic CSV-format export file
1321	 *
1322	 *	@param string $options {ip|htaccess|csv} - comma separated list (no spaces) to select which files to write
1323	 *	@param string $typeList - optional comma-separated list of ban types required (default is all)
1324	 *	Uses constants:
1325	 *		BAN_FILE_IP_NAME		Saves list of banned and whitelisted IP addresses
1326	 *		BAN_FILE_ACTION_NAME	Details of actions for different ban types
1327	 *		BAN_FILE_HTACCESS		File in format for direct paste into .htaccess
1328	 *		BAN_FILE_CSV_NAME
1329	 *		BAN_FILE_EXTENSION		File extension to append
1330	 *
1331	 */
1332	public function writeBanListFiles($options = 'ip', $typeList = '')
1333	{
1334		e107::getMessage()->addDebug("Writing new Banlist files.");
1335		$sql = e107::getDb();
1336		$ipManager = e107::getIPHandler();
1337
1338		$optList = explode(',',$options);
1339		$fileList = array();				// Array of file handles once we start
1340
1341		$fileNameList = array('ip' => eIPHandler::BAN_FILE_IP_NAME, 'htaccess' => eIPHandler::BAN_FILE_HTACCESS, 'csv' => eIPHandler::BAN_FILE_CSV_NAME);
1342
1343		$qry = 'SELECT * FROM `#banlist` ';
1344		if ($typeList != '') $qry .= " WHERE`banlist_bantype` IN ({$typeList})";
1345		$qry .= ' ORDER BY `banlist_bantype` DESC';			// Order ensures whitelisted addresses appear first
1346
1347		// Create a temporary file for each type as demanded. Vet the options array on this pass, as well
1348		foreach($optList as $k => $opt)
1349		{
1350			if (isset($fileNameList[$opt]))
1351			{
1352				if ($tmp = fopen($this->ourConfigDir.$fileNameList[$opt].'_tmp'.eIPHandler::BAN_FILE_EXTENSION, 'w'))
1353				{
1354					$fileList[$opt] = $tmp;			// Save file handle
1355					fwrite($fileList[$opt], "<?php\n; die();\n");
1356					//echo "Open File for write: ".$this->ourConfigDir.$fileNameList[$opt].'_tmp'.eIPHandler::BAN_FILE_EXTENSION.'<br />';
1357				}
1358				else
1359				{
1360					unset($optList[$k]);
1361					/// @todo - flag error?
1362				}
1363			}
1364			else
1365			{
1366				unset($optList[$k]);
1367			}
1368		}
1369
1370		if ($sql->gen($qry))
1371		{
1372			while ($row = $sql->db_Fetch())
1373			{
1374				$row['banlist_ip'] = $this->trimWildcard($row['banlist_ip']);
1375				if ($row['banlist_ip'] == '') continue;								// Ignore empty IP addresses
1376				if ($ipManager->whatIsThis($row['banlist_ip']) != 'ip') continue;		// Ignore non-numeric IP Addresses
1377				if ($row['banlist_bantype'] == eIPHandler::BAN_TYPE_LEGACY) $row['banlist_bantype'] = eIPHandler::BAN_TYPE_UNKNOWN;		// Handle legacy bans
1378				foreach ($optList as $opt)
1379				{
1380					$line = '';
1381					switch ($opt)
1382					{
1383						case 'ip' :
1384							// IP_address	action	expiry_time additional_parameters
1385							$line = $row['banlist_ip'].' '.$row['banlist_bantype'].' '.$row['banlist_banexpires']."\n";
1386							break;
1387						case 'htaccess' :
1388							$line = (($row['banlist_bantype'] > 0) ? 'allow from ' : 'deny from ').$row['banlist_ip']."\n";
1389							break;
1390						case 'csv' :		/// @todo - when PHP5.1 is minimum, can use fputcsv() function
1391							$line = $row['banlist_ip'].','.$this->dateFormat($row['banlist_datestamp']).','.$this->dateFormat($row['banlist_expires']).',';
1392							$line .= $row['banlist_bantype'].',"'.$row['banlist_reason'].'","'.$row['banlist_notes'].'"'."\n";
1393							break;
1394					}
1395					fwrite($fileList[$opt], $line);
1396				}
1397			}
1398		}
1399
1400		// Now close each file
1401		foreach ($optList as $opt)
1402		{
1403			fclose($fileList[$opt]);
1404		}
1405
1406		// Finally, delete the working file, rename the temporary one
1407		// Docs suggest that 'newname' is auto-deleted if it exists (as it usually should)
1408		//		- but didn't appear to work, hence copy then delete
1409		foreach ($optList as $opt)
1410		{
1411			$oldName = $this->ourConfigDir.$fileNameList[$opt].'_tmp'.eIPHandler::BAN_FILE_EXTENSION;
1412			$newName = $this->ourConfigDir.$fileNameList[$opt].eIPHandler::BAN_FILE_EXTENSION;
1413			copy($oldName, $newName);
1414			unlink($oldName);
1415		}
1416	}
1417
1418
1419	/**
1420	 *    Trim wildcards from IP addresses
1421	 *
1422	 * @param string $ip - IP address in any normal form
1423	 *
1424	 *    Note - this removes all characters after (and including) the first '*' or 'x' found. So an '*' or 'x' in the middle of a string may
1425	 *            cause unexpected results.
1426	 * @return string
1427	 */
1428	private function trimWildcard($ip)
1429	{
1430		$ip = trim($ip);
1431		$temp = strpos($ip, 'x');
1432		if ($temp !== FALSE)
1433		{
1434			return substr($ip, 0, $temp);
1435		}
1436		$temp = strpos($ip, '*');
1437		if ($temp !== FALSE)
1438		{
1439			return substr($ip, 0, $temp);
1440		}
1441		return $ip;
1442	}
1443
1444
1445	/**
1446	 *	Format date and time for export into a text file.
1447	 *
1448	 *	@param int $date - standard Unix time stamp
1449	 *
1450	 *	@return string. '0' if date is zero, else formatted in consistent way.
1451	 */
1452	private function dateFormat($date)
1453	{
1454		if ($date == 0) return '0';
1455		return strftime('%Y%m%d_%H%M%S',$date);
1456	}
1457
1458
1459
1460	/**
1461	 *	Return string corresponding to a ban type
1462	 *	@param int $banType - constant representing the ban type
1463	 *	@param bool $forMouseover - if true, its the (usually longer) explanatory string for a mouseover
1464	 *
1465	 *	@return string
1466	 */
1467	public function getBanTypeString($banType, $forMouseover = FALSE)
1468	{
1469		switch ($banType)
1470		{
1471			case eIPHandler::BAN_TYPE_LEGACY :	$listOffset = 0; break;
1472			case eIPHandler::BAN_TYPE_MANUAL :	$listOffset = 1; break;
1473			case eIPHandler::BAN_TYPE_FLOOD :	$listOffset = 2; break;
1474			case eIPHandler::BAN_TYPE_HITS :	$listOffset = 3; break;
1475			case eIPHandler::BAN_TYPE_LOGINS :	$listOffset = 4; break;
1476			case eIPHandler::BAN_TYPE_IMPORTED :	$listOffset = 5; break;
1477			case eIPHandler::BAN_TYPE_USER :	$listOffset = 6; break;
1478			case eIPHandler::BAN_TYPE_TEMPORARY :	$listOffset = 9; break;
1479
1480			case eIPHandler::BAN_TYPE_WHITELIST :
1481				return BANLAN_120;		// Special case - may never occur
1482			case eIPHandler::BAN_TYPE_UNKNOWN :
1483			default :
1484				if (($banType > 0) && ($banType < 9))
1485				{
1486					$listOffset = $banType;			// BC conversions
1487				}
1488				else
1489				{
1490					$listOffset = 8;
1491				}
1492		}
1493		if ($forMouseover) return constant('BANLAN_11'.$listOffset);
1494		return constant('BANLAN_10'.$listOffset);
1495	}
1496
1497
1498
1499	/**
1500	 *	Write a text file containing the ban messages related to each ban reason
1501	 */
1502	public function writeBanMessageFile()
1503	{
1504		$pref['ban_messages'] = e107::getPref('ban_messages');
1505
1506		$oldName = $this->ourConfigDir.eIPHandler::BAN_FILE_ACTION_NAME.'_tmp'.eIPHandler::BAN_FILE_EXTENSION;
1507		if ($tmp = fopen($oldName, 'w'))
1508		{
1509			fwrite($tmp, "<?php\n; die();\n");
1510			foreach ($this->getValidReasonList() as $type)
1511			{
1512				fwrite($tmp,'['.$type.']'.$pref['ban_messages'][$type]."\n");
1513			}
1514			fclose($tmp);
1515			$newName = $this->ourConfigDir.eIPHandler::BAN_FILE_ACTION_NAME.eIPHandler::BAN_FILE_EXTENSION;
1516			copy($oldName, $newName);
1517			unlink($oldName);
1518		}
1519	}
1520
1521
1522
1523	/**
1524	 *	Check whether the message file (containing responses to ban types) exists
1525	 *
1526	 *	@return boolean TRUE if exists, FALSE if doesn't exist
1527	 */
1528	public function doesMessageFileExist()
1529	{
1530		return is_readable($this->ourConfigDir.eIPHandler::BAN_FILE_ACTION_NAME.eIPHandler::BAN_FILE_EXTENSION);
1531	}
1532
1533
1534
1535	/**
1536	 *	Get entries from the ban action log
1537	 *
1538	 *	@param int $start - offset into list (zero is first entry)
1539	 *	@param int $count - number of entries to return - zero is a special case
1540	 *	@param int $numEntry - filled in on return with the total number of entries in the log file
1541	 *
1542	 *	@return array of strings; each string is a single log entry, newest first.
1543	 *
1544	 *	Returns an empty array if an error occurs (or if no entries)
1545	 *	If $count is zero, all entries are returned, in ascending order.
1546	 */
1547	public function getLogEntries($start, $count, &$numEntry)
1548	{
1549		$ret = array();
1550		$numEntry = 0;
1551		$fileName = e_SYSTEM.eIPHandler::BAN_LOG_DIRECTORY.eIPHandler::BAN_FILE_LOG_NAME;
1552		if (!is_readable($fileName)) return $ret;
1553
1554		$vals  = file($fileName);
1555		if ($vals === FALSE) return $ret;
1556		if (substr($vals[0], 0, 5) == '<?php')
1557		{
1558			unset($vals[0]);
1559		}
1560		if (substr($vals[0], 0, 1) == ';') unset($vals[0]);
1561		$numEntry = count($vals);
1562		if ($start > $numEntry) return $ret;		// Empty return if beyond the end
1563		if ($count == 0) return $vals;				// Special case - return the lot in ascending date order
1564		// Array is built up with newest last - but we want newest first. And we don't want to duplicate the array!
1565		if (($start + $count) > $numEntry) $count = $numEntry - $start;		// Last segment might not have enough entries
1566		$ret = array_slice($vals, -$start - $count, $count);
1567		return array_reverse($ret);
1568	}
1569
1570
1571	/**
1572	 *	Converts one of the strings returned in a getLogEntries string into an array of values
1573	 *
1574	 *	@param string $string - a text line, possibly including a 'newline' at the end
1575	 *
1576	 *	@return array of up to $count entries
1577	 *		['banDate'] - time/date stamp
1578	 *		['banIP'] - IP address involved
1579	 *		['banReason'] - Numeric reason code for entry
1580	 *		['banNotes'] = any text appended
1581	 */
1582	public function splitLogEntry($string)
1583	{
1584		$temp = explode(' ',$string, 4);
1585		while (count($temp) < 4) $temp[] = '';
1586		$ret['banDate'] = $temp[0];
1587		$ret['banIP'] = $temp[1];
1588		$ret['banReason'] = $temp[2];
1589		$ret['banNotes'] = str_replace("\n", '', $temp[3]);
1590		return $ret;
1591	}
1592
1593
1594	/**
1595	 *	Delete ban Log file
1596	 *
1597	 *	@return boolean TRUE on success, FALSE on failure
1598	 */
1599	public function deleteLogFile()
1600	{
1601		$fileName = e_SYSTEM.eIPHandler::BAN_LOG_DIRECTORY.eIPHandler::BAN_FILE_LOG_NAME;
1602		return unlink($fileName);
1603	}
1604
1605
1606	/**
1607	 *	Update expiry time for IP addresses that have accessed the site while banned.
1608	 *	Processes the entries in the 'ban retrigger' action file, and deletes the file
1609	 *
1610	 *	Needs to be called from a cron job, at least once per hour, and ideally every few minutes. Otherwise banned users who access
1611	 *	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
1612	 *	an issue in practice)
1613	 *
1614	 *	@return int number of IP addresses updated
1615	 *
1616	 *	@todo - implement cron job and test
1617	 */
1618	public function banRetriggerAction()
1619	{
1620		//if (!e107::getPref('ban_retrigger')) return 0;		// Should be checked earlier
1621
1622		$numEntry = 0;			// Make sure this variable declared before passing it - total number of log entries.
1623		$ipAction = array();	// Array of IP addresses to action
1624		$fileName = $this->ourConfigDir.eIPHandler::BAN_FILE_RETRIGGER_NAME.eIPHandler::BAN_FILE_EXTENSION;
1625		$entries = file($fileName);
1626		if (!is_array($entries))
1627		{
1628			return 0;			// Probably no retrigger actions
1629		}
1630		@unlink($fileName);				// Delete the action file now we've read it in.
1631
1632		// Scan the list completely before doing any processing - this will ensure we only process the most recent entry for each IP address
1633		while (count($entries) > 0)
1634		{
1635			$line = array_shift($entries);
1636			$info = $this->splitLogEntry($line);
1637			if ($info['banReason'] < 0)
1638			{
1639				$ipAction[$info['banIP']] = array('date' => $info['banDate'], 'reason' => $info['banReason']);			// This will result in us gathering the most recent access from each IP address
1640			}
1641		}
1642
1643		if (count($ipAction) == 0) return 0;				// Nothing more to do
1644
1645		// Now run through the database updating times
1646		$numRet = 0;
1647		$pref['ban_durations'] = e107::getPref('ban_durations');
1648		$ourDb = e107::getDb();		// Should be able to use $sql, $sql2 at this point
1649		$writeDb = e107::getDb('sql2');
1650
1651		foreach ($ipAction as $ipKey => $ipInfo)
1652		{
1653			if ($ourDb->select('banlist', '*', "`banlist_ip`='".$ipKey."'") === 1)
1654			{
1655				if ($row = $ourDb->fetch())
1656				{
1657					// @todo check next line
1658					$writeDb->db_Update('banlist',
1659					'`banlist_banexpires` = '.intval($row['banlist_banexpires'] + $pref['ban_durations'][$row['banlist_banreason']]));
1660					$numRet++;
1661				}
1662			}
1663		}
1664		if ($numRet)
1665		{
1666			$this->writeBanListFiles('ip');		// Just rewrite the ban list - the actual IP addresses won't have changed
1667		}
1668		return $numRet;
1669	}
1670}
1671
1672
1673