<?php

class Tac_AnyApi_Model_AnyApi extends XenForo_Model
{
	
	
	
	public function isAnyApiBot($visibleUsername, $visibleEmail, $input, $apiNo, $apiSettings)
	{
		// first check that this API is active / even has paramaters, if not... quickly return false
		if($apiSettings['api_active'] == 0|| $apiSettings['url'] == "" || $apiSettings['logic_set'] == "" )
		{
			return false;	
		}
		
		$data = $input->filter(array(
			'username'   => XenForo_Input::STRING, // with FBHP installed, these are hidden
			'email'      => XenForo_Input::STRING,
		)); 
		
		
		//if we don't have visible username / email... use the hidden values to do the check
		$username = urlencode($data['username'] && ($data['username'] != 'abaf1b4e8d0e34afa3') ? $data['username'] : $visibleUsername);   
		$email = 	urlencode($data['email'] && ($data['email'] != 'x@819978f0-0b0f-11e2-892e-0800200c9a66.com') ? $data['email'] : $visibleEmail); 
		$apiName = $apiSettings['name'];
		$apiKey = $apiSettings['key'];
		$requestUrl = $apiSettings['url'];
		
		
		$apiRequestString = $this->apiRequestString($username, $email, $requestUrl, $apiName, $apiKey);
		
		$responseCache = $this->checkResponseCache($apiRequestString);
		if(!$responseCache)
		{			
			$apiResponse = $this->getApiResponse($apiRequestString, $apiSettings['data_type']);
			$this->addReponseCache($apiRequestString, $apiResponse);
		}
		else
		{
			$apiResponse = $responseCache['api_response'];
		}
				
		$decodedResponse = $this->decodeResponse($apiResponse, $apiSettings['data_type']);

		//if(!$decodedResponse){return false;} // no, we still want to log results when the API returns nothing
		
		$isApiBot = $this->isBotAccoringToConditions($decodedResponse, $apiSettings, $username, $email, $apiRequestString, $apiResponse);
		

		return $isApiBot; 

	}


	
	
	public function isBotAccoringToConditions($decodedResponse, $apiSettings, $username = "", $email = "", $apiRequestString = "", $apiResponse = "")
	{
		// we have up to 10 sets of conditions, how these sets are logically joined is admin defined (by logic_set)
		// for example, we could have something like this:
		// (Rule1 && Rule2 && Rule3) || (Rule4 && Rule5 && Rule6) || (Rule7 && Rule8 && Rule9) || Rule10 => A || B || C
		// or this:
		// (Rule1) && (Rule2) || (Rule3) && (Rule4) || (Rule5) 
		// we may not need to check every single condition, for instance (true || false) only the 1st bool needs to be evalauted to find true
		
		$origOperator = array('AND', 'OR');
		$replaceOperator = array('&&', '||');
		$logic_set = $apiSettings['logic_set'];
		$logic_set = str_replace($origOperator, $replaceOperator, $logic_set);
		
		$conditions = unserialize($apiSettings['rules']);
		
		
		$origRule = array('Rule1', 'Rule2', 'Rule3', 'Rule4', 'Rule5', 'Rule6', 'Rule7', 'Rule8', 'Rule9', 'Rule10');
		$replaceRule = array(
			($this->checkCondition($decodedResponse, $conditions[1])? "T" : "F"),
			($this->checkCondition($decodedResponse, $conditions[2])? "T" : "F"),
			($this->checkCondition($decodedResponse, $conditions[3])? "T" : "F"),
			($this->checkCondition($decodedResponse, $conditions[4])? "T" : "F"),
			($this->checkCondition($decodedResponse, $conditions[5])? "T" : "F"),
			($this->checkCondition($decodedResponse, $conditions[6])? "T" : "F"),
			($this->checkCondition($decodedResponse, $conditions[7])? "T" : "F"),
			($this->checkCondition($decodedResponse, $conditions[8])? "T" : "F"),
			($this->checkCondition($decodedResponse, $conditions[9])? "T" : "F"), 
			($this->checkCondition($decodedResponse, $conditions[10])? "T" : "F"),
		);		

		
		

		$logic_set = str_replace($origRule, $replaceRule, $logic_set);
		
		// this logic set is admin defined, so we can use eval on it (but rip out anything we don't expect to be there 1st)
		$logic_set_bool = preg_replace("/[^FT ()0-9\&\|]/", "", $logic_set);
		$logic_set_bool = str_replace("F", "false", $logic_set_bool);
		$logic_set_bool = str_replace("T", "true", $logic_set_bool);
		
		$logic_set_bool_ex = "return (".$logic_set_bool.");";
		
		
		$final_logic_set_bool = eval($logic_set_bool_ex);
		
		
		$is_blocked = ($final_logic_set_bool ? 1 : 0);
		$message = ($final_logic_set_bool ? $apiSettings['name'].' Blocked User From Registering' : $apiSettings['name'].' Allowed User');
		$logWriter = XenForo_DataWriter::create('Tac_AnyApi_DataWriter_Log');
		//$logWriter->setExistingData($log_id);	
		//$logWriter->bulkSet();
		$logWriter->set('log_date', XenForo_Application::$time);
		$logWriter->set('ip_address', $this->convertIpToLong());
		$logWriter->set('username', $username);
		$logWriter->set('email', $email);
		$logWriter->set('message', $message);
		$logWriter->set('api_request_string', $apiRequestString);
		$logWriter->set('api_response', $apiResponse);
		$logWriter->set('rules', $apiSettings['rules']);
		$logWriter->set('logic_set', $apiSettings['logic_set']);
		$logWriter->set('logic_set_bool', $logic_set_bool);
		$logWriter->set('is_blocked', $is_blocked);	
		$logWriter->save();	

		return $final_logic_set_bool;
			
	}
	
	public function checkResponseCache($apiRequestString)
	{	
		
		$options = XenForo_Application::get('options');
		$responseCache = (XenForo_Application::$time) - (($options->aaResponseCache) * 24 * 60 * 60);
		
		return $this->_getDb()->fetchRow('
			SELECT *
			FROM sf_anyapi_response_cache 
			WHERE api_request_string = ?
			AND time_stamp > ?
		', array($apiRequestString, $responseCache));
		
	}
	
	public function addReponseCache($apiRequestString, $apiResponse)
	{
		// only add to cache if we have a response (otherwise bots could keep returning if the api is down/slow on 1st reponse)
		if($apiResponse)
		{
			$writer = XenForo_DataWriter::create('Tac_AnyApi_DataWriter_ResponseCache');
			$writer->set('time_stamp', XenForo_Application::$time);
			$writer->set('api_request_string', $apiRequestString);
			$writer->set('api_response', $apiResponse);
			$writer->save();	
		}
	}
	
	
	public function checkCondition($apiResponse, $condition)
	{
		/* work out the logic of the condition and see if it applies to the response
		* if we get here, then the condition definately has a logic string.. break the string down to work out what it is
		*
		* The logic should always be in the form: 
		* {$response['someindex']} OPERATOR VALUE
		* For example: 	
		* {$response['email']['lastseen']} > 0 // bot emails banned forever 
		*
		*/
		
		if(!$apiResponse)
		{
			return false;
		}
		
		
		// 1st, drop comments
		
		$conditionArray = explode("//", $condition);
		$condition = trim($conditionArray[0]);
		
		// now turn string into exectuble php, this could be done with eval(), but it's not safe... so lets do some replacing
		
		// there will always be 2 sides to this equation, and it will be split by one of the following operators: < > =	contains !contains
		$operatorLestThan = false; $operatorGreaterThan = false; $operatorEquals = false; 
		$operatorContains = false; $operatorNotContains = false;
		
		$operatorLestThan = (strpos($condition, '<') > -1 ? true : false); 
		$operatorGreaterThan = (strpos($condition, '>') > -1 ? true : false);
		
		$operatorNotEquals = (strpos($condition, '!=') > -1 ? true : false);	
		if(!$operatorNotEquals)
		{
			$operatorEquals = (strpos($condition, '=') > -1 ? true : false);
		}
		
		$operatorNotContains = (strpos($condition, '!contains') > -1 ? true : false);
		if(!$operatorNotContains)
		{
			$operatorContains = (strpos($condition, 'contains') > -1 ? true : false);
		}
		
		
		$LHS = ""; $RHS = "";
		
		if($operatorLestThan)
		{
			$cond = explode("<", $condition);
			$LHS = trim($cond[0]); $RHS = trim($cond[1]);
		}
		else if($operatorGreaterThan)
		{
			$cond = explode(">", $condition);
			$LHS = trim($cond[0]); $RHS = trim($cond[1]);	
		}
		else if($operatorEquals)
		{
			$cond = explode("=", $condition);
			$LHS = trim($cond[0]); $RHS = trim($cond[1]);	
		}	
		else if($operatorNotEquals)
		{
			$cond = explode("!=", $condition);
			$LHS = trim($cond[0]); $RHS = trim($cond[1]);	
		}
		else if($operatorContains)
		{
			$cond = explode("contains", $condition);
			$LHS = trim($cond[0]); $RHS = trim($cond[1]);	
		}
		else if($operatorNotContains)
		{
			$cond = explode("!contains", $condition);
			$LHS = trim($cond[0]); $RHS = trim($cond[1]);	
		}				
		else // it's not a condition that's supported, return false (not bot)
		{
			return false;
		}
			
		$LHSValue = $this->evaluateLHS($LHS, $apiResponse);
		
		if ($operatorNotContains || $operatorContains)
		{
			// we're using the contains operator, so don't numerically evaluate the RHS
			if($operatorContains)
			{
				$RHSValue = $this->containsRHS($RHS, $apiResponse);
				return $RHSValue; // no need to evaulate left and rhs when using contains
			}
			else if($operatorNotContains)
			{
				$RHSValue = $this->notContainsRHS($RHS, $apiResponse);
				return $RHSValue; // no need to evaulate left and rhs when using contains
			}	
		}
		else 
		{
			$RHSValue = $this->evaluateRHS($RHS, $apiResponse);
		}
		// now we need to evaluate {$LHSValue OPERATOR $RHSValue}
	
		if($LHSValue === false || $RHSValue === false)
		{
			return false;		
		}
		
		if($operatorLestThan)
		{
			//echo('$operatorLestThan > ');
			if(is_numeric($LHSValue) && is_numeric($RHSValue))
			{
				//echo (float)$LHSValue." < ".(float)$RHSValue.': ';	
				return ( (float)$LHSValue < (float)$RHSValue);}
			return ($LHSValue < $RHSValue);
		}
		
		if($operatorGreaterThan)
		{
			//echo('$operatorGreaterThan > ');
			if(is_numeric($LHSValue) && is_numeric($RHSValue))
			{
				//echo (float)$LHSValue." > ".(float)$RHSValue.': ';
				return ( (float)$LHSValue > (float)$RHSValue);
			}
			return ($LHSValue > $RHSValue);
		}			
		
		if($operatorEquals)
		{	
			//echo('$operatorEquals > ');
			if(is_numeric($LHSValue) && is_numeric($RHSValue))
			{
				//echo (float)$LHSValue." == ".(float)$RHSValue.': ';
				return ( (float)$LHSValue ==(float)$RHSValue);}
			return ($LHSValue == $RHSValue);
		}
			
	return false;	
	}
	
	public function containsRHS($RHS, $apiResponse)
	{
		$RHS = str_replace("'", "", $RHS);
		if (strpos($apiResponse, $RHS) !== false)
		{return true;}
		return false;
	}
	
	public function notContainsRHS($RHS, $apiResponse)
	{
		$RHS = str_replace("'", "", $RHS);
		if (strpos($apiResponse, $RHS) === false)
		{return true;}
		return false;
	}
	
	public function evaluateLHS($LHS, $apiResponse) // this will always be the JSON index: $response['email']['lastseen'], we also support $response[email][lastseen]
	{
		
		$remove = array('"', "'", ']', '{', '}');
		$LHS = str_replace($remove, "", $LHS);	// $response[email[lastseen		
		$LHSArgs = array_map('trim', explode('[', $LHS)); // [0]=$response, [1]=email, [2]=lastseen
		
		
		
		if(!$LHSArgs || $LHSArgs[0] != '$response')
		{
			return false; // whatever this is, we dont support it,  return false (not bot)
		}

		
		$nestSize = count($LHSArgs);
        $LHSValue = "";
        
        // Before assigning LHSValue, check the index exists, if it doesn't, return false (non bot)		
        		switch ($nestSize) {
				    case 0: 
				    case 1: // we should never get here, unless there are no indexes with the $response in the ACP
				        return false;
				        break;
				    case 2:
				        $LHSValue = (array_key_exists($LHSArgs[1], $apiResponse) ? $apiResponse[$LHSArgs[1]] : false);
				        break;
				    case 3:
				    	if (	
				    		array_key_exists($LHSArgs[1], $apiResponse) && 
				    		array_key_exists($LHSArgs[2], $apiResponse[$LHSArgs[1]]) 
				    		) 
				    	{
				        	$LHSValue = $apiResponse[$LHSArgs[1]][$LHSArgs[2]];
			        	}
			        	else 
			        	{
				        	$LHSValue = false;
			        	}
				        break;
				    case 4:
				    	if (	
				    		array_key_exists($LHSArgs[1], $apiResponse) && 
				    		array_key_exists($LHSArgs[2], $apiResponse[$LHSArgs[1]]) &&
				    		array_key_exists($LHSArgs[3], $apiResponse[$LHSArgs[1]][$LHSArgs[2]])  
				    		) 
				    	{
				        	$LHSValue = $apiResponse[$LHSArgs[1]][$LHSArgs[2]][$LHSArgs[3]];
			        	}
			        	else 
			        	{
				        	$LHSValue = false;
			        	}				        
				        break;
				   	case 5:
				   		if (	
				    		array_key_exists($LHSArgs[1], $apiResponse) && 
				    		array_key_exists($LHSArgs[2], $apiResponse[$LHSArgs[1]]) &&
				    		array_key_exists($LHSArgs[3], $apiResponse[$LHSArgs[1]][$LHSArgs[2]]) &&
				    		array_key_exists($LHSArgs[4], $apiResponse[$LHSArgs[1]][$LHSArgs[2]][$LHSArgs[3]])   
				    		) 
				    	{
				        	$LHSValue = $apiResponse[$LHSArgs[1]][$LHSArgs[2]][$LHSArgs[3]][$LHSArgs[4]];
			        	}
			        	else 
			        	{
				        	$LHSValue = false;
			        	}					        
				        break;
				   	case 6:
				   		if (	
				    		array_key_exists($LHSArgs[1], $apiResponse) && 
				    		array_key_exists($LHSArgs[2], $apiResponse[$LHSArgs[1]]) &&
				    		array_key_exists($LHSArgs[3], $apiResponse[$LHSArgs[1]][$LHSArgs[2]]) &&
				    		array_key_exists($LHSArgs[4], $apiResponse[$LHSArgs[1]][$LHSArgs[2]][$LHSArgs[3]]) && 
				    		array_key_exists($LHSArgs[5], $apiResponse[$LHSArgs[1]][$LHSArgs[2]][$LHSArgs[3]][$LHSArgs[4]])  
				    		) 
				    	{
				        	$LHSValue = $apiResponse[$LHSArgs[1]][$LHSArgs[2]][$LHSArgs[3]][$LHSArgs[4]][$LHSArgs[5]];
			        	}
			        	else 
			        	{
				        	$LHSValue = false;
			        	}				        
				        break;    
				}
		
						
		// now check for date format, if it's a date, convert it to unix time stamp
		if (strtotime($LHSValue) > 0 )
		{
		 return strtotime($LHSValue);	
		}

		return $LHSValue;
	}
	
	public function evaluateRHS($RHS, $apiResponse)
	{
		
		
		// 1stly, convert known variables (eg: $timenow - 1814400 // 3 weeks )
		$replaceVariables = array('$timenow');
		$replaceValues = array(time());
		$RHS = str_replace($replaceVariables, $replaceValues, $RHS);	// 1360091477 - 1814400 
		
		// since this side can be an equation with -/+/* and /  operators, split and evaluate.. lets not support brackets for now (to complicated)
		$RHS = str_replace(array('(', ')', '{', '}'), '', $RHS); // things we might need to support latter
		
		$RHSarray = preg_split("/(\+ |\- |\*|\/)+/", $RHS, -1, PREG_SPLIT_DELIM_CAPTURE);
		$RHSarray = array_map('trim', $RHSarray);
		
		
		// if rhs has [var op var], or [var op var op var] etc..
		while(count($RHSarray) > 2)
		{
			// assuming this conforms to [var] [operatror] [var]
			$threePartValue = $this->evaluateThreePartOperatorEquation($RHSarray[0],$RHSarray[1],$RHSarray[2]);
			
			// now remove the 1st 3 arrays of $RHSarray and replace with $threePartValue (then repeat
			unset($RHSarray[0]);
			unset($RHSarray[1]);
			unset($RHSarray[2]);
			
			$RHSarray = array_merge(array($threePartValue), $RHSarray); // using array_merge will rebuild the indexes
			
		}
	
	// if the correct format has been used, this should flattern to a 1 index array	
	return $RHSarray[0];
			
	}
	
	
	
	public function evaluateThreePartOperatorEquation($varA, $operator, $varB)
	{
		// for now, for the RHS equation, we're only supporting [num operator num] where num is float or an int 
		$val = false;
		// work out if vars are strings / values 
		if(is_numeric($varA) && is_numeric($varB) && ($operator == '+' || $operator == '-' || $operator == '*' || $operator == '/' ) )
		{
		
			switch ($operator) 
			{
			 	case '+': 
			    	$val = (float)$varA + (float)$varB;
			    	break;	
			    case '-': 
			    	$val = (float)$varA - (float)$varB;
			    	break;
			    case '*': 
			    	$val = (float)$varA * (float)$varB;
			    	break;
			    case '/': 
			    	$val = (float)$varA / (float)$varB;
			    	break;
		    }
		}		
		return $val;
	}
	
	
	
	
	
	
	public function getRealFeildData($input)
	{
		
		if($input)
		{	
			$addOnModel = $this->getModelFromCache('XenForo_Model_AddOn');
			if (($addon = $addOnModel->getAddOnById('FoolBotHoneyPot')) && !empty($addon['active']) )
			{
				$uuids = $this->getModelFromCache('Tac_FoolBotHoneyPot_Model_HoneyPot')->getFeildUUIDs();
				
				// if we didn't get uuids, the session has timed out
				if(!$uuids){return array();}	
						 
				$uuidData = $input->filter(array(
					$uuids['1']  => XenForo_Input::STRING, // 'visible to humans username'
					$uuids['2']  => XenForo_Input::STRING, // 'visible to humans email'
					$uuids['5']  => XenForo_Input::STRING, // 'visible to humans timezone'
					$uuids['6']  => XenForo_Input::STRING, // 'visible to humans gender'
				));	
	
				$data = array(
					'username'   => $uuidData[$uuids['1']],
					'email'      =>  $uuidData[$uuids['2']],
					'timezone'   =>  $uuidData[$uuids['5']],
					'gender'     =>  $uuidData[$uuids['6']],
				);	
				return $data;
			
			}
			
			$data = $input->filter(array(
				'username'   => XenForo_Input::STRING,
				'email'      => XenForo_Input::STRING,
				'timezone'   => XenForo_Input::STRING,
				'gender'     => XenForo_Input::STRING,
			)); 
			return $data;		
		}		
		return $input;
	}
	

	public function apiRequestString($username = NULL, $email = NULL, $requestUrl, $apiName, $apiKey)
	{	
		
		//$ip_address = long2ip($this->convertIpToLong());		
		$users_ip = $_SERVER['REMOTE_ADDR'];
		
		// $users_ip = '176.119.0.17';
		// /*
		$reverse_octet_ip = $this->reverseOctet($users_ip);	
		$acpIndexNames = array('{$ip}', '{$username}', '{$email}', '{$key}', '{$reverse_octet_ip}');
		$acpIndexValues = array($users_ip, $username, $email, $apiKey, $reverse_octet_ip);		
		$apiRequestString = str_replace($acpIndexNames, $acpIndexValues, $requestUrl);
		// */
		
        // note, CHANGE THIS BEFORE GOING LIVE ************************************************************ <===
		// do this for testing 
		 /*
		$reverse_octet_ip = $this->reverseOctet('91.207.8.74');
		$acpIndexNames = array('{$ip}', '{$username}', '{$email}', '{$key}','{$reverse_octet_ip}');
		$acpIndexValuesFake = array('91.207.8.74', 'kukisis', 'zpuvtao@gmail.com', $apiKey, $reverse_octet_ip);		
		$apiRequestString = str_replace($acpIndexNames, $acpIndexValuesFake, $requestUrl);
		 */
		
		return $apiRequestString;					
	}

	
	public function reverseOctet($ip)
	{
		$ipArray = explode(".",$ip);
		return $ipArray[3].".".$ipArray[2].".".$ipArray[1].".".$ipArray[0];
	}
	
	
	public function getApiResponse($requestUrl, $dataType)
	{
		
		if($dataType == 'octet')
		{	
			try
			{
				$gethostbyname = gethostbyname($requestUrl);
				if($gethostbyname == $requestUrl)
				{
					return; 
				}	
				return $gethostbyname;	
			}			
			catch (Exception $e) 
			{			
				return;		
			}	
		}
		
		
		
		
		

		$response = "";
		try
		{
			$zend_Http_Client = new Zend_Http_Client($requestUrl);
			try
			{
				$response = $zend_Http_Client->request()->getBody();
				return $response;
			}
			catch (Zend_Http_Client_Adapter_Exception $e){}

			if($response == NULL)
			{
				$response = file_get_contents($requestUrl);
				return $response;
			}
			return 	$response;	
		}	
		catch (Exception $e) 
		{			
			return $response;		
		}		
	}
	
	
	
	public function decodeResponse($apiResponse, $dataType)
	{
		$decode = "";
		if($dataType == 'plain_text')
		{
			return $apiResponse;
		}
		
		if($dataType == 'octet')
		{
			return  explode(".", $apiResponse);
		}
		
		if($dataType == 'json')
		{
			try
			{				
				$decode = json_decode($apiResponse, true);
			}
			catch (Exception $e){}	
			if(!$decode)
			{
				try
				{									    		
	   	 			$jsonResponseIndexQuoteFix = preg_replace('/(\w+):/i', '"\1":', $apiResponse);
	    			$decode = json_decode($apiResponse,true);
				}
				catch (Exception $e){}
			}
			return 	$decode;
		}
		
		if($dataType == 'xml')
		{
			return 'AnyApi Does not yet support XML';
		}
		
					
	}	
		
	
	public function convertIpToLong($ipAddress = null)
	{
		if ($ipAddress === null)
		{
			$ipAddress = (isset($_SERVER['REMOTE_ADDR']) ? $_SERVER['REMOTE_ADDR'] : 0);
		}
		if (is_string($ipAddress) && strpos($ipAddress, '.'))
		{
			$ipAddress = ip2long($ipAddress);
		}
		return sprintf('%u', $ipAddress);
	}	
	
	
	
	public function runDailyCleanUp()
	{
		$options = XenForo_Application::get('options');
		$time_stamp = (XenForo_Application::$time) - (($options->aaResponseCache) * 24 * 60 * 60);
		$threeMonth = (XenForo_Application::$time) - (3 * 28 * 24 * 60 * 60);
		
		$rowA = $this->_getDb()->fetchRow('
			DELETE 
			FROM sf_anyapi_response_cache
			WHERE time_stamp < ?
			', $time_stamp);
		
		$rowB= $this->_getDb()->fetchRow('
			DELETE 
			FROM sf_anyapi_log
			WHERE log_date < ?
			', $threeMonth);
		
	}
	
	
	
}