<?php
/**
* @version	$Id: user_helper.php 15374 2012-05-30 15:27:07Z alex $
* @package	In-Portal
* @copyright	Copyright (C) 1997 - 2009 Intechnic. All rights reserved.
* @license      GNU/GPL
* In-Portal is Open Source software.
* This means that this software may have been modified pursuant
* the GNU General Public License, and as distributed it includes
* or is derivative of works licensed under the GNU General Public License
* or other free or open source software licenses.
* See http://www.in-portal.org/license for copyright notices and details.
*/

	defined('FULL_PATH') or die('restricted access!');

	class UserHelper extends kHelper {

		/**
		 * Event to be used during login processings
		 *
		 * @var kEvent
		 */
		var $event = null;

		/**
		 * Performs user login and returns the result
		 *
		 * @param string $username
		 * @param string $password
		 * @param bool $dry_run
		 * @param bool $remember_login
		 * @param string $remember_login_cookie
		 * @return int
		 */
		function loginUser($username, $password, $dry_run = false, $remember_login = false, $remember_login_cookie = '')
		{
			if (!isset($this->event)) {
				$this->event = new kEvent('u:OnLogin');
			}

			if (!$password && !$remember_login_cookie) {
				return LoginResult::INVALID_PASSWORD;
			}

			$object =& $this->getUserObject();

			// process "Save Username" checkbox
			if ($this->Application->isAdmin) {
				$save_username = $this->Application->GetVar('cb_save_username') ? $username : '';
				$this->Application->Session->SetCookie('save_username', $save_username, strtotime('+1 year'));

				// cookie will be set on next refresh, but refresh won't occur if
				// login error present, so duplicate cookie in kHTTPQuery
				$this->Application->SetVar('save_username', $save_username);
			}

			// logging in "root" (admin only)
			$super_admin = ($username == 'super-root') && $this->verifySuperAdmin();
			if ($this->Application->isAdmin && ($username == 'root') || ($super_admin && $username == 'super-root')) {
				$root_password = $this->Application->ConfigValue('RootPass');

				$password_formatter = $this->Application->recallObject('kPasswordFormatter');
				/* @var $password_formatter kPasswordFormatter */

				if ($root_password != $password_formatter->EncryptPassword($password, 'b38')) {
					return LoginResult::INVALID_PASSWORD;
				}

				$user_id = USER_ROOT;
				$object->Clear($user_id);
				$object->SetDBField('Username', 'root');

				if (!$dry_run) {
					$this->loginUserById($user_id, $remember_login_cookie);

					if ($super_admin) {
						$this->Application->StoreVar('super_admin', 1);
					}

					// reset counters
					$this->Application->resetCounters('UserSessions');

					$this->_processLoginRedirect('root', $password);
					$this->_processInterfaceLanguage();
				}

				return LoginResult::OK;
			}

			$user_id = $this->getUserId($username, $password, $remember_login_cookie);

			if ($user_id) {
				$object->Load($user_id);

				if (!$this->checkBanRules($object)) {
					return LoginResult::BANNED;
				}

				if ($object->GetDBField('Status') == STATUS_ACTIVE) {
					if ( !$this->checkLoginPermission() ) {
						return LoginResult::NO_PERMISSION;
					}

					if (!$dry_run) {
						$this->loginUserById($user_id, $remember_login_cookie);

						if ($remember_login) {
							// remember username & password when "Remember Login" checkbox us checked (when user is using login form on Front-End)
							$this->Application->Session->SetCookie('remember_login', $username . '|' . md5($password), strtotime('+1 month'));
						}

						if (!$remember_login_cookie) {
							// reset counters
							$this->Application->resetCounters('UserSessions');

							$this->_processLoginRedirect($username, $password);
							$this->_processInterfaceLanguage();
						}
					}

					return LoginResult::OK;
				}
				else {
					$pending_template = $this->Application->GetVar('pending_disabled_template');

					if ($pending_template !== false && !$dry_run) {
						// when user found, but it's not yet approved redirect hit to notification template
						$this->event->redirect = $pending_template;

						return LoginResult::OK;
					}
					else {
						// when no notification template given return an error
						return LoginResult::INVALID_PASSWORD;
					}
				}
			}

			if (!$dry_run) {
				$this->event->SetRedirectParam('pass', 'all');
//				$this->event->SetRedirectParam('pass_category', 1); // to test
			}

			return LoginResult::INVALID_PASSWORD;
		}

		/**
		 * Login user by it's id
		 *
		 * @param int $user_id
		 * @param bool $remember_login_cookie
		 */
		function loginUserById($user_id, $remember_login_cookie = false)
		{
			$object =& $this->getUserObject();

			$this->Application->StoreVar('user_id', $user_id);
			$this->Application->SetVar('u.current_id', $user_id);
			$this->Application->Session->SetField('PortalUserId', $user_id);

			if ($user_id != USER_ROOT) {
				$groups = $this->Application->RecallVar('UserGroups');
				$this->Application->Session->SetField('GroupId', reset( explode(',', $groups) ));
				$this->Application->Session->SetField('GroupList', $groups);
				$this->Application->Session->SetField('TimeZone', $object->GetDBField('TimeZone'));
			}

			$this->Application->LoadPersistentVars();

			if (!$remember_login_cookie) {
				// don't change last login time when auto-login is used
				$this_login = (int)$this->Application->RecallPersistentVar('ThisLogin');
				$this->Application->StorePersistentVar('LastLogin', $this_login);
				$this->Application->StorePersistentVar('ThisLogin', adodb_mktime());
			}

			$this->Application->HandleEvent(new kEvent('u:OnAfterLogin'));
		}

		/**
		 * Checks login permission
		 *
		 * @return bool
		 */
		function checkLoginPermission()
		{
			$object =& $this->getUserObject();
			$ip_restrictions = $object->GetDBField('IPRestrictions');

			if ( $ip_restrictions && !$this->Application->isDebugMode() && !kUtil::ipMatch($ip_restrictions, "\n") ) {
				return false;
			}

			$groups = $object->getMembershipGroups(true);
			if ( !$groups ) {
				$groups = Array ();
			}

			$default_group = $this->getUserTypeGroup();
			if ( $default_group !== false ) {
				array_push($groups, $default_group);
			}

			// store groups, because kApplication::CheckPermission will use them!
			array_push($groups, $this->Application->ConfigValue('User_LoggedInGroup'));
			$groups = array_unique($groups);
			$this->Application->StoreVar('UserGroups', implode(',', $groups), true); // true for optional

			return $this->Application->CheckPermission($this->Application->isAdmin ? 'ADMIN' : 'LOGIN', 1);
		}

		/**
		 * Returns default user group for it's type
		 *
		 * @return bool|string
		 * @access protected
		 */
		protected function getUserTypeGroup()
		{
			$group_id = false;
			$object =& $this->getUserObject();

			if ( $object->GetDBField('UserType') == UserType::USER ) {
				$group_id = $this->Application->ConfigValue('User_NewGroup');
			}
			elseif ( $object->GetDBField('UserType') == UserType::ADMIN ) {
				$group_id = $this->Application->ConfigValue('User_AdminGroup');
			}

			$ip_restrictions = $this->getGroupsWithIPRestrictions();

			if ( !isset($ip_restrictions[$group_id]) || kUtil::ipMatch($ip_restrictions[$group_id], "\n") ) {
				return $group_id;
			}

			return false;
		}

		/**
		 * Returns groups with IP restrictions
		 *
		 * @return Array
		 * @access public
		 */
		public function getGroupsWithIPRestrictions()
		{
			static $cache = null;

			if ( $this->Application->isDebugMode() ) {
				return Array ();
			}

			if ( !isset($cache) ) {
				$sql = 'SELECT IPRestrictions, GroupId
						FROM ' . TABLE_PREFIX . 'UserGroups
						WHERE IPRestrictions IS NOT NULL';
				$cache = $this->Conn->GetCol($sql, 'GroupId');
			}

			return $cache;
		}

		/**
		 * Performs user logout
		 *
		 */
		function logoutUser()
		{
			if (!isset($this->event)) {
				$this->event = new kEvent('u:OnLogout');
			}

			$sync_manager = $this->Application->recallObject('UsersSyncronizeManager', null, Array(), Array ('InPortalSyncronize'));
			/* @var $sync_manager UsersSyncronizeManager */

			$sync_manager->performAction('LogoutUser');

			$this->Application->HandleEvent(new kEvent('u:OnBeforeLogout'));

			$user_id = USER_GUEST;
			$this->Application->SetVar('u.current_id', $user_id);

			$object = $this->Application->recallObject('u.current', null, Array('skip_autoload' => true));
			/* @var $object UsersItem */

			$object->Load($user_id);

			$this->Application->DestroySession();

			$this->Application->StoreVar('user_id', $user_id, true);
			$this->Application->Session->SetField('PortalUserId', $user_id);

			$group_list = $this->Application->ConfigValue('User_GuestGroup') . ',' . $this->Application->ConfigValue('User_LoggedInGroup');
			$this->Application->StoreVar('UserGroups', $group_list, true);
			$this->Application->Session->SetField('GroupList', $group_list);

			if ($this->Application->ConfigValue('UseJSRedirect')) {
				$this->event->SetRedirectParam('js_redirect', 1);
			}

			$this->Application->resetCounters('UserSessions');
			$this->Application->Session->SetCookie('remember_login', '', strtotime('-1 hour'));

			// don't pass user prefix on logout, since resulting url will have broken "env"
			$this->event->SetRedirectParam('pass', MOD_REWRITE ? 'm' : 'all');
		}

		/**
		 * Returns user id based on given criteria
		 *
		 * @param string $username
		 * @param string $password
		 * @param string $remember_login_cookie
		 * @return int
		 */
		function getUserId($username, $password, $remember_login_cookie)
		{
			$password = md5($password);

			if ($remember_login_cookie) {
				list ($username, $password) = explode('|', $remember_login_cookie); // 0 - username, 1 - md5(password)
			}

			$sql = 'SELECT PortalUserId
					FROM ' . TABLE_PREFIX . 'Users
					WHERE (Email = %1$s OR Username = %1$s) AND (Password = %2$s)';
			return $this->Conn->GetOne( sprintf($sql, $this->Conn->qstr($username), $this->Conn->qstr($password) ) );
		}

		/**
		 * Process all required data and redirect logged-in user
		 *
		 * @param string $username
		 * @param string $password
		 */
		function _processLoginRedirect($username, $password)
		{
			// set next template
			$next_template = $this->Application->GetVar('next_template');

			if ($next_template) {
				$this->event->redirect = $next_template;
			}

			// process IIS redirect
			if ($this->Application->ConfigValue('UseJSRedirect')) {
				$this->event->SetRedirectParam('js_redirect', 1);
			}

			// synchronize login
			$sync_manager = $this->Application->recallObject('UsersSyncronizeManager', null, Array(), Array ('InPortalSyncronize'));
			/* @var $sync_manager UsersSyncronizeManager */

			$sync_manager->performAction('LoginUser', $username, $password);
		}

		/**
		 * Sets correct interface language after successful login, based on user settings
		 *
		 * @return void
		 * @access protected
		 */
		protected function _processInterfaceLanguage()
		{
			if ( defined('IS_INSTALL') && IS_INSTALL ) {
				$this->event->SetRedirectParam('m_lang', 1); // data
				$this->Application->Session->SetField('Language', 1); // interface

				return;
			}

			$language_field = $this->Application->isAdmin ? 'AdminLanguage' : 'FrontLanguage';
			$primary_language_field = $this->Application->isAdmin ? 'AdminInterfaceLang' : 'PrimaryLang';
			$is_root = $this->Application->RecallVar('user_id') == USER_ROOT;

			$object =& $this->getUserObject();

			$user_language_id = $is_root ? $this->Application->RecallPersistentVar($language_field) : $object->GetDBField($language_field);

			$sql = 'SELECT LanguageId, IF(LanguageId = ' . (int)$user_language_id . ', 2, ' . $primary_language_field . ') AS SortKey
					FROM ' . TABLE_PREFIX . 'Languages
					WHERE Enabled = 1
					HAVING SortKey <> 0
					ORDER BY SortKey DESC';
			$language_info = $this->Conn->GetRow($sql);
			$language_id = $language_info && $language_info['LanguageId'] ? $language_info['LanguageId'] : $user_language_id;

			if ( $user_language_id != $language_id ) {
				// first login OR language was deleted or disabled
				if ( $is_root ) {
					$this->Application->StorePersistentVar($language_field, $language_id);
				}
				else {
					$object->SetDBField($language_field, $language_id);
					$object->Update();
				}
			}

			// set language for Admin Console & Front-End with disabled Mod-Rewrite
			$this->event->SetRedirectParam('m_lang', $language_id); // data
			$this->Application->Session->SetField('Language', $language_id); // interface

			// set language for Front-End with enabled Mod-Rewrite
			if ( MOD_REWRITE ) {
				$this->_injectLanguageIntoUrl($language_id);
			}
		}

		/**
		 * Inject language into whatever page user wants to go after login
		 *
		 * @param int $language_id
		 * @return void
		 * @access protected
		 */
		protected function _injectLanguageIntoUrl($language_id)
		{
			$url = $this->Application->HREF($this->event->redirect, '', $this->event->getRedirectParams(), $this->event->redirectScript);
			$vars = $this->Application->parseRewriteUrl($url, 'pass');

			$vars['pass'] = implode(',', $vars['pass']);
			$vars['m_lang'] = $language_id;
			$template = $vars['t'];
			unset($vars['is_virtual'], $vars['t']);

			$this->event->redirect = $template;
			$this->event->setRedirectParams($vars, false);
		}

		/**
		 * Checks that user is allowed to use super admin mode
		 *
		 * @return bool
		 */
		function verifySuperAdmin()
		{
			$sa_mode = kUtil::ipMatch(defined('SA_IP') ? SA_IP : '');
			return $sa_mode || $this->Application->isDebugMode();
		}

		/**
		 * Returns user object, used during login processing
		 *
		 * @return UsersItem
		 * @access public
		 */
		public function &getUserObject()
		{
			$prefix_special = $this->Application->isAdmin ? 'u.current' : 'u'; // "u" used on front not to change theme

			$object = $this->Application->recallObject($prefix_special, null, Array('skip_autoload' => true));
			/* @var $object UsersItem */

			return $object;
		}

		/**
		 * Checks, if given user fields matches at least one of defined ban rules
		 *
		 * @param kDBItem $object
		 * @return bool
		 */
		function checkBanRules(&$object)
		{
			$table = $this->Application->getUnitOption('ban-rule', 'TableName');

			if (!$this->Conn->TableFound($table)) {
				// when ban table not found -> assume user is ok by default
				return true;
			}

			$sql = 'SELECT *
					FROM ' . $table . '
					WHERE ItemType = 6 AND Status = ' . STATUS_ACTIVE . '
					ORDER BY Priority DESC';
			$rules = $this->Conn->Query($sql);

			$found = false;

			foreach ($rules as $rule) {
				$field = $rule['ItemField'];
				$this_value = mb_strtolower( $object->GetDBField($field) );
				$test_value = mb_strtolower( $rule['ItemValue'] );

				switch ( $rule['ItemVerb'] ) {
					case 1: // is
						if ($this_value == $test_value) {
							$found = true;
						}
						break;

					case 2: // is not
						if ($this_value != $test_value) {
							$found = true;
						}
						break;

					case 3: // contains
						if ( strstr($this_value, $test_value) ) {
							$found = true;
						}
						break;

					case 4: // not contains
						if ( !strstr($this_value, $test_value) ) {
							$found = true;
						}
						break;

					case 7: // exists
						if ( strlen($this_value) > 0 ) {
							$found = true;
						}
						break;

					case 8: // unique
						if ( $this->_checkValueExist($field, $this_value) ) {
							$found = true;
						}
						break;
				}

				if ( $found ) {
					// check ban rules, until one of them matches

					if ( $rule['RuleType'] ) {
						// invert rule type
						$found = false;
					}

					break;
				}
			}

			return !$found;
		}

		/**
		 * Checks if value is unique in Users table against the specified field
		 *
		 * @param string $field
		 * @param string $value
		 * @return string
		 */
		function _checkValueExist($field, $value)
	    {
	    	$sql = 'SELECT *
	    			FROM ' . $this->Application->getUnitOption('u', 'TableName') . '
					WHERE '. $field .' = ' . $this->Conn->qstr($value);

			return $this->Conn->GetOne($sql);
	    }

		public function validateUserCode($user_code, $code_type, $expiration_timeout = null)
		{
			$expiration_timeouts = Array (
				'forgot_password' => 'config:Users_AllowReset',
				'activation' => 'config:UserEmailActivationTimeout',
				'verify_email' => 'config:Users_AllowReset',
				'custom' => '',
			);

		    if ( !$user_code ) {
		    	return 'code_is_not_valid';
		    }

		    $sql = 'SELECT PwRequestTime, PortalUserId
		    		FROM ' . TABLE_PREFIX . 'Users
		    		WHERE PwResetConfirm = ' . $this->Conn->qstr( trim($user_code) );
		    $user_info = $this->Conn->GetRow($sql);

		    if ( $user_info === false ) {
		    	return 'code_is_not_valid';
		    }

	    	$expiration_timeout = isset($expiration_timeout) ? $expiration_timeout : $expiration_timeouts[$code_type];

	    	if ( preg_match('/^config:(.*)$/', $expiration_timeout, $regs) ) {
	    		$expiration_timeout = $this->Application->ConfigValue( $regs[1] );
	    	}

	    	if ( $expiration_timeout && $user_info['PwRequestTime'] < strtotime('-' . $expiration_timeout . ' minutes') ) {
				return 'code_expired';
	    	}

			return $user_info['PortalUserId'];
		}

		/**
		 * Restores user's email, returns error label, if error occurred
		 *
		 * @param string $hash
		 * @return string
		 * @access public
		 */
		public function restoreEmail($hash)
		{
			if ( !preg_match('/^[a-f0-9]{32}$/', $hash) ) {
				return 'invalid_hash';
			}

			$sql = 'SELECT PortalUserId, PrevEmails
					FROM ' . TABLE_PREFIX . 'Users
					WHERE PrevEmails LIKE ' . $this->Conn->qstr('%' . $hash . '%');
			$user_info = $this->Conn->GetRow($sql);

			if ( $user_info === false ) {
				return 'invalid_hash';
			}

			$prev_emails = $user_info['PrevEmails'];
			$prev_emails = $prev_emails ? unserialize($prev_emails) : Array ();

			if ( !isset($prev_emails[$hash]) ) {
				return 'invalid_hash';
			}

			$email_to_restore = $prev_emails[$hash];
			unset($prev_emails[$hash]);

			$object = $this->Application->recallObject('u.email-restore', null, Array ('skip_autoload' => true));
			/* @var $object UsersItem */

			$object->Load($user_info['PortalUserId']);
			$object->SetDBField('PrevEmails', serialize($prev_emails));
			$object->SetDBField('Email', $email_to_restore);
			$object->SetDBField('EmailVerified', 1);

			return $object->Update() ? '' : 'restore_impossible';
		}
	}
