8detect
7/3/2013 - 11:07 PM

Core.php

Core.php

<?php

/**
 * Bundle All Core Classes 
 * Core; Controller; Event; FxFilter
 */

namespace PHN\Core {


class Core{
  public $cfg;
	public $loader;
	public $event; //system events
	public $fxFilter; //function filters
	public $plugins;
	
	public $appName='Main'; //as default
	public $controller;
	public $controllerName='Index';
	public $controllerAction='index';
	
	
	public $callbacks;
	public $errors=array(); //error logs	
	public $logs=array('queries'=>0,'queries_fail'=>0);//debug logs

	
	/**
	 * allowed dynamic functions addon
	 * ex: $object->func = function($format) use ($object) {
			echo date($format, $object->date);
		};
	 */
	function __call($name, $arguments) 
	{
		if (isset($this->$name) && $this->$name instanceof Closure) {
			return call_user_func_array($this->$name, $arguments);
		}
	}

	function __construct($cfg=null) {
		global $_CFG,$Loader;
		
		$this->loader = &$Loader ;
		
		if (!empty($cfg)) $this->cfg = &$cfg;
		else $this->cfg = &$_CFG;
		

		$this->event = new EventDispatcher();
		$this->fxFilter = new FxFilter();
		
		$this->_initCallback();
		
	}
	
	private function _initCallback()
	{
		
		//set output gzip
		if (isset($this->cfg['output_gzip']) && $this->cfg['output_gzip']) ob_start("ob_gzhandler");
		
		//call when 404 encountered
		$this->event->addListener('404', array($this,'__error404'));
		
		//ip tracking to inform DOS attack
		//$this->event->addListener('IP_TRACKING', array($this,'__iptracking'));
		
//		register_shutdown_function(function(){
//			//if (isset( $this->cfg['debug_verbose']) && $this->cfg['debug_verbose'] ) echo "<h2>shut down signal</h2>";
//			//do batch shutdown event	
//			$this->event->dispatch('CLEANUP_USER_SESSION');
//			$this->event->dispatch('UPDATE_USER_SESSION');
//			$this->event->dispatch('IP_TRACKING');
//			$this->event->dispatch('SHUTDOWN');
//		});
		register_shutdown_function(array($this,'__shutdown'));
		
		//error custom callback
		// nếu debug_mode != 1 ( NORMAL ) thì toàn bộ warning, exception sẽ được log riêng và trình bày cuối trang 
		// hoặc ghi ra console, file , email ...
		if (isset($this->cfg['debug_mode']) && $this->cfg['debug_mode'] !=1)
		{	
			set_error_handler(array($this,'__errorHandler'));
			set_exception_handler(array($this,'__exceptionHandler'));
		}
	}
	
	// <editor-fold desc="Callback Methods" >
	
	public function __error404(){
		//echo "<h1>Error 404 Resource Not Found</h1><br/>";
		header('HTTP/1.0 404 Not Found');
	}
	

	public function __shutdown(){

		$this->event->dispatch('CLEANUP_USER_SESSION');
		$this->event->dispatch('UPDATE_USER_SESSION');		
		$this->event->dispatch('IP_TRACKING');
		$this->event->dispatch('SHUTDOWN');

		
		if (isset( $this->cfg['debug_mode']) && $this->cfg['debug_mode'] )
		{
			if (isset( $this->cfg['debug_benchmark']) && $this->cfg['debug_benchmark'])
			{
				$execute_s = (microtime(true) - START_TIME)* 1000 ;
				
				$this->logs['benchmark'] = array (
					"<b>Execute (s) :</b> " . $execute_s ,
					"<b>Memory consume : </b>" . memory_get_usage(1) . ' bytes',
					"<b>Peak memory consume : </b>" .memory_get_peak_usage (1) . ' bytes'
				);

			}
			
			echo "<hr style='clear:both'><b>Debug Information</b><br>";
			
			if (!empty($this->logs['benchmark'] ))
			{
				echo "<p style=\"margin-left:8px\"><u>Benchmark </u>:<br>";
				echo implode('<br>',$this->logs['benchmark'] );
				echo "</p>";				
			}
			
			if (!empty($this->logs['error'] ))
			{
				echo "<p style=\"margin-left:8px\"><u>Runtime Errors : </u>:<br>";
				echo implode('<br>',$this->logs['error'] );
				echo "</p>";				
			}
			
			if (!empty($this->logs['db_server_error'] ))
			{
				echo "<p style=\"margin-left:8px\"><u>Database Server Error </u>:<br>";
				echo "<b>Detail :</b> <div style='margin-left:20px;' >";
				echo implode('<br>',$this->logs['db'] );
				echo "</div></p>";	
			}	

			if (!empty($this->logs['queries'] ) && $this->cfg['debug_db'])
			{
				echo "<p style=\"margin-left:8px\"><u>Database </u>:<br>";
				echo "<b>Database connections :</b> " . count(\PHN\Model\Model::$dboCache) . "<br>";
				echo "<b>Queries :</b> " . $this->logs['queries'] . "<br>";
				if ($this->logs['queries_fail']) echo "<b style='color:red'>Queries Failed :</b> " . $this->logs['queries_fail'] . "<br>";
				
				if ($this->cfg['debug_db_dump']) {
					echo "<b>Queries Detail :</b> <div style='margin-left:20px;' >";
					echo implode('<br>',$this->logs['db'] );
				}
				echo "</div></p>";				
			}			
			
		}		
		
		
	}
	
	public function __errorHandler($errno, $errstr, $errfile, $errline) 
	{

		if (!(error_reporting() & $errno)) {
			// This error code is not included in error_reporting
			return;
		}
		
		$e = new \ErrorException($errstr,$errno,0,$errfile,$errline);
		
		$this->__exceptionHandler($e);		

		
		/* Don't execute PHP internal error handler */
		return true;
	}

	public function __exceptionHandler($e){

		/*
		 * final public string Exception::getMessage ( void )
		final public Exception Exception::getPrevious ( void )
		final public mixed Exception::getCode ( void )
		final public string Exception::getFile ( void )
		final public int Exception::getLine ( void )
		final public array Exception::getTrace ( void )
		final public string Exception::getTraceAsString ( void )
		public string Exception::__toString ( void )
		final private void Exception::__clone ( void )
		 */
		$errno= $e->getCode();
		$errstr= $e->getMessage();
		$errfile= $e->getFile();
		$errline=$e->getLine();
		$traces = $e->getTrace();
		
		if (!isset($this->logs['error'])) $this->logs['error']=array();
		$message = &$this->logs['error'];
		
		switch ($errno) {
			case E_USER_ERROR:
				$message[]= "<b>Error</b> [$errno] $errstr on line $errline in file $errfile";
				break;

			case E_USER_WARNING:
				$message[]=  "<b>Warning</b> [$errno] $errstr on line $errline in file $errfile";
				break;

			case E_USER_NOTICE:
				$message[]=  "<b>Notice</b> [$errno] $errstr on line $errline in file $errfile";
				break;

			default:
				$message[]=  "<b>Warning</b> <span style='color:red'>[$errno] $errstr </span>on line $errline in file $errfile";
				break;
		}

	}
	

	// </editor-fold >	
}



class Controller{
	
	public $app ;
	
	/**
	 * shortcut to global &$_CFG
	 * @var hash 
	 */
	public $cfg;
	
	/**
	 * only access after call $this->updateUser() <br>
	 * ua_type, ua_name, ip, session_id, user_id, username, <br>
	 * login_utime, start_utime, current_utime <br>
	 * geo_country_code2, geo_country_code3, geo_country_name, geo_lang, geo_lat, geo_long
	 * @var hash 
	 */
	public $user=array(); //user information
	
	
	/**
	 * current loaded access list control
	 * @var hash 
	 */
	public $acl=array();
	
	
	/**
	 * current loaded model
	 * @var object
	 */
	public $model;


	//caching
	static $modelCache;
	
	
	//caching
	static $aclCache;


	/**
	 * logs for debugging
	 * @var hash 
	 */
	public $logs=array();


	public function __construct(&$app){
		//echo __NAMESPACE__ .' :: ' . __CLASS__ . '<br>';
		$this->app = $app;
		$this->cfg = &$this->app->cfg;
	}
	
	public function getSrcDir(){
		return SRC_DIR;
	}
	public function getIndexDir(){
		return INDEX_DIR;
	}
	
	/**
	 * Can be called anytime
	 * sh = session hash / server hash
	 */
	public function sessionStart(){
		
		if (!isset($_SESSION)) {

			session_set_cookie_params('', '/', '.' . $this->cfg['domain'], 0, 1);
			if (!isset($_COOKIE['sh'])) {
				$hash = uniqid();
				setcookie('sh', $hash, 0, '/', '.' . $this->cfg['domain'], 0, 1);
				session_name(md5($hash));
			} else {
				session_name(md5($_COOKIE['sh']));
			}
			session_start();
		}
	}
	
	
	/**
	 * Automatically set locale based on default config / ($_SERVER['HTTP_ACCEPT_LANGUAGE']
	 * set geo graphic information to $this->user
	 */
	public function setLocale($loc=null)
	{
		$this->sessionStart();		

		//user selection
		if (!empty($loc)) $this->locale = $loc;
		
		if (!empty($_GET['loc'])) $_SESSION['loc'] = $_GET['loc'];
		
		if (empty($this->locale) && isset($_SESSION['loc'])) $this->locale=$_SESSION['loc'];
		else 
		{	
		
			//if not try with DB
			if (empty($this->locale) && isset( $this->cfg['locale_by_db']) ){

				//get from DB
				$geo=$this->getGeoDataByIP();
				$this->locale = $geo['lang'] . '_' . $geo['codetwo'];
			}

			//if not try with http header
			if (empty($this->locale) && isset( $this->cfg['locale_by_header'])  && !empty($_SERVER['HTTP_ACCEPT_LANGUAGE'])){

				//get from header
				$this->locale=locale_accept_from_http($_SERVER['HTTP_ACCEPT_LANGUAGE']);

			}		


			//default locale fallback
			if (empty($this->locale)){
				if (isset( $this->cfg['locale'])) $this->locale = $this->cfg['locale'];
				else $this->locale='en_US';
			}

		}
		//
		$parse=locale_parse($this->locale);
		$this->locale_lang = $parse['language'];
		$this->locale_country = isset($parse['region'])?$parse['region']:''; //codetwo in upper case
		
		//var_dump($this->locale,$this->cfg['locale']['textdomain'],$this->cfg['locale']['translate_dir']);
		
		putenv('LC_ALL='.$this->locale);
		setlocale(LC_ALL,$this->locale);
		
		foreach ($this->cfg['bindtextdomain'] as $key=>$value) 
		{
			bindtextdomain($key,$value);
		}
		
		//select the domain for current gettext		
		textdomain($this->cfg['textdomain']);
	}
	

	const UA_BOT=1,UA_PC=2,UA_TABLET=3,UA_MOBILE=4;
	
	/**
	 * @param bool $return_full if true then return array <b> ['type'=>BOT|TABLET|MOBILE|PC,'name'=>'UA name'] </b>
	 * else (default) return only type
	 * @param bool nocache nếu nocache true thì $_SESSION['UA'] sẽ bị xóa 
	 * @return :	PC  nhóm desktop
	 * if UA = PC thi co them $_SESSION['UA_NAME'] =  'chrome','firefox','safari','opera','ie'
	 * @return : BOT | MOBILE | TABLET nhóm mobile
	 * if UA = BOT thi co them $_SESSION['UA_NAME'] = 'google','yahoo','bot','spider','crawl','ask','archiver','baidu','msnbot'
	 * hàm dùng session để cached 
	 * 
	 */
	public function getUA($return_full=false,$nocache=false) {
		
		$this->sessionStart();
		
		$ua = strtolower($_SERVER["HTTP_USER_AGENT"]); //nen luu session
		
		if ($nocache && isset($_SESSION['UA'])) unset($_SESSION['UA']);

		if (empty($_SESSION['UA'])) 
		{
			$bots = array('google', 'yahoo', 'bot', 'spider', 'crawl', 'jeeves', 'archiver', 'baidu', 'msnbot', 'sogou', 'yandex');
			foreach ($bots as $bot) 
			{
				if (strpos($ua, $bot) !== false) {
					$_SESSION['UA'] = Controller::UA_BOT;
					$_SESSION['UA_NAME'] = $bot;
					
					if (!$return_full) return $_SESSION['UA'];
					else return array('type'=>$_SESSION['UA'],'name'=>$_SESSION['UA_NAME']);
				}
			}

			
			#is tablet ?
			//iPad vừa có iPad vừa có chữ Mobile
			//Blackberry tablet dùng Tablet
			//Amazon Kindle 600x800 dùng Kindle
			if (preg_match('#ipad#', $ua)) 
			{
				$_SESSION['UA_NAME'] = 'iPad';
				$_SESSION['UA'] = Controller::UA_TABLET;
				
				if (!$return_full) return $_SESSION['UA'];
				else return array('type'=>$_SESSION['UA'],'name'=>$_SESSION['UA_NAME']);
			}
			elseif (preg_match('#kindle#', $ua))
			{
				$_SESSION['UA_NAME'] = 'Kindle';
				$_SESSION['UA'] =Controller::UA_TABLET;	
				if (!$return_full) return $_SESSION['UA'];
				else return array('type'=>$_SESSION['UA'],'name'=>$_SESSION['UA_NAME']);				
			}
			elseif (preg_match('#tablet#', $ua))
			{
				$_SESSION['UA_NAME'] = 'General Tablet';
				
				if (strpos('RIM',$ua)) $_SESSION['UA_NAME'] = 'BlackBerry';
				
				$_SESSION['UA'] = Controller::UA_TABLET;
				
				if (!$return_full) return $_SESSION['UA'];
				else return array('type'=>$_SESSION['UA'],'name'=>$_SESSION['UA_NAME']);				
			}			
			
			#is mobile ?
			//Android nếu có Mobile thì không phải Tablet
			//windows phone : IEMobile/version
			//những thiết bị khác có mobile hoặc mobi thì chắc là mobile
			if (preg_match('#mobi#', $ua))
			{
				$_SESSION['UA_NAME'] = 'General Mobile';
				
				if  (strpos($ua,'android')) $_SESSION['UA_NAME'] = 'Android';
				elseif (strpos($ua,'iphone')) $_SESSION['UA_NAME'] = 'iPhone';
				elseif (strpos($ua,'blackberry')) $_SESSION['UA_NAME'] = 'BlackBerry';
				elseif  (strpos($ua,'msie')) $_SESSION['UA_NAME'] = 'MSIE';
				
				$_SESSION['UA'] = Controller::UA_MOBILE;

				if (!$return_full) return $_SESSION['UA'];
				else return array('type'=>$_SESSION['UA'],'name'=>$_SESSION['UA_NAME']);				
			}
			
			//nếu ở trên không có Mobile mà dưới này có Android thì => tablet
			if (preg_match('#android#', $ua)) 
			{
				$_SESSION['UA_NAME'] = 'Android';
				$_SESSION['UA'] =Controller::UA_TABLET;
				
				if (!$return_full) return $_SESSION['UA'];
				else return array('type'=>$_SESSION['UA'],'name'=>$_SESSION['UA_NAME']);				
			}	

			//else desktop browser	
			if (preg_match('#firefox#', $ua))
			{		
				$_SESSION['UA'] = Controller::UA_PC;
				$_SESSION['UA_NAME'] = 'Firefox';
			}	
			elseif (preg_match('#chrome#', $ua))
			{		
				$_SESSION['UA'] = Controller::UA_PC;
				$_SESSION['UA_NAME'] = 'Chrome';
			}
			elseif (preg_match('#msie#', $ua))
			{		
				$_SESSION['UA'] =  Controller::UA_PC;
				$_SESSION['UA_NAME'] = 'IE';
			}			
			elseif (preg_match('#safari#', $ua))
			{		
				$_SESSION['UA'] =  Controller::UA_PC;
				$_SESSION['UA_NAME'] = 'Safari';
			}
			elseif (preg_match('#opera#', $ua))
			{
				$_SESSION['UA'] =  Controller::UA_PC;
				$_SESSION['UA_NAME'] = 'Opera';
			}	
			else
				$_SESSION['UA'] = null;
		}

		if (!$return_full) return $_SESSION['UA'];
		else return array('type'=>$_SESSION['UA'],'name'=>$_SESSION['UA_NAME']);
	}

	/**
	 * tra ve cac record ve dia ly tu ip cho truoc
	 * ham nay khong tich hop trong app ma de trong CORE::routeRequest
	 * vi su dung trong 
	 * @param $ip null thi lay $_SERVER['REMOTE_ADDR']
	 * @return array|null [lang,countryname,codetwo,codethree,lat,long,registry]
	 */
	public function getGeoDataByIP($ip = null) {

		$loc = null;
		if (empty($ip)) $ip = $_SERVER['REMOTE_ADDR'];		

		if (isset($_SESSION['geo_' . $ip])) return $_SESSION['geo_' . $ip];
		
		$IPNUM = sprintf("%u", ip2long($ip));
		
		$shareDB = $this->db('share');

		$geo = $shareDB->getOne("
			SELECT rangestart,rangeend,_geo_ip_country.codetwo,_geo_ip_country.codethree,_geo_country.country as countryname,_geo_ip_country.registy as registry,
			
			_geo_country.lang,_geo_country.lat,_geo_country.long
			FROM _geo_ip_country,_geo_country
			WHERE $IPNUM BETWEEN rangestart AND rangeend
			AND _geo_ip_country.codetwo = _geo_country.codetwo
			LIMIT 1
		");

		$_SESSION['geo_' . $ip] = $geo;

		return $geo;
	}
	
	/**
	 * 
	 * @return string current Url
	 */
	public function getCurrentUrl(){
		$url =	( !isset($_SERVER["HTTPS"]) || $_SERVER["HTTPS"] != 'on' ) ? 'http://' . $_SERVER["SERVER_NAME"] : 'https://' . $_SERVER["SERVER_NAME"];
		$url .= ( $_SERVER["SERVER_PORT"] !== '80' ) ? ":" . $_SERVER["SERVER_PORT"] : "";
		$url .= $_SERVER["REQUEST_URI"];
		return $url;
	}
	


// <editor-fold defaultstate="collapsed" desc=" Authenticate && User ">
	
	/**
	 * check if auth cookie is valid
	 * @param array $param : changing param['expire'|'expire_delta','randsig','passsalt']
	 * @return true/false
	 */
	public function validateAuthCookie($param){
		
		if (isset($_COOKIE['usrsig'])){
			
			$validate = false;
			
			$parts = explode('_',$_COOKIE['usrsig']);
			
			if (count($parts) >= 3) {

				$sig = $parts[0];
				$expire = $parts[1];
				$uid = $parts[2];

				if (!$param['db']) {
					$db = $this->db();
					$db->table('user_auth');
				}
				else $db = $param['db'];

				$row=$db->getOne(array('select'=>'passalt,randsig','where'=>array('uid = ?',$uid)));
				if ($row) {
					$ua = $_SERVER['HTTP_USER_AGENT'];
					$server_sig = md5($row['randsig'] . $row['passsalt'] . $ua );
					if ($sig == $server_sig) {
						
						if (isset($param['expire'])) $expire = $param['expire'];
						elseif (isset($param['expire_delta'])) $expire +=$param['expire_delta'];
						
						//update randsig
						$randsig = md5($uid.time());
						
						$db->update(array('randsig'=>$randsig),array('uid = ?',$uid));						

						$token = md5($randsig .  $row['passsalt'] . $ua ) . '_' . $expire . '_' . $uid;

						if (isset($this->user['cookie_path'])) $path=$this->user['cookie_path']; else $path ='/';
						if (isset($this->user['cookie_domain'])) $domain=$this->user['cookie_domain']; else $domain =$this->user['domain'];

						setcookie('usrsig',$token,$expire,$path,$domain);							

						
						$validate = true;
					}
				}
				
			}
			
			
			if (!$validate) {	
				setcookie('usrsig','',time() - 3600);//remove the tained cookie
				return false;
			}
			else return true;			
			
		}
		else return false;
		
	}
	
	/**
	 * 
	 * @param type $param [username||email|password ( md5 ), 'db' ]
	 * @param mixed $msg return 401 : unauthorized, 404 user not found, $user if success ( from db )
	 *  @return true/false
	 */
	public function validateAuth($param,&$msg){
		
		if (!$param['db']) {
			$db = $this->db();
			$db->table('user_auth');
		}
		else $db = $param['db'];
		
		if (isset($param['username'])) $where = array('username = ?',$param['username']);
		elseif (isset($param['email'])) $where = array('email = ?',$param['email']);
		
		$row = $db->getOne(array(
			'select'=>"user_id,username,uid,email,passsalt,password,last_login",
			'where'=>$where
		));
		
		if ($row){
			
			$password = md5( $param['password'] . $row['username'] . $row['passsalt'] ) ;
			
			if ($password == $row['password'] ) {
				$err = $row;
				return true;
			}
			
			$msg = 401;//invalid password
			return false;
		}
		else {
			
			$msg = 404;//user not found
			return false;
		}
		
		
	}

	/**
	 * 
	 * @param array $user
	 */
	public function regAuthCookie($param){

		$uid=$param['uid'];
		$randsig = $param['randsig'];
		$passsalt = $param['passalt'];
		$expire = $param['expire'];
		if (!$expire) $expire = 1209600; // 2 weeks
		$ua = $_SERVER['HTTP_USER_AGENT'];
		
		$token = md5($randsig . $passsalt . $ua ) . '_' . $expire . '_' . $uid;
		
		if (isset($this->user['cookie_path'])) $path=$this->user['cookie_path']; else $path ='/';
		if (isset($this->user['cookie_domain'])) $domain=$this->user['cookie_domain']; else $domain =$this->user['domain'];

		setcookie('usrsig',$token,$expire,$path,$domain);
		
	}
	
	/**
	 * 
	 */
	public function isSessionExpired(){
		if (empty($this->cfg['session_expire'])) $this->cfg['session_expire'] = 1800;//30mins
		return ( (time() - $_SESSION['current_utime']) >  $this->cfg['session_expire']) ;		
	}
	
	/**
	 * check if user has logged or not.
	 * if page require login then call in the order as :
	 * if (!isLoggedIn()) {
	 *	$this->unregAuthSession();
	 *	$this->reRoute($this->cfg['route_default]['auth']); // auth controller : 
	 *	return; 
	 * }
	 * 
	 * $this->updateUser() ; //dont call updateUser before isLoggedIn() because updateUser 
	 * 
	 * @return boolean
	 */
	public function isLoggedIn(){
		if ( isset($_SESSION['login_utime']) && !$this->isSessionExpired() ) return true;
		else {
			$this->unregAuthSession();
		}
	}
	
	/**
	 * after logged in succesfully must call this to register Auth Session
	 * @param array $user=array(user_id,username)
	 */
	public function regAuthSession($user){
		$db = $this->db();
		$db->table('user_session');
		
		//changing new session_id for securing 
		$old_session_id = $this->user['session_id'];
		$session_id = session_regenerate_id();
		$this->user['session_id']=$session_id;
		
		$this->user['user_id'] = $_SESSION['uid']=$user['user_id'];
		$this->user['username'] = $_SESSION['uname']=$user['username'];		
		
		//change sesssion_id for preventing session fixation
		$db->update(array(
			'session_id'=>$session_id
		),"session_id='".$old_session_id."'",1);
		
		//create login_utime
		$this->user['login_utime']=$_SESSION['login_utime']=time();		
	}
	
	/**
	 * unregister Auth Session but still keep normal session ( guest )
	 */
	public function unregAuthSession(){
		unset($this->user['user_id'],$_SESSION['uid'],$_SESSION['login_utime']);
		//keep 'username' to notice something (relog , warning ... )
	}

	/**
	 *  update user info -> $this->user
	 * @param type $updateDB update to DB or not ( default = true )
	 * @todo nếu có if (isLoggedIn) {} thì gọi sau isLoggedIn {} 
	 * @return void
	 */
	public function updateUser($updateDB = true){		
		
		$this->getUA();		
		$this->user['ua_type'] = $_SESSION['UA'];
		$this->user['ua_name'] = $_SESSION['UA_NAME'];
		
		$this->user['ip'] = $_SERVER['REMOTE_ADDR'];
		
		$this->user['session_id'] = session_id();
		
		$this->user['user_id'] = isset($_SESSION['uid'])? $_SESSION['uid']:null;
		$this->user['username'] = isset($_SESSION['uname'])? $_SESSION['uname']:null;
		
		$this->user['login_utime'] = isset($_SESSION['login_utime'])? $_SESSION['login_utime']:null;		
		
		if (!isset($_SESSION['start_utime'])) $_SESSION['start_utime'] = time();
			
		$this->user['start_utime'] = $_SESSION['start_utime'];
		
		$current =  time();
		$_SESSION['current_utime'] = $current;
		$this->user['current_utime'] = $current;
		
		//$this->user['app'] = $this->cfg['app'];
		//$this->user['current_url'] = $_SERVER['HTTP_HOST'] .$_SERVER['REQUEST_URI'];
		
		$geo = $this->getGeoDataByIP();
		
		if (!empty($geo))
		{
			$this->user['geo_country_code2'] = isset($geo['codetwo'])?$geo['codetwo']:null;
			$this->user['geo_country_code3'] = isset($geo['codethree'])?$geo['codethree']:null;
			$this->user['geo_country_name'] = isset($geo['countryname'])?$geo['countryname']:null;
			$this->user['geo_lang'] = isset($geo['lang'])?$geo['lang'] :null;
			if (!empty($this->user['geo_country_code2'])) $this->user['geo_lang'].='_'.$this->user['geo_country_code2'];
			
			$this->user['geo_lat'] = isset($geo['lat'])?$geo['lat']:null;
			$this->user['geo_long'] = isset($geo['long'])?$geo['long']:null;
		}
		
		
		if ($updateDB){
			
			$this->app->event->addListener('UPDATE_USER_SESSION', function(){
				
				//_vd($this->user);
				$now=time();
				$db = $this->db();
				//db server error
				if (!$db) return; 
				
				$db->table('user_session');
				
				$input = array(
					'session_current_unixtime' => $now,
					'user_id' => isset($this->user['id']) ?$this->user['id']:null  ,
					'username' => isset($this->user['name']) ?$this->user['name']:null,
					'app_code' => $this->cfg['app'],
					'current_page' => $this->getCurrentUrl(),
					'hits' => array('++')
				);
				
				if (!$db->update($input,"session_id = '{$this->user['session_id']}'"))
				{
					//unset logged_session
					$this->unregAuthSession();

					$input = array_merge($input, array(
						'session_id' => $this->user['session_id'],
						'IP' => $_SERVER['REMOTE_ADDR'],
						'port' => $_SERVER['REMOTE_PORT'],
						'http_user_agent' => $_SERVER['HTTP_USER_AGENT'],
						'session_start_unixtime' => $now,
						'server' => $_SERVER['HTTP_HOST'],
						'hits' => 1,
						'geo_lang' => $this->user['geo_lang'],
						'geo_country_code2' => $this->user['geo_country_code2'],
						'geo_country_name' => $this->user['geo_country_name'],
						'geo_lat' => $this->user['geo_lat'],
						'geo_long' => $this->user['geo_long'],
						));
					
					$db->insert($input);
					
				}
			
				
				
			});
			
			$this->app->event->addListener('CLEANUP_USER_SESSION', function(){
				
				$db = $this->db();
				//db error
				if (!$db) return;

				$now = time();
				//xoa bot cac session row da het han trong vong $_CFG['session_login_expire'] unixtime
				if (!isset($_SESSION['SESSION_CLEANUP']) || ( ($now - $_SESSION['SESSION_CLEANUP'] ) > $this->cfg['session_expire'])) {
					$db->table('user_session')->delete("$now - session_current_unixtime > {$this->cfg['session_expire']} ");
					$_SESSION['SESSION_CLEANUP'] = time(); // danh dau time
				}
			});
			
		}
		
	}

	/**	
	 * ['ua_type'] ['ua_name'] 	['ip'] 	['session_id']	['user_id']['username']	['login_utime'] ['start_utime']
	['current_utime'] ['app'] ['current_url'] ['geo_country_code2'] ['geo_country_code3'] ['geo_country_name']
	['geo_lang'] ['geo_country_code2'] ['geo_lat'] ['geo_long']
	 * @return array userInfo
	 */
	public function getUser(){
		return $this->user;
	}
	
// </editor-fold>

	/**
	 * create a db
	 * @param mixed $cfg as string or array, if string then function use $_CFG['db']
	 * @return \PHN\Model\CommonDB
	 */
	public function db($cfg='default'){
		if (is_string($cfg)) $cfg = $this->cfg['db'][$cfg];
		
		try{ 
			return \PHN\Model\Model::create($this,$cfg);		
		}
		catch (\Exception $e){
			return null;
		}
	}

	/**
	 * load a model, save to $this->model and return $this->model
	 * @param string $class to load
	 * @return \PHN\Model\Model	obj
	 */
	public function model($class=null){
		
		//auto get model by this controller name
		if (empty($class)) $class = $this->app->controllerName.'Model';
		
		if (isset(self::$modelCache[$class])) return $this->model=&self::$modelCache[$class];
		
		$name='\\'.$this->app->appName.'\\Model\\'.$class;
		self::$modelCache[$class] = new $name($this);
		return $this->model=&self::$modelCache[$class];
	}
	

	/**
	 * create acl object
	 * @param string|array $cfg
	 * @return \PHN\Model\Acl acl 
	 */
	public function acl($cfg=null){

		if (empty($cfg)) {

			if ($this->app->appName != 'Main') $cfg=$this->app->appName.'/'.'global';
			else $cfg = 'global';
		}

		if ( is_string($cfg)){	
			
			$cacheKey = $cfg;
			
			if (isset(self::$aclCache[$cacheKey])) return self::$aclCache[$cacheKey];			

			$cfg = include(SRC_DIR.'/config/acl/'.$cfg.'.php');
			
			try{
				$this->acl = new \PHN\Model\Acl($cfg, $this);			
				self::$aclCache[$cacheKey] = $this->acl;
			}
			catch (\Exception $e){
				//this ACL use db but could not connect
				$this->acl=null;
			}
			
		}//cfg as array , no support for cache
		else 
			try {
				$this->acl = new \PHN\Model\Acl($cfg, $this);
			}
			catch (\Exception $e){
				//this ACL use db but could not connect
				$this->acl=null;
			}
		
		return $this->acl;
		
	}
	
	/**
	 * 
	 * @param type $url
	 * @param type $query the query string if any
	 * nếu query == true thì tự động append ?redirect=current_url
	 */
	public function redirect($url,$query=null){
		
		if (!empty($query)){
			
			if (!is_array($query) && $query == true )
			{
				$query=array('redirect'=>"http://".$_SERVER['HTTP_HOST'].$_SERVER['REQUEST_URI']);
			}
			$url .= '?'.http_build_query($query); 
		}
		
		header('Location: '.$url);

	}
	
	/**
	 * $path default = 'Auth/index' or <br>
	 * array('app'=>'SomeApp','controller'=>'Index','action'=>'index','param'=>?)
	 * @param string/array $path
	 */
	public function reRoute($path) {
		
		if (is_string($path)) {
			
			$this->app->doRoute(null,$path);
		}
		//as array param
		else $this->app->doRoute($path);
		
	}
	
	
	/**
	 * create a template engine
	 * @param type $cfg
	 * @return \PHN\Template\Template
	 */
	public function template($cfg=null){
		
		if (!$cfg) $cfg = &$this->cfg;
		return new \PHN\View\Template($cfg,$this);		
		
	}
	
	/**
	 * load Form with config ( file or array ) <Br>
	 * if you load Form then no need to use ->template <Br>
	 * because Form extends Template <Br>
	 * cfg = "xxx" or 'App/xxx' relative to application/config
	 * default no param : cfg == $ControllerAction.cfg.php
	 * @param string/array $cfg
	 */
	public function form($cfg=null)
	{
		if (empty($cfg)) {
			
			if ($this->app->appName != 'Main') $cfg=$this->app->appName.'/'.$this->app->controllerName;
			else $cfg = $this->app->controllerName;
		}
		
		if ( is_string($cfg)){	
			
			$rules = include(SRC_DIR.'/config/form/'.$cfg.'.php');
			$this->form =new \PHN\Model\Form($rules,$this);
		}
		else{
			$this->form =new \PHN\Model\Form($cfg,$this);			
			
		}
		
		return $this->form;	
		
	}




//<editor-fold desc="Event Helpers">
	
	/**
	 * 
	 * @param type $eventName
	 * @param type $callback
	 * @param int $priority default = 0
	 */
	public function addListener($eventName,$callback,$priority=0){
		$this->app->event->addListener($eventName,$callback,$priority);
	}
	
	/**
	 * 
	 * @param string $eventName
	 * @param function $listener , nếu function null thì remove all handle for the eventName
	 */
	public function removeListener($eventName, $listener = null){
		$this->app->event->removeListener($eventName, $listener);
	}
	
	/**
	 * 
	 * @param string $eventName
	 * @param \PHN\Core\Event $event , event sẽ được truyền vào callback($event)
	 * dùng event ( new Event() ) rồi gán các dynamic param để callback xử lý nếu cần thiết
	 */
	public function triggerEvent($eventName, Event $event = null){
		$this->app->event->dispatch($eventName,$event);
	}	
	
	
//</editor-fold>

//<editor-fold desc="Fx Filtering Helpers">

	/**
	 * 
	 * @param type $filterName
	 * @param type $function
	 * @param type $priority
	 */
	public function addFxFilter($filterName, $function, $priority = 0){
		$this->app->fxFilter->add($filterName, $function, $priority);
	}
	
	/**
	 * 
	 * @param type $filterName
	 * @param type $function
	 */
	public function removeFxFilter($filterName, $function = null){
		$this->app->fxFilter->remove($filterName, $function );
	}

	/**
	 * 
	 * @param type $filterName
	 * @param type $param
	 */
	public function callFxFilter($filterName,$param=null){
		 $this->app->fxFilter->call($filterName,$param);
	}


//</editor-fold>
}



/**
 * Event is the base class for classes containing event data.
 *
 * This class contains no event data. It is used by events that do not pass
 * state information to an event handler when an event is raised.
 *
 * You can call the method stopPropagation() to abort the execution of
 * further listeners in your event listener.
 *
 * @author  Guilherme Blanco <guilhermeblanco@hotmail.com>
 * @author  Jonathan Wage <jonwage@gmail.com>
 * @author  Roman Borschel <roman@code-factory.org>
 * @author  Bernhard Schussek <bschussek@gmail.com>
 *
 * @api
 */
class Event {

	/**
	 * @var Boolean Whether no further event listeners should be triggered
	 */
	private $propagationStopped = false;

	/**
	 * @var EventDispatcher Dispatcher that dispatched this event
	 */
	private $dispatcher;

	/**
	 * @var string This event's name
	 */
	private $name;

	/**
	 * Returns whether further event listeners should be triggered.
	 *
	 * @see Event::stopPropagation
	 * @return Boolean Whether propagation was already stopped for this event.
	 *
	 * @api
	 */
	public function isPropagationStopped() {
		return $this->propagationStopped;
	}

	/**
	 * Stops the propagation of the event to further event listeners.
	 *
	 * If multiple event listeners are connected to the same event, no
	 * further event listener will be triggered once any trigger calls
	 * stopPropagation().
	 *
	 * @api
	 */
	public function stopPropagation() {
		$this->propagationStopped = true;
	}

	/**
	 * Stores the EventDispatcher that dispatches this Event
	 *
	 * @param EventDispatcherInterface $dispatcher
	 *
	 * @api
	 */
	public function setDispatcher($dispatcher) {
		$this->dispatcher = $dispatcher;
	}

	/**
	 * Returns the EventDispatcher that dispatches this Event
	 *
	 * @return EventDispatcherInterface
	 *
	 * @api
	 */
	public function getDispatcher() {
		return $this->dispatcher;
	}

	/**
	 * Gets the event's name.
	 *
	 * @return string
	 *
	 * @api
	 */
	public function getName() {
		return $this->name;
	}

	/**
	 * Sets the event's name property.
	 *
	 * @param string $name The event name.
	 *
	 * @api
	 */
	public function setName($name) {
		$this->name = $name;
	}

}

/**
 * The EventDispatcherInterface is the central point of Symfony's event listener system.
 *
 * Listeners are registered on the manager and events are dispatched through the
 * manager.
 *
 * @author  Guilherme Blanco <guilhermeblanco@hotmail.com>
 * @author  Jonathan Wage <jonwage@gmail.com>
 * @author  Roman Borschel <roman@code-factory.org>
 * @author  Bernhard Schussek <bschussek@gmail.com>
 * @author  Fabien Potencier <fabien@symfony.com>
 * @author  Jordi Boggiano <j.boggiano@seld.be>
 * @author  Jordan Alliot <jordan.alliot@gmail.com>
 *
 * @api
 */
class EventDispatcher {

	private $listeners = array();
	private $sorted = array();

	/**
	 * @see EventDispatcherInterface::dispatch
	 *
	 * @api
	 */
	public function dispatch($eventName, Event $event = null) {
		if (null === $event) {
			$event = new Event();
		}

		$event->setDispatcher($this);
		$event->setName($eventName);

		if (!isset($this->listeners[$eventName])) {
			return $event;
		}

		$this->doDispatch($this->getListeners($eventName), $eventName, $event);

		return $event;
	}

	/**
	 * @see EventDispatcherInterface::getListeners
	 */
	public function getListeners($eventName = null) {
		if (null !== $eventName) {
			if (!isset($this->sorted[$eventName])) {
				$this->sortListeners($eventName);
			}

			return $this->sorted[$eventName];
		}

		foreach (array_keys($this->listeners) as $eventName) {
			if (!isset($this->sorted[$eventName])) {
				$this->sortListeners($eventName);
			}
		}

		return $this->sorted;
	}

	/**
	 * @see EventDispatcherInterface::hasListeners
	 */
	public function hasListeners($eventName = null) {
		return (Boolean) count($this->getListeners($eventName));
	}

	/**
	 * @see EventDispatcherInterface::addListener
	 * $priority higher the earlier
	 * @api
	 */
	public function addListener($eventName, $listener, $priority = 0) {
		$this->listeners[$eventName][$priority][] = $listener;
		unset($this->sorted[$eventName]);
	}

	/**
	 * @see EventDispatcherInterface::removeListener
	 * @modified by NP $listener =null , remove all
	 */
	public function removeListener($eventName, $listener = null) {
		if (!isset($this->listeners[$eventName])) {
			return;
		}
		
		// <editor-fold desc="modified here::adding">
		if (!$listener) {
			unset($this->listeners[$eventName],$this->sorted[$eventName]);
			return;
		}
		// </editor-fold>

		foreach ($this->listeners[$eventName] as $priority => $listeners) {
			if (false !== ($key = array_search($listener, $listeners, true))) {
				unset($this->listeners[$eventName][$priority][$key], $this->sorted[$eventName]);
			}
		}
	}

	/**
	 * @see EventDispatcherInterface::addSubscriber
	 *
	 * @api
	 */
	public function addSubscriber(EventSubscriberInterface $subscriber) {
		foreach ($subscriber->getSubscribedEvents() as $eventName => $params) {
			if (is_string($params)) {
				$this->addListener($eventName, array($subscriber, $params));
			} elseif (is_string($params[0])) {
				$this->addListener($eventName, array($subscriber, $params[0]), isset($params[1]) ? $params[1] : 0);
			} else {
				foreach ($params as $listener) {
					$this->addListener($eventName, array($subscriber, $listener[0]), isset($listener[1]) ? $listener[1] : 0);
				}
			}
		}
	}

	/**
	 * @see EventDispatcherInterface::removeSubscriber
	 */
	public function removeSubscriber(EventSubscriberInterface $subscriber) {
		foreach ($subscriber->getSubscribedEvents() as $eventName => $params) {
			if (is_array($params) && is_array($params[0])) {
				foreach ($params as $listener) {
					$this->removeListener($eventName, array($subscriber, $listener[0]));
				}
			} else {
				$this->removeListener($eventName, array($subscriber, is_string($params) ? $params : $params[0]));
			}
		}
	}

	/**
	 * Triggers the listeners of an event.
	 *
	 * This method can be overridden to add functionality that is executed
	 * for each listener.
	 *
	 * @param array[callback] $listeners The event listeners.
	 * @param string          $eventName The name of the event to dispatch.
	 * @param Event           $event     The event object to pass to the event handlers/listeners.
	 */
	protected function doDispatch($listeners, $eventName, Event $event) {
		foreach ($listeners as $listener) {
			call_user_func($listener, $event);
			if ($event->isPropagationStopped()) {
				break;
			}
		}
	}

	/**
	 * Sorts the internal list of listeners for the given event by priority.
	 *
	 * @param string $eventName The name of the event.
	 */
	private function sortListeners($eventName) {
		$this->sorted[$eventName] = array();

		if (isset($this->listeners[$eventName])) {
			krsort($this->listeners[$eventName]);
			$this->sorted[$eventName] = call_user_func_array('array_merge', $this->listeners[$eventName]);
		}
	}

}



class FxFilter{
	
	private $filters = array();
	private $sorted = array();

	public function get($filterName = null) 
	{
		if (null !== $filterName) 
		{
			if (!isset($this->sorted[$filterName])) 
			{
				$this->sort($filterName);
			}

			return $this->sorted[$filterName];
		}

		foreach (array_keys($this->filters) as $filterName) 
		{
			if (!isset($this->sorted[$filterName])) {
				$this->sort($filterName);
			}
		}

		return $this->sorted;
	}


	public function has($filterName = null) {
		return (Boolean) count($this->get($filterName));
	}

	/**
	 * 
	 * @param string $filterName
	 * @param callable $function
	 * @param int $priority
	 */
	public function add($filterName, $function, $priority = 0) {
		$this->filters[$filterName][$priority][] = $function;
		unset($this->sorted[$filterName]);
	}

	/**
	 * 
	 * @param type $filterName
	 * @param type $function
	 * 
	 */
	public function remove($filterName, $function = null) {
		if (!isset($this->filters[$filterName])) {
			return;
		}

		if (!$function) {
			unset($this->filters[$filterName],$this->sorted[$filterName]);
			return;
		}

		foreach ($this->filters[$filterName] as $priority => $filters) {
			if (false !== ($key = array_search($function, $filters, true))) {
				unset($this->filters[$filterName][$priority][$key], $this->sorted[$filterName]);
			}
		}
	}


	public function call($filterName,$param=null) {

		if (!isset($this->filters[$filterName])) {
			return $param;
		}
		
		$filters=$this->get($filterName);
	
		$args = func_get_args();
		foreach ($filters as $filter) {
			$args[1] = $param;
			$param=call_user_func_array($filter,  array_slice($args, 1));
		}
	}

	private function sort($filterName) {
		$this->sorted[$filterName] = array();

		if (isset($this->filters[$filterName])) {
			krsort($this->filters[$filterName]);
			$this->sorted[$filterName] = call_user_func_array('array_merge', $this->filters[$filterName]);
		}
	}

}
		
		
}//end namespace Core




// <editor-fold defaultstate="collapsed" desc="extra helper function">

namespace {
	
	/**
	 * var_dump
	 */
	function _vd(){
		
		
		$msgs=debug_backtrace();
		//var_dump($msgs);exit;
		
		$msg = $msgs[1];
		
		echo '<div style="border:1px solid #dd0000; padding:8px;"><div style="color:#ff0000">Call <b>' . $msg['function'] . '()</b> #Line: <b> ' . @$msg['line'] . '</b> @ '.@$msg['file'] . '</div>';
		echo '<div style="background-color:#eee; padding:8px;"><pre>';
		call_user_func_array('var_dump',func_get_args());
		echo '</pre></div></div>';
	}
	
}

// </editor-fold>