<?php
/**
 * login.php - Login controller
 *
 * @author $Author: dtong $
 * @version $Id: login.php,v 1.33 2011/05/18 03:23:15 dtong Exp $
 * @copyright Copyright (c) 2009, Tiller Software Co., Ltd.
*/

class Login extends CI_Controller {
	//5 failed login within 10 minutes will lock the account for 10 minutes
	const FAILED_LOCKOUT_COUNT = 5;
	//In minutes
	const LOCKOUT_PERIOD = 15;
	const SECONDS_IN_MINUTES = 60;
	//DB has 500 bytes so make 2 less to ensure it never gets overfilled
	const MAX_DB_IP_STORAGE = 498;
	//This is the max length of a IPV6 address.
	const MAX_IP_ADDRESS_LENGTH = 45;
	const IPLIST_DELIMETER = ',';

	function __construct() {
      parent::__construct();
		$this->load->helper(array('form', 'url'));
      $this->load->library('form_validation');
      osa_load_lang('login');
	}

   function index() {
   	if ( $this->login_model->isAuthenticated() ) {
   		if ( $this->session->flashdata('LOGINPAGE_NO_REDIRECT_HOME') != '1') {
   	  		redirect('home');
      		exit;
   		}
   	}

      $this->form_validation->set_rules('username', 'Username',
         'trim|required|min_length[1]|strtolower');
      //Can't use a variable to set lenght of password, has to be hardcoded here
      $this->form_validation->set_rules('password', 'Password',
         'required|min_length[1]');
      //$this->form_validation->set_rules('passconf', 'Password Confirmation', 'required');
      //$this->form_validation->set_rules('email', 'Email', 'required');

      $msg = FALSE;
      if ($this->form_validation->run() == FALSE) {
      	//Just to make sure we are clean
      	$username = set_value('username');
      	$password = set_value('password');
      	if ( $username != '' )
      		$msg = lang('login_missingpassword');
      	$sess_username = $this->session->flashdata('AUTH_FAILED_USERNAME');
      	if ( $sess_username ) {
      		$username = $sess_username;
      		$msg = lang('login_invalidlogin');
      	}
      	if ( $this->session->flashdata(CMS_SESSION_FAILED_LOGIN_LOCKOUT) == TRUE ) {
      		$msg = sprintf(lang('login_lockout'), self::FAILED_LOCKOUT_COUNT, self::LOCKOUT_PERIOD);
      	}
      	$msg_mode= FALSE;
      	if ( $this->config->item('cms_manage_mode') ) {
      		 $msg_mode = lang('login_managemode') ;
      	}
      	if (  $this->config->item('cms_sitestop') ) {
      		$msg_mode = lang('gen_maintenance_msg');
      	}

      	$sess_managemode = $this->session->flashdata('AUTH_FAILED_MANAGE_MODE');
      	if ( $sess_managemode )
      		 $msg = '';  //$msg_mode = lang('login_managemode');

      	//$this->login_model->destroyLogin();
      	osa_del_treecookies();
      	$data = array();
      	//Pass array of array for view within view
      	$data['data'] = array('username'=>osa_des_decrypt($username), 'msg'=>$msg, 'msg_mode'=>$msg_mode);
      	//main_login_view contains multiple views
      	$this->load->view('login_m_view', $data);
      }
      else {
      	$username = set_value('username');
      	$password = set_value('password');
      	$username = osa_des_decrypt($username);
      	$password = osa_des_decrypt($password);
      	if ( $this->lockout_check($username) && $this->login_model->authenticate($username, $password) ) {
      		 $this->success_update($username);
      		 //Load the template cache library. This will create the template cache if there isn't one yet.
      		 //For a new system, the first login will create this cache
      		 //The library param can be anything, it is there to tell the lib to check for mod time
      		 //If the template files or related source files got updated then the cache would get updated too.
   			 $this->load->library('cms/cms_template/cms_template_init', array('checkmodtime'=>TRUE));
   			 //This will initilize the unit_search_cache memory tables
   			 //The check_empty is a dummy array to tell the object to check for empty tables
   			 $this->load->library('cms/memory_table/unit_cache_table', array('check_empty'=>TRUE));
   			 $this->unit_cache_table->auto_update(); //Run the scheduled auto update if necessary
             redirect('home');
             exit;
      	}
      	else {
				$this->session->set_flashdata('AUTH_FAILED_USERNAME', $username);
				$this->failed_update($username);
				//$this->lockout_check($username, TRUE);
				redirect('');
            exit;
        }
      }
   }

   function logout() {
      $this->login_model->destroyLogin();
      @osa_php_session_destroy();
      osa_del_treecookies();
      redirect('');
      exit;
   }

   function key() {
   	$key = osa_des_session_key();
   	if ( !empty($key) ) {
   		//Session has the key client has already so just say success
   		echo 'success';
   		exit;
   	}
   	//This generates a new key since the session already expired.
   	$this->load->helper('des_helper');
   	echo stringToHex(osa_des_random_key());
   	exit;
   }

   //Check to see if the account is locked out
   private function lockout_check($username, $set_session=TRUE) {
   	if ( !$this->config->item('cms_failed_login_lockout_check') ) {
   		return TRUE;
   	}
   	//This is the same call in login_model->authenticate() and the result is already cached
   	//if we are here
		$user = $this->login_model->getDbUser($username, TRUE);
		if ( !is_object($user) ) {
			osa_log(__METHOD__, 'Failed to get user object.', $username);
			return FALSE;
		}

		if ( $this->match_ip($user->successips, $_SERVER['REMOTE_ADDR'])) {
			//The IP did login successfully in the past so skip the lockout
			return TRUE;
		}

		$dif = time() - strtotime($user->lastfailedlogin);
		if ( $user->lastfailedlogin && $user->failedlogincount >= self::FAILED_LOCKOUT_COUNT &&
			  $dif < (self::LOCKOUT_PERIOD * self::SECONDS_IN_MINUTES) ) {
			if ( $set_session ) {
				//Informs the account lockout
				$this->session->set_flashdata(CMS_SESSION_FAILED_LOGIN_LOCKOUT, TRUE);
			}
			return FALSE;
		}
		return TRUE;
   }

   //Updates the user record for failed attempt
	private function failed_update($username) {
		$user = $this->login_model->getDbUser($username, TRUE);
		if ( !$user ) {
			return FALSE;
		}
		$dif = time() - strtotime($user->lastfailedlogin);
		if ( $user->failedlogincount >  self::FAILED_LOCKOUT_COUNT &&
			  $dif < (self::LOCKOUT_PERIOD * self::SECONDS_IN_MINUTES) ) {
			//It is already locked so no need to update the DB
			return TRUE;
		}
		$record = new stdClass();
		$record->failedips = $user->failedips;
		$record->failedlogincount = $user->failedlogincount + 1;
		if ( $record->failedlogincount <= 0 ) {
			//This means recent failed login attempt after a successful attempt so reset it.
			//A successful login will make the failed login count into a negative number.
			$record->failedlogincount = 1;
		}
		$record->lastfailedlogin = osa_dbdate();
		if ( $dif >= (self::LOCKOUT_PERIOD * self::SECONDS_IN_MINUTES) ) {
			//A failed attempt after the lockout period expires.
			$record->failedlogincount = 1;
		}
		$this->load->model('admin/user_model');
		//It is not really used, just for tracking purpose
		$record->failedips = $this->add_ip2list($record->failedips, $_SERVER['REMOTE_ADDR']);
		return $this->user_model->update_user($user->id, $record);
   }

   //Updates the user record for success login
   private function success_update($username) {
   	$user = $this->login_model->getDbUser($username, TRUE);
		if ( !$user ) {
			osa_log(__METHOD__, 'Failed to get user object.', $username);
			return FALSE;
		}
		$failedlogincount = $user->failedlogincount;
		if ( $failedlogincount > 0 ) {
			//This for keeping track the last fail count after the first successful login
			//A negative number means the last failelogincount
			$failedlogincount *= -1;
		}
		elseif ( $failedlogincount < 0 ) {
			//This will clear the negative number on the second successful login
			$failedlogincount = 0;
		}
		$record = new stdClass();
		$record->failedlogincount = $failedlogincount;
		$record->lastlogin = osa_dbdate();
		$record->failedips = NULL;
		$record->successips = $this->add_ip2list($user->successips, $_SERVER['REMOTE_ADDR']);
		$this->load->model('admin/user_model');
		return $this->user_model->update_user($user->id, $record);
   }

   //Returns true if $ip is in $iplist, false otherwise. This works for IPv4 and IPv6.
   //This is a full IP matching and if user is using dynamic IP then this is useless.
   //This is done to prevent DoS attach but only if user coming from the same static IP(s).
   //TODO: Should allow subnet matching for IPv4 and IPv6 since it is not likely brute force and DoS attacks
   //		  would come from the same subnet. Thinking to use class B for IPv4, for IPv6???
   private function match_ip($iplist, $ip) {
   	if ( !$ip || !$iplist ) {
   		return FALSE;
   	}
   	$iparray = explode(',', $iplist);
   	return in_array($ip, $iparray);
   }

   //Returns a new list with $ip added to $iplist. This will purge old ips from $iplist if not enough storage.
   //This is not specific to any field in the user table.
   //This can mix IPv4 and IPv6 addresses
   private function add_ip2list($iplist, $ip) {
   	$iplist = trim($iplist);
   	$ip = trim($ip);
   	if ( empty($ip) || strlen($ip) > self::MAX_IP_ADDRESS_LENGTH ) {
   		osa_log(__METHOD__, 'Invalid ip input.', $ip);
   		return $iplist;
   	}
   	if ( empty($iplist) ) {
   		return $ip;
   	}
		if ( $this->match_ip($iplist, $ip) ) {
			//IP is already in the list so don't change anything
			return $iplist;
		}
   	//Needs one more for the delimeter
   	$ip_length = strlen($ip) + 1;
   	$newlist = $iplist;
   	if ( (strlen($iplist) + $ip_length) > self::MAX_DB_IP_STORAGE ) {
   		//Not enough storage so purge old ones
   		$count = 0;
			while (TRUE) {
				$newlist = osa_str_extract($newlist, self::IPLIST_DELIMETER, FALSE);
				if ( (strlen($newlist) + $ip_length) <= self::MAX_DB_IP_STORAGE ) {
					break;
				}
				if ( $count++ > 100 ) {
					//Just in case something goes wrong
					osa_log(__METHOD__, 'Looped too many times, just return the original iplist.', $ip, $iplist);
					return $iplist;
				}
			}
   	}
   	return $newlist . self::IPLIST_DELIMETER . $ip;
   }
}
?>