<?php
/**
 * Piwik - Open source web analytics
 *
 * @link http://piwik.org
 * @license http://www.gnu.org/licenses/gpl-3.0.html GPL v3 or later
 * @version $Id: Visit.php 3571 2011-01-03 09:26:06Z matt $
 *
 * @category Piwik
 * @package Piwik
 */

/**
 * @package Piwik
 * @subpackage Piwik_Tracker
 */
interface Piwik_Tracker_Visit_Interface {
	function setRequest($requestArray);
	function handle();
}

/**
 * Class used to handle a Visit.
 * A visit is either NEW or KNOWN.
 * - If a visit is NEW then we process the visitor information (settings, referers, etc.) and save
 * a new line in the log_visit table.
 * - If a visit is KNOWN then we update the visit row in the log_visit table, updating the number of pages
 * views, time spent, etc.
 *
 * Whether a visit is NEW or KNOWN we also save the action in the DB.
 * One request to the piwik.php script is associated to one action.
 *
 * @package Piwik
 * @subpackage Piwik_Tracker
 */
class Piwik_Tracker_Visit implements Piwik_Tracker_Visit_Interface
{
	/**
	 * @var Piwik_Cookie
	 */
	protected $cookie = null;
	protected $visitorInfo = array();
	protected $userSettingsInformation = null;
	protected $idsite;
	protected $visitorKnown;
	protected $request;

	// @see detect*() referer methods
	protected $typeRefererAnalyzed;
	protected $nameRefererAnalyzed;
	protected $keywordRefererAnalyzed;
	protected $refererHost;
	protected $refererUrl;
	protected $refererUrlParse;
	protected $currentUrlParse;
	
	// can be overwritten in constructor
	protected $timestamp;
	protected $ipString;

	public function __construct($forcedIpString = null, $forcedDateTime = null)
	{
		$this->timestamp = time();
		if(!empty($forcedDateTime))
		{
			$this->timestamp = strtotime($forcedDateTime);
		}
		$ipString = $forcedIpString;
		if(empty($ipString))
		{
			$ipString = Piwik_Common::getIpString();
		}
		
		$this->ipString = Piwik_Common::getIp($ipString);
	}
	
	function setRequest($requestArray)
	{
		$this->request = $requestArray;

		$idsite = Piwik_Common::getRequestVar('idsite', 0, 'int', $this->request);
		Piwik_PostEvent('Tracker.setRequest.idSite', $idsite);
		if($idsite <= 0)
		{
			$idsite = Piwik_Common::getRequestVar('idsite', '', 'string', $this->request);
			Piwik_Tracker_ExitWithException(new Exception('Invalid idsite: '.$idsite));
		}
		$this->idsite = $idsite;
		
		// When the 'url' and referer url parameter are not given, we might be in the 'Simple Image Tracker' mode.
		// The URL can default to the Referer, which will be in this case 
		// the URL of the page containing the Simple Image beacon
		if(empty($this->request['urlref'])
			&& empty($this->request['url']))
		{
			$this->request['url'] = @$_SERVER['HTTP_REFERER'];
		}
	}

	/**
	 *	Main algorithm to handle the visit.
	 *
	 *  Once we have the visitor information, we have to determine if the visit is a new or a known visit.
	 *
	 * 1) When the last action was done more than 30min ago,
	 * 	  or if the visitor is new, then this is a new visit.
	 *
	 * 2) If the last action is less than 30min ago, then the same visit is going on.
	 *	Because the visit goes on, we can get the time spent during the last action.
	 *
	 * NB:
	 *  - In the case of a new visit, then the time spent
	 *	during the last action of the previous visit is unknown.
	 *
	 *	- In the case of a new visit but with a known visitor,
	 *	we can set the 'returning visitor' flag.
	 *
	 * In all the cases we set a cookie to the visitor with the new information.
	 */
	public function handle()
	{
		// the IP is needed by isExcluded() and GoalManager->recordGoals()
		$this->visitorInfo['location_ip'] = $this->ipString;
		
		if($this->isExcluded())
		{
			return;
		}
		
		$goalManager = new Piwik_Tracker_GoalManager();
		$someGoalsConverted = false;
		$actionUrlId = 0;
		$action = null;

		$idGoal = Piwik_Common::getRequestVar('idgoal', 0, 'int', $this->request);
		$requestIsManualGoalConversion = ($idGoal > 0);
		// this request is from the JS call to piwikTracker.trackGoal()
		if($requestIsManualGoalConversion)
		{
			$someGoalsConverted = $goalManager->detectGoalId($this->idsite, $idGoal, $this->request);
			// if we find a idgoal in the URL, but then the goal is not valid, this is most likely a fake request
			if(!$someGoalsConverted)
			{
				printDebug('Invalid goal tracking request for goal id = '.$idGoal);
				unset($goalManager);
				return;
			}
		}
		// normal page view, potentially triggering a URL matching goal
		else
		{
			$action = $this->newAction();
			$this->handleAction($action);
			$someGoalsConverted = $goalManager->detectGoalsMatchingUrl($this->idsite, $action);

			$action->loadIdActionNameAndUrl();
			$actionUrlId = $action->getIdActionUrl();
		}

		// the visitor and session
		$this->recognizeTheVisitor();

		$isLastActionInTheSameVisit = $this->isLastActionInTheSameVisit();

		// Known visit when:
		// ( - the visitor has the Piwik cookie with the idcookie ID used by Piwik to match the visitor
		//   OR
		//   - the visitor doesn't have the Piwik cookie but could be match using heuristics @see recognizeTheVisitor()
		// )
		// AND
		// - the last page view for this visitor was less than 30 minutes ago @see isLastActionInTheSameVisit()
		if( $this->isVisitorKnown()
			&& $isLastActionInTheSameVisit)
		{
			$idActionReferer = $this->visitorInfo['visit_exit_idaction_url'];
			try {
				$this->handleKnownVisit($actionUrlId, $someGoalsConverted);
				if(!is_null($action))
				{
					$action->record( 	$this->visitorInfo['idvisit'],
										$idActionReferer,
										$this->visitorInfo['time_spent_ref_action']
								);
				}
			} catch(Piwik_Tracker_Visit_VisitorNotFoundInDatabase $e) {
				
				// There is an edge case when:
				// - two manual goal conversions happen in the same second
				// - which result in handleKnownVisit throwing the exception 
				//   because the UPDATE didn't affect any rows (one row was found, but not updated since no field changed)
				// - the exception is caught here and will result in a new visit incorrectly
				// In this case, we cancel the current conversion to be recorded:
				if($requestIsManualGoalConversion)
				{
					$someGoalsConverted = false;
				}
				// When the row wasn't found in the logs, and this is a pageview or
				// goal matching URL, we force a new visitor  
				else
				{
					$this->visitorKnown = false;
				}
			}
		}

		// New visit when:
		// - the visitor has the Piwik cookie but the last action was performed more than 30 min ago @see isLastActionInTheSameVisit()
		// - the visitor doesn't have the Piwik cookie, and couldn't be matched in @see recognizeTheVisitor()
		// - the visitor does have the Piwik cookie but the idcookie and idvisit found in the cookie didn't match to any existing visit in the DB
		if(!$this->isVisitorKnown()
			|| !$isLastActionInTheSameVisit)
		{
			$this->handleNewVisit($actionUrlId, $someGoalsConverted);
				if(!is_null($action))
			{
				$action->record( $this->visitorInfo['idvisit'], 0, 0 );
			}
		}

		// update the cookie with the new visit information
		$this->updateCookie();

		// record the goals if applicable
		if($someGoalsConverted)
		{
			$goalManager->setCookie($this->cookie);
			$goalManager->recordGoals( $this->idsite, $this->visitorInfo, $action);
		}
		unset($goalManager);
		unset($action);
		$this->printCookie();
		
	}
	
	protected function printCookie()
	{
		printDebug("<pre>");
		printDebug($this->cookie);
		printDebug("</pre>");
	}

	protected function handleAction($action)
	{
		$action->setIdSite($this->idsite);
		$action->setRequest($this->request);
		$action->init();
		if($this->detectActionIsOutlinkOnAliasHost($action))
		{
			printDebug("The outlink's URL host is one  of the known host for this website. We don't record this click.");
			return;
		}
		if(isset($GLOBALS['PIWIK_TRACKER_DEBUG']) && $GLOBALS['PIWIK_TRACKER_DEBUG'])
		{
			switch($action->getActionType()) {
				case Piwik_Tracker_Action::TYPE_ACTION_URL:
					$type = "normal page view";
					break;
				case Piwik_Tracker_Action::TYPE_DOWNLOAD:
					$type = "download";
					break;
				case Piwik_Tracker_Action::TYPE_OUTLINK:
					$type = "outlink";
					break;
			}
			printDebug("Detected action <u>$type</u>,
						Action name: ". $action->getActionName() . ",
						Action URL = ". $action->getActionUrl() );
		}
	}
	/**
	 * In the case of a known visit, we have to do the following actions:
	 *
	 * 1) Insert the new action
	 * 2) Update the visit information
	 *
	 * This method triggers two events:
	 *
	 * Tracker.knownVisitorUpdate is triggered before the visit information is updated
	 * Event data is an array with the values to be updated (could be changed by plugins)
	 *
	 * Tracker.knownVisitorInformation is triggered after saving the new visit data
	 * Even data is an array with updated information about the visit
	 */
	protected function handleKnownVisit($actionUrlId, $someGoalsConverted)
	{
		// gather information that needs to be updated
		$valuesToUpdate = array();
		if($someGoalsConverted)
		{
			$valuesToUpdate['visit_goal_converted'] = 1;
		}

		$sqlActionUpdate = '';
		if(!empty($actionUrlId))
		{
			$valuesToUpdate['visit_exit_idaction_url'] = $actionUrlId;
			$sqlActionUpdate = "visit_total_actions = visit_total_actions + 1, ";
		}

		$serverTimestamp 	= $this->getCurrentTimestamp();
		$datetimeServer = Piwik_Tracker::getDatetimeFromTimestamp($serverTimestamp);
		printDebug("Visit known. Current date is ".$datetimeServer);

		$visitTotalTime = $this->getCurrentTimestamp() - $this->visitorInfo['visit_first_action_time'];
		$valuesToUpdate['visit_last_action_time'] = $datetimeServer;
		$valuesToUpdate['visit_total_time'] = $visitTotalTime;

		// trigger event before update
		Piwik_PostEvent('Tracker.knownVisitorUpdate', $valuesToUpdate);
		
		// Will be updated in cookie
		$timeSpentRefererAction = $serverTimestamp - $this->visitorInfo['visit_last_action_time'];
		if($timeSpentRefererAction > Piwik_Tracker_Config::getInstance()->Tracker['visit_standard_length'])
		{
			$timeSpentRefererAction = 0;
		}
		$this->visitorInfo['time_spent_ref_action'] = $timeSpentRefererAction;
		
		// update visitorInfo
		foreach($valuesToUpdate AS $name => $value)
		{
			$this->visitorInfo[$name] = $value;
		}
		
		// build sql query
		$updateParts = $sqlBind = array();

		foreach($valuesToUpdate AS $name => $value)
		{
			$updateParts[] = $name." = ?";
			$sqlBind[] = $value;
		}

		$sqlQuery = "/* SHARDING_ID_SITE = ". $this->idsite ." */
						UPDATE ". Piwik_Common::prefixTable('log_visit')."
						SET $sqlActionUpdate ".implode($updateParts, ', ')."
						WHERE idsite = ?
							AND idvisit = ?
							AND visitor_idcookie = ?";
		array_push($sqlBind, $this->idsite, $this->visitorInfo['idvisit'], $this->visitorInfo['visitor_idcookie'] );
		$result = Piwik_Tracker::getDatabase()->query($sqlQuery, $sqlBind);

		printDebug('Updating visitor with idvisit='.$this->visitorInfo['idvisit'].', setting visit_last_action_time='.$datetimeServer.' and visit_total_time='.$visitTotalTime);
		
		$this->visitorInfo['visit_last_action_time'] = $serverTimestamp;
		if(Piwik_Tracker::getDatabase()->rowCount($result) == 0)
		{
			printDebug("Visitor with this idcookie and idvisit wasn't found in the DB.");
			throw new Piwik_Tracker_Visit_VisitorNotFoundInDatabase(
						"The visitor with visitor_idcookie=".$this->visitorInfo['visitor_idcookie']." and idvisit=".$this->visitorInfo['idvisit']
						." wasn't found in the DB, we fallback to a new visitor");
		}

		Piwik_PostEvent('Tracker.knownVisitorInformation', $this->visitorInfo);
	}

	/**
	 * In the case of a new visit, we have to do the following actions:
	 *
	 * 1) Insert the new action
	 *
	 * 2) Insert the visit information
	 */
	protected function handleNewVisit($actionUrlId, $someGoalsConverted)
	{
		printDebug("New Visit.");

		$localTimes = array(
			'h' => (string) Piwik_Common::getRequestVar( 'h', $this->getCurrentDate("H"), 'int', $this->request),
			'i' => (string) Piwik_Common::getRequestVar( 'm', $this->getCurrentDate("i"), 'int', $this->request),
			's' => (string) Piwik_Common::getRequestVar( 's', $this->getCurrentDate("s"), 'int', $this->request)
		);
		foreach($localTimes as $k => $time) 
		{
			if(strlen($time) == 1) 
			{
				$localTimes[$k] = '0' . $time;
			}
		}
		$localTime = $localTimes['h'] .':'. $localTimes['i'] .':'. $localTimes['s'];
		
		$serverTimestamp 	= $this->getCurrentTimestamp();

		$idcookie = $this->getVisitorIdcookie();
		$returningVisitor = $this->isVisitorKnown() ? 1 : 0;

		$defaultTimeOnePageVisit = Piwik_Tracker_Config::getInstance()->Tracker['default_time_one_page_visit'];

		$userInfo = $this->getUserSettingsInformation();
		$country = Piwik_Common::getCountry($userInfo['location_browser_lang'], 
											$enableLanguageToCountryGuess = Piwik_Tracker_Config::getInstance()->Tracker['enable_language_to_country_guess'], 
											$this->getVisitorIp());
		$refererInfo = $this->getRefererInformation();

		/**
		 * Save the visitor
		 */
		$this->visitorInfo = array(
			'idsite' 					=> $this->idsite,
			'visitor_localtime' 		=> $localTime,
			'visitor_idcookie' 			=> $idcookie,
			'visitor_returning' 		=> $returningVisitor,
			'visit_server_date'     	=> $this->getCurrentDate(),
			'visit_first_action_time' 	=> Piwik_Tracker::getDatetimeFromTimestamp($serverTimestamp),
			'visit_last_action_time' 	=> Piwik_Tracker::getDatetimeFromTimestamp($serverTimestamp),
			'visit_entry_idaction_url' 	=> $actionUrlId,
			'visit_exit_idaction_url' 	=> $actionUrlId,
			'visit_total_actions' 		=> 1,
			'visit_total_time' 			=> $defaultTimeOnePageVisit,
			'visit_goal_converted'  	=> $someGoalsConverted ? 1: 0,
			'referer_type' 				=> $refererInfo['referer_type'],
			'referer_name' 				=> $refererInfo['referer_name'],
			'referer_url' 				=> Piwik_Common::unsanitizeInputValue($refererInfo['referer_url']),
			'referer_keyword' 			=> $refererInfo['referer_keyword'],
			'config_md5config' 			=> $userInfo['config_md5config'],
			'config_os' 				=> $userInfo['config_os'],
			'config_browser_name' 		=> $userInfo['config_browser_name'],
			'config_browser_version' 	=> $userInfo['config_browser_version'],
			'config_resolution' 		=> $userInfo['config_resolution'],
			'config_pdf' 				=> $userInfo['config_pdf'],
			'config_flash' 				=> $userInfo['config_flash'],
			'config_java' 				=> $userInfo['config_java'],
			'config_director' 			=> $userInfo['config_director'],
			'config_quicktime'			=> $userInfo['config_quicktime'],
			'config_realplayer' 		=> $userInfo['config_realplayer'],
			'config_windowsmedia' 		=> $userInfo['config_windowsmedia'],
			'config_gears'	 			=> $userInfo['config_gears'],
			'config_silverlight'		=> $userInfo['config_silverlight'],
			'config_cookie' 			=> $userInfo['config_cookie'],
			'location_ip' 				=> $this->getVisitorIp(),
			'location_browser_lang'		=> $userInfo['location_browser_lang'],
			'location_country' 			=> $country,
		);

		Piwik_PostEvent('Tracker.newVisitorInformation', $this->visitorInfo);

		$this->saveVisitorInformation();
	}

	/**
	 * Save new visitor information to log_visit table.
	 * Provides pre- and post- event hooks (Tracker.saveVisitorInformation and Tracker.saveVisitorInformation.end) for plugins
	 */
	protected function saveVisitorInformation()
	{
		Piwik_PostEvent('Tracker.saveVisitorInformation', $this->visitorInfo);


		$this->visitorInfo['location_continent'] = Piwik_Common::getContinent( $this->visitorInfo['location_country'] );
		$this->visitorInfo['location_browser_lang'] = substr($this->visitorInfo['location_browser_lang'], 0, 20);
		$this->visitorInfo['referer_name'] = substr($this->visitorInfo['referer_name'], 0, 70);
		$this->visitorInfo['referer_keyword'] = substr($this->visitorInfo['referer_keyword'], 0, 255);
		$this->visitorInfo['config_resolution'] = substr($this->visitorInfo['config_resolution'], 0, 9);

		$fields = implode(", ", array_keys($this->visitorInfo));
		$values = substr(str_repeat( "?,",count($this->visitorInfo)),0,-1);

		printDebug($this->visitorInfo);
		Piwik_Tracker::getDatabase()->query( "INSERT INTO ".Piwik_Common::prefixTable('log_visit').
						" ($fields) VALUES ($values)", array_values($this->visitorInfo));

		$idVisit = Piwik_Tracker::getDatabase()->lastInsertId();
		$this->visitorInfo['idvisit'] = $idVisit;

		$serverTimestamp 	= $this->getCurrentTimestamp();
		$this->visitorInfo['visit_first_action_time'] = $serverTimestamp;
		$this->visitorInfo['visit_last_action_time'] = $serverTimestamp;

		Piwik_PostEvent('Tracker.saveVisitorInformation.end', $this->visitorInfo);
	}

	/**
	 *  Returns vistor cookie
	 *
	 *  @return string
	 */
	protected function getVisitorIdcookie()
	{
		if($this->isVisitorKnown())
		{
			$idcookie = $this->visitorInfo['visitor_idcookie'];
		}
		else
		{
			$idcookie = $this->getVisitorUniqueId();
		}

		return $idcookie;
	}

	/**
	 * Returns the visitor's IP address
	 *
	 * @return long
	 */
	protected function getVisitorIp()
	{
		return $this->visitorInfo['location_ip'];
	}
	

	/**
	 * Returns the visitor's browser (user agent)
	 *
	 * @return string
	 */
	protected function getUserAgent()
	{
		return @$_SERVER['HTTP_USER_AGENT'];
	}

	/**
	 * Returns the current date in the "Y-m-d" PHP format
	 *
	 * @return string
	 */
	protected function getCurrentDate( $format = "Y-m-d")
	{
		return date($format, $this->getCurrentTimestamp() );
	}

	/**
	 * Returns the current Timestamp
	 *
	 * @return int
	 */
	protected function getCurrentTimestamp()
	{
		return $this->timestamp;
	}

	/**
	 * Test if the current visitor is excluded from the statistics.
	 *
	 * Plugins can for example exclude visitors based on the
	 * - IP
	 * - If a given cookie is found
	 *
	 * @return bool True if the visit must not be saved, false otherwise
	 */
	protected function isExcluded()
	{
		$excluded = false;
		
		$ip = $this->getVisitorIp();
		$ua = $this->getUserAgent();

		/*
		 * Live/Bing/MSN bot and Googlebot are evolving to detect cloaked websites.
		 * As a result, these sophisticated bots exhibit characteristics of
		 * browsers (cookies enabled, executing JavaScript, etc).
		 */
		$dotIp = long2ip($ip);
		if (strpos($dotIp, '65.55') === 0						// Live/Bing
				|| strpos($dotIp, '207.46') === 0				// MSN
				|| strpos($ua, 'bingbot') !== false				// Bingbot
				|| strpos($ua, 'Googlebot') !== false			// Googlebot
				|| strpos($ua, 'Google Web Preview') !== false)	// Google Instant
		{
			printDebug('Search bot detected, visit excluded');
			$excluded = true;
		}
				  
				  
		/*
		 * Requests built with piwik.js will contain a rec=1 parameter. This is used as
		 * an indication that the request is made by a JS enabled device. By default, Piwik 
		 * doesn't track non-JS visitors.
		 */
		if(!$excluded)
		{
			$parameterForceRecord = 'rec';
			$toRecord = Piwik_Common::getRequestVar($parameterForceRecord, false, 'int', $this->request);
			if(!$toRecord)
			{
				printDebug($_SERVER['REQUEST_METHOD'].' parameter '.$parameterForceRecord.' not found in URL, request excluded');
				$excluded = true;
			}
		}
		
		/* custom filters can override the built-in filters above */
		Piwik_PostEvent('Tracker.Visit.isExcluded', $excluded);
		
		/* 
		 * Following exclude operations happen after the hook. 
		 * These are of higher priority and should not be overwritten by plugins.
		 */ 
		
		// Checking if the Piwik ignore cookie is set
		if(!$excluded)
		{
			$excluded = $this->isIgnoreCookieFound();
		}
		
		// Checking for excluded IPs
		if(!$excluded)
		{
			$excluded = $this->isVisitorIpExcluded($ip);
		}
		
		if($excluded)
		{
			printDebug("Visitor excluded.");
			return true;
		}

		return false;
	}
	
	/**
	 * Looks for the ignore cookie that users can set in the Piwik admin screen.
	 * @return bool
	 */
	protected function isIgnoreCookieFound()
	{
		if(Piwik_Tracker_Cookie::isIgnoreCookieFound())
		{
			printDebug('Piwik ignore cookie was found, visit not tracked.');
			return true;
		}
		return false;
	}

	/**
	 * Checks if the visitor ip is in the excluded list
	 * 
	 * @param $ip Long IP
	 * @return bool
	 */
	protected function isVisitorIpExcluded($ip)
	{
		$websiteAttributes = Piwik_Common::getCacheWebsiteAttributes( $this->idsite );
		if(!empty($websiteAttributes['excluded_ips']))
		{
			foreach($websiteAttributes['excluded_ips'] as $ipRange)
			{
				if($ip >= $ipRange[0]
					&& $ip <= $ipRange[1])
				{
					printDebug('Visitor IP '.long2ip($ip).' is excluded from being tracked');
					return true;
				}
			}
		}
		return false;
	}
	
	/**
	 * Returns the cookie name used for the Piwik Tracker cookie
	 *
	 * @return string
	 */
	protected function getCookieName()
	{
		return Piwik_Tracker_Config::getInstance()->Tracker['cookie_name'];
	}

	/**
	 * Returns the cookie expiration date.
	 *
	 * @return int
	 */
	protected function getCookieExpire()
	{
		return $this->getCurrentTimestamp() + Piwik_Tracker_Config::getInstance()->Tracker['cookie_expire'];
	}
	
	/**
	 * Returns cookie path
	 * 
	 * @return string
	 */
	protected function getCookiePath()
	{
		return Piwik_Tracker_Config::getInstance()->Tracker['cookie_path'];
	}

	/**
	 * This methods tries to see if the visitor has visited the website before.
	 *
	 * We have to split the visitor into one of the category
	 * - Known visitor
	 * - New visitor
	 *
	 * A known visitor is a visitor that has already visited the website in the current month.
	 * We define a known visitor using the algorithm:
	 *
	 * 1) Checking if a cookie contains
	 * 		// a unique id for the visitor
	 * 		- id_visitor
	 *
	 * 		// the timestamp of the last action in the most recent visit
	 * 		- timestamp_last_action
	 *
 	 *  	// the timestamp of the first action in the most recent visit
	 * 		- timestamp_first_action
	 *
	 * 		// the ID of the most recent visit (which could be in the past or the current visit)
	 * 		- id_visit
	 *
	 * 		// the ID of the most recent action
	 * 		- id_last_action
	 *
	 * 2) If the visitor doesn't have a cookie, we try to look for a similar visitor configuration.
	 * 	  We search for a visitor with the same plugins/OS/Browser/Resolution for today for this website.
	 */
	protected function recognizeTheVisitor()
	{
		$this->visitorKnown = false;
		$this->setCookie( new Piwik_Cookie( 
								$this->getCookieName(), 
								$this->getCookieExpire(), 
								$this->getCookiePath(),
								$key = $this->idsite ) );

		$this->printCookie();
		
		/*
		 * Case the visitor has the piwik cookie.
		 * We make sure all the data that should saved in the cookie is available.
		 */
		if( false !== ($idVisitor = $this->cookie->get( Piwik_Tracker::COOKIE_INDEX_IDVISITOR )) )
		{
			$timestampLastAction = $this->cookie->get( Piwik_Tracker::COOKIE_INDEX_TIMESTAMP_LAST_ACTION );
			$timestampFirstAction = $this->cookie->get( Piwik_Tracker::COOKIE_INDEX_TIMESTAMP_FIRST_ACTION );
			$idVisit = $this->cookie->get( Piwik_Tracker::COOKIE_INDEX_ID_VISIT );
			$idLastAction = $this->cookie->get( Piwik_Tracker::COOKIE_INDEX_ID_LAST_ACTION );

			if(		$timestampLastAction !== false && is_numeric($timestampLastAction)
				&& 	$timestampFirstAction !== false && is_numeric($timestampFirstAction)
				&& 	$idVisit !== false && is_numeric($idVisit)
				&& 	$idLastAction !== false && is_numeric($idLastAction)
			)
			{
				$this->visitorInfo['visitor_idcookie'] = $idVisitor;
				$this->visitorInfo['visit_last_action_time'] = $timestampLastAction;
				$this->visitorInfo['visit_first_action_time'] = $timestampFirstAction;
				$this->visitorInfo['idvisit'] = $idVisit;
				$this->visitorInfo['visit_exit_idaction_url'] = $idLastAction;

				$this->visitorKnown = true;

				printDebug("The visitor is known because he has the piwik cookie (idcookie = {$this->visitorInfo['visitor_idcookie']}, idvisit = {$this->visitorInfo['idvisit']}, last action = ".date("r", $this->visitorInfo['visit_last_action_time']).") ");
			}
			else
			{
				printDebug("Visitor has the piwik cookie, but some values are invalid: ts first action = $timestampLastAction, ts last action = $timestampFirstAction, idvisit = $idVisit, id last action = $idLastAction.");
			}
		}
		else
		{
			printDebug("Visitor doesn't have the piwik cookie (id=".$idVisitor);
		}

		/*
		 * If the visitor doesn't have the piwik cookie, we look for a visitor
		 * that has exactly the same configuration and that visited the website today.
		 */
		if( !$this->visitorKnown
			&& Piwik_Tracker_Config::getInstance()->Tracker['enable_detect_unique_visitor_using_settings'])
		{
			$userInfo = $this->getUserSettingsInformation();
			$md5Config = $userInfo['config_md5config'];

			$visitRow = Piwik_Tracker::getDatabase()->fetch(
										" SELECT  	visitor_idcookie,
													visit_last_action_time,
													visit_first_action_time,
													idvisit,
													visit_exit_idaction_url
										FROM ".Piwik_Common::prefixTable('log_visit').
										" WHERE visit_server_date = ?
											AND idsite = ?
											AND config_md5config = ?
										ORDER BY visit_last_action_time DESC
										LIMIT 1",
										array( $this->getCurrentDate(), $this->idsite, $md5Config));
			if($visitRow
				&& count($visitRow) > 0)
			{
				$this->visitorInfo['visitor_idcookie'] = $visitRow['visitor_idcookie'];
				$this->visitorInfo['visit_last_action_time'] = strtotime($visitRow['visit_last_action_time']);
				$this->visitorInfo['visit_first_action_time'] = strtotime($visitRow['visit_first_action_time']);
				$this->visitorInfo['idvisit'] = $visitRow['idvisit'];
				$this->visitorInfo['visit_exit_idaction_url'] = $visitRow['visit_exit_idaction_url'];

				$this->visitorKnown = true;

				printDebug("The visitor is known because of his userSettings+IP (idcookie = {$visitRow['visitor_idcookie']}, idvisit = {$this->visitorInfo['idvisit']}, last action = ".date("r", $this->visitorInfo['visit_last_action_time']).", first action = ".date("r", $this->visitorInfo['visit_first_action_time']) .")");
			}
		}
	}

	/**
	 * Gets the UserSettings information and returns them in an array of name => value
	 *
	 * @return array
	 */
	protected function getUserSettingsInformation()
	{
		// we already called this method before, simply returns the result
		if(is_array($this->userSettingsInformation))
		{
			return $this->userSettingsInformation;
		}
		require_once PIWIK_INCLUDE_PATH . '/libs/UserAgentParser/UserAgentParser.php';

		$plugin_Flash 			= Piwik_Common::getRequestVar( 'fla', 0, 'int', $this->request);
		$plugin_Java 			= Piwik_Common::getRequestVar( 'java', 0, 'int', $this->request);
		$plugin_Director 		= Piwik_Common::getRequestVar( 'dir', 0, 'int', $this->request);
		$plugin_Quicktime		= Piwik_Common::getRequestVar( 'qt', 0, 'int', $this->request);
		$plugin_RealPlayer 		= Piwik_Common::getRequestVar( 'realp', 0, 'int', $this->request);
		$plugin_PDF 			= Piwik_Common::getRequestVar( 'pdf', 0, 'int', $this->request);
		$plugin_WindowsMedia 	= Piwik_Common::getRequestVar( 'wma', 0, 'int', $this->request);
		$plugin_Gears			= Piwik_Common::getRequestVar( 'gears', 0, 'int', $this->request);
		$plugin_Silverlight		= Piwik_Common::getRequestVar( 'ag', 0, 'int', $this->request);
		$plugin_Cookie 			= Piwik_Common::getRequestVar( 'cookie', 0, 'int', $this->request);

		$userAgent		= Piwik_Common::sanitizeInputValues($this->getUserAgent());
		$aBrowserInfo	= UserAgentParser::getBrowser($userAgent);

		$browserName	= ($aBrowserInfo !== false && $aBrowserInfo['id'] !== false) ? $aBrowserInfo['id'] : 'UNK';
		$browserVersion	= ($aBrowserInfo !== false && $aBrowserInfo['version'] !== false) ? $aBrowserInfo['version'] : '';

		$os				= UserAgentParser::getOperatingSystem($userAgent);
		$os				= $os === false ? 'UNK' : $os['id'];

		$resolution		= Piwik_Common::getRequestVar('res', 'unknown', 'string', $this->request);

		$browserLang	= Piwik_Common::getBrowserLanguage();

		$configurationHash = $this->getConfigHash(
												$os,
												$browserName,
												$browserVersion,
												$resolution,
												$plugin_Flash,
												$plugin_Java,
												$plugin_Director,
												$plugin_Quicktime,
												$plugin_RealPlayer,
												$plugin_PDF,
												$plugin_WindowsMedia,
												$plugin_Gears,
												$plugin_Silverlight,
												$plugin_Cookie,
												$this->getVisitorIp(),
												$browserLang);

		$this->userSettingsInformation = array(
			'config_md5config' => $configurationHash,
			'config_os' 			=> $os,
			'config_browser_name' 	=> $browserName,
			'config_browser_version' => $browserVersion,
			'config_resolution' 	=> $resolution,
			'config_pdf' 			=> $plugin_PDF,
			'config_flash' 			=> $plugin_Flash,
			'config_java' 			=> $plugin_Java,
			'config_director' 		=> $plugin_Director,
			'config_quicktime' 		=> $plugin_Quicktime,
			'config_realplayer' 	=> $plugin_RealPlayer,
			'config_windowsmedia' 	=> $plugin_WindowsMedia,
			'config_gears'	 		=> $plugin_Gears,
			'config_silverlight'	=> $plugin_Silverlight,
			'config_cookie' 		=> $plugin_Cookie,
			'location_browser_lang' => $browserLang,
		);

		return $this->userSettingsInformation;
	}

	/**
	 * Returns true if the last action was done during the last 30 minutes
	 * @return bool
	 */
	protected function isLastActionInTheSameVisit()
	{
		return isset($this->visitorInfo['visit_last_action_time'])
					&& ($this->visitorInfo['visit_last_action_time']
						> ($this->getCurrentTimestamp() - Piwik_Tracker_Config::getInstance()->Tracker['visit_standard_length']));
	}

	/**
	 * Returns true if the recognizeTheVisitor() method did recognize the visitor
	 */
	protected function isVisitorKnown()
	{
		return $this->visitorKnown === true;
	}


	/**
	 * Update the cookie information.
	 */
	protected function updateCookie()
	{
		printDebug("We manage the cookie...");

		if( isset($this->visitorInfo['referer_type'])
			&& $this->visitorInfo['referer_type'] != Piwik_Common::REFERER_TYPE_DIRECT_ENTRY)
		{
			// if the setting is set to use only the first referer,
			// we don't update the cookie referer values if they are already set
			if( !Piwik_Tracker_Config::getInstance()->Tracker['use_first_referer_to_determine_goal_referer']
				|| $this->cookie->get( Piwik_Tracker::COOKIE_INDEX_REFERER_TYPE ) == false)
			{
				$this->cookie->set( Piwik_Tracker::COOKIE_INDEX_REFERER_TYPE, $this->visitorInfo['referer_type']);
				$this->cookie->set( Piwik_Tracker::COOKIE_INDEX_REFERER_NAME, $this->visitorInfo['referer_name']);
				$this->cookie->set( Piwik_Tracker::COOKIE_INDEX_REFERER_KEYWORD, $this->visitorInfo['referer_keyword']);
				$this->cookie->set( Piwik_Tracker::COOKIE_INDEX_REFERER_ID_VISIT, $this->visitorInfo['idvisit']);
				$this->cookie->set( Piwik_Tracker::COOKIE_INDEX_REFERER_TIMESTAMP, $this->getCurrentTimestamp());
			}
		}

		// idcookie has been generated in handleNewVisit or we simply propagate the old value
		$this->cookie->set( 	Piwik_Tracker::COOKIE_INDEX_IDVISITOR,
								$this->visitorInfo['visitor_idcookie'] );

		// the last action timestamp is the current timestamp
		$this->cookie->set( 	Piwik_Tracker::COOKIE_INDEX_TIMESTAMP_LAST_ACTION,
								$this->visitorInfo['visit_last_action_time'] );

		// the first action timestamp is the timestamp of the first action of the current visit
		$this->cookie->set( 	Piwik_Tracker::COOKIE_INDEX_TIMESTAMP_FIRST_ACTION,
								$this->visitorInfo['visit_first_action_time'] );

		// the idvisit has been generated by mysql in handleNewVisit or simply propagated here
		$this->cookie->set( 	Piwik_Tracker::COOKIE_INDEX_ID_VISIT,
								$this->visitorInfo['idvisit'] );

		// the last action ID is the current exit idaction
		if(isset($this->visitorInfo['visit_exit_idaction_url'] ))
		{
			$this->cookie->set( 	Piwik_Tracker::COOKIE_INDEX_ID_LAST_ACTION,
								$this->visitorInfo['visit_exit_idaction_url'] );
		}

		// for a new visit, we flag the visit with visitor_returning
		if(isset($this->visitorInfo['visitor_returning']))
		{
			$this->cookie->set( Piwik_Tracker::COOKIE_INDEX_VISITOR_RETURNING,
								$this->visitorInfo['visitor_returning'] );
		}

		$this->cookie->save();
	}

	/**
	 * Returns an object able to handle the current action
	 * Plugins can return an override Action that for example, does not record the action in the DB
	 *
	 * @return Piwik_Tracker_Action child or fake but with same public interface
	 */
	protected function newAction()
	{
		$action = null;
		Piwik_PostEvent('Tracker.newAction', $action);

		if(is_null($action))
		{
			$action = new Piwik_Tracker_Action();
		}
		elseif(!($action instanceof Piwik_Tracker_Action_Interface))
		{
			throw new Exception("The Action object set in the plugin must implement the interface Piwik_Tracker_Action_Interface");
		}
		return $action;
	}

	/**
	 * Returns an array containing the following information:
	 * - referer_type
	 *		- direct			-- absence of referer URL OR referer URL has the same host
	 *		- site				-- based on the referer URL
	 *		- search_engine		-- based on the referer URL
	 *		- campaign			-- based on campaign URL parameter
	 *
	 * - referer_name
	 * 		- ()
	 * 		- piwik.net			-- site host name
	 * 		- google.fr			-- search engine host name
	 * 		- adwords-search	-- campaign name
	 *
	 * - referer_keyword
	 * 		- ()
	 * 		- ()
	 * 		- my keyword
	 * 		- my paid keyword
	 * 		- ()
	 * 		- ()
	 *
	 * - referer_url : the same for all the referer types
	 *
	 */
	public function getRefererInformation()
	{
		// default values for the referer_* fields
		$this->typeRefererAnalyzed = Piwik_Common::REFERER_TYPE_DIRECT_ENTRY;
		$this->nameRefererAnalyzed = '';
		$this->keywordRefererAnalyzed = '';
		$this->refererHost = '';

		// get the urls and parse them
		$refererUrl	= Piwik_Common::getRequestVar( 'urlref', '', 'string', $this->request);
		$currentUrl	= Piwik_Common::getRequestVar( 'url', '', 'string', $this->request);

		$this->refererUrl = $refererUrl;
		$this->refererUrlParse = @parse_url(Piwik_Common::unsanitizeInputValue($refererUrl));
		$this->currentUrlParse = @parse_url(Piwik_Common::unsanitizeInputValue($currentUrl));

		if(isset($this->refererUrlParse['host']))
		{
			$this->refererHost = $this->refererUrlParse['host'];
		}

		$refererDetected = false;

		if( !empty($this->currentUrlParse['host'])
			&&	$this->detectRefererCampaign() )
		{
			$refererDetected = true;
		}

		if(!$refererDetected)
		{
			if( $this->detectRefererDirectEntry()
				|| $this->detectRefererSearchEngine() )
			{
				$refererDetected = true;
			}
		}

		if(!empty($this->refererHost)
			&& !$refererDetected)
		{
			$this->typeRefererAnalyzed = Piwik_Common::REFERER_TYPE_WEBSITE;
			$this->nameRefererAnalyzed = $this->refererHost;
		}

		$refererInformation = array(
			'referer_type' 		=> $this->typeRefererAnalyzed,
			'referer_name' 		=> $this->nameRefererAnalyzed,
			'referer_keyword' 	=> $this->keywordRefererAnalyzed,
			'referer_url' 		=> $refererUrl,
		);

		return $refererInformation;
	}

	/*
	 * Search engine detection
	 */
	protected function detectRefererSearchEngine()
	{
		$rawRefererUrl = Piwik_Common::unsanitizeInputValue($this->refererUrl);
		$searchEngineInformation = Piwik_Common::extractSearchEngineInformationFromUrl($rawRefererUrl);
		Piwik_PostEvent('Tracker.detectRefererSearchEngine', $searchEngineInformation, $rawRefererUrl);
		if($searchEngineInformation === false)
		{
			return false;
		}
		$this->typeRefererAnalyzed = Piwik_Common::REFERER_TYPE_SEARCH_ENGINE;
		$this->nameRefererAnalyzed = $searchEngineInformation['name'];
		$this->keywordRefererAnalyzed = $searchEngineInformation['keywords'];
		return true;
	}

	/*
	 * Campaign analysis
	 */
	protected function detectRefererCampaign()
	{
		if(isset($this->currentUrlParse['query']))
		{
			$campaignVariableName = Piwik_Tracker_Config::getInstance()->Tracker['campaign_var_name'];
			$campaignName = Piwik_Common::getParameterFromQueryString($this->currentUrlParse['query'], $campaignVariableName);

			if( !empty($campaignName))
			{
				$campaignKeywordVariableName = Piwik_Tracker_Config::getInstance()->Tracker['campaign_keyword_var_name'];
				$campaignKeyword = Piwik_Common::getParameterFromQueryString($this->currentUrlParse['query'], $campaignKeywordVariableName);

				$this->typeRefererAnalyzed = Piwik_Common::REFERER_TYPE_CAMPAIGN;
				$this->nameRefererAnalyzed = $campaignName;

				if(!empty($campaignKeyword))
				{
					$this->keywordRefererAnalyzed = $campaignKeyword;
				}

				return true;
			}
		}
		return false;
	}


	/*
	 * We have previously tried to detect the campaign variables in the URL
	 * so at this stage, if the referer host is the current host,
	 * or if the referer host is any of the registered URL for this website,
	 * it is considered a direct entry
	 */
	protected function detectRefererDirectEntry()
	{
		if(!empty($this->refererHost))
		{
			// is the referer host the current host?
			if(isset($this->currentUrlParse['host']))
			{
				$currentHost = $this->currentUrlParse['host'];
				if($currentHost == $this->refererHost)
				{
					$this->typeRefererAnalyzed = Piwik_Common::REFERER_TYPE_DIRECT_ENTRY;
					return true;
				}
			}
			if($this->isHostKnownAliasHost($this->refererHost))
			{
				$this->typeRefererAnalyzed = Piwik_Common::REFERER_TYPE_DIRECT_ENTRY;
				return true;
			}
		}
		return false;
	}

	/**
	 * @param $action
	 * @return bool true if the outlink the visitor clicked on points to one of the known hosts for this website
	 */
	protected function detectActionIsOutlinkOnAliasHost(Piwik_Tracker_Action_Interface $action)
	{
		if($action->getActionType() != Piwik_Tracker_Action_Interface::TYPE_OUTLINK)
		{
			return false;
		}
		$actionUrl = $action->getActionUrl();
		$actionUrlParsed = @parse_url(Piwik_Common::unsanitizeInputValue($actionUrl));
		if(!isset($actionUrlParsed['host']))
		{
			return false;
		}
		return $this->isHostKnownAliasHost($actionUrlParsed['host']);
	}

	// is the referer host any of the registered URLs for this website?
	protected function isHostKnownAliasHost($urlHost)
	{
		$websiteData = Piwik_Common::getCacheWebsiteAttributes($this->idsite);
		if(isset($websiteData['hosts']))
		{
			$canonicalHosts = array();
			foreach($websiteData['hosts'] as $host) {
				$canonicalHosts[] = str_replace('www.', '' , $host);
			}
			$canonicalHost = str_replace('www.', '', $urlHost);
			if(in_array($canonicalHost, $canonicalHosts))
			{
				return true;
			}
		}
		return false;
	}

	/**
	 * Returns a MD5 of all the configuration settings
	 * @return string
	 */
	protected function getConfigHash( $os, $browserName, $browserVersion, $resolution, $plugin_Flash, $plugin_Java, $plugin_Director, $plugin_Quicktime, $plugin_RealPlayer, $plugin_PDF, $plugin_WindowsMedia, $plugin_Gears, $plugin_Silverlight, $plugin_Cookie, $ip, $browserLang)
	{
		return md5( $os . $browserName . $browserVersion . $resolution . $plugin_Flash . $plugin_Java . $plugin_Director . $plugin_Quicktime . $plugin_RealPlayer . $plugin_PDF . $plugin_WindowsMedia . $plugin_Gears . $plugin_Silverlight . $plugin_Cookie . $ip . $browserLang );
	}

	/**
	 * Returns either
	 * - "-1" for a known visitor
	 * - a unique 32 char identifier @see Piwik_Common::generateUniqId()
	 */
	protected function getVisitorUniqueId()
	{
		if($this->isVisitorKnown())
		{
			return -1;
		}
		else
		{
			return Piwik_Common::generateUniqId();
		}
	}

	protected function setCookie( $cookie )
	{
		$this->cookie = $cookie;
	}
}

/**
 * @package Piwik
 * @subpackage Piwik_Tracker
 */
class Piwik_Tracker_Visit_VisitorNotFoundInDatabase extends Exception {
}

/**
 * @package Piwik
 * @subpackage Piwik_Tracker
 */
class Piwik_Tracker_Visit_Excluded extends Exception {
}
