Index: branches/5.2.x/core/units/helpers/user_helper.php
===================================================================
--- branches/5.2.x/core/units/helpers/user_helper.php	(revision 16563)
+++ branches/5.2.x/core/units/helpers/user_helper.php	(revision 16564)
@@ -1,725 +1,725 @@
 <?php
 /**
 * @version	$Id$
 * @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') ) {
 				/** @var kPasswordFormatter $password_formatter */
 				$password_formatter = $this->Application->recallObject('kPasswordFormatter');
 
 				if ( !$password_formatter->checkPasswordFromSetting('RootPass', $password) ) {
 					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();
 					$this->_fixNextTemplate();
 				}
 
 				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)
 							$sql = 'SELECT MD5(Password)
 									FROM ' . TABLE_PREFIX . 'Users
 									WHERE PortalUserId = ' . $user_id;
 							$remember_login_hash = $this->Conn->GetOne($sql);
 
 							$this->Application->Session->SetCookie('remember_login', $username . '|' . $remember_login_hash, strtotime('+1 month'));
 						}
 
 						if ( !$remember_login_cookie ) {
 							// reset counters
 							$this->Application->resetCounters('UserSessions');
 
 							$this->_processLoginRedirect($username, $password);
 							$this->_processInterfaceLanguage();
 							$this->_fixNextTemplate();
 						}
 					}
 
 					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->SetVar('u.current_id', $user_id);
 
 			if ( !$this->Application->isAdmin ) {
 				// needed for "profile edit", "registration" forms ON FRONT ONLY
 				$this->Application->SetVar('u_id', $user_id);
 			}
 
 			$this->Application->StoreVar('user_id', $user_id);
 			$this->Application->Session->SetField('PortalUserId', $user_id);
 
 			if ($user_id != USER_ROOT) {
 				$groups = $this->Application->RecallVar('UserGroups');
 				list ($first_group, ) = explode(',', $groups);
 
 				$this->Application->Session->SetField('GroupId', $first_group);
 				$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());
 			}
 
 			$hook_event = new kEvent('u:OnAfterLogin');
 			$hook_event->MasterEvent = $this->event;
 			$this->Application->HandleEvent($hook_event);
 		}
 
 		/**
 		 * 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';
+						WHERE COALESCE(IPRestrictions, "") <> ""';
 				$cache = $this->Conn->GetCol($sql, 'GroupId');
 			}
 
 			return $cache;
 		}
 
 		/**
 		 * Performs user logout
 		 *
 		 */
 		function logoutUser()
 		{
 			if (!isset($this->event)) {
 				$this->event = new kEvent('u:OnLogout');
 			}
 
 			$hook_event = new kEvent('u:OnBeforeLogout');
 			$hook_event->MasterEvent = $this->event;
 			$this->Application->HandleEvent($hook_event);
 
 			$this->_processLoginRedirect();
 
 			$user_id = USER_GUEST;
 			$this->Application->SetVar('u.current_id', $user_id);
 
 			/** @var UsersItem $object */
 			$object = $this->Application->recallObject('u.current', null, Array('skip_autoload' => true));
 
 			$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');
 
 			$this->_fixNextTemplate();
 		}
 
 		/**
 		 * 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)
 		{
 			if ( $remember_login_cookie ) {
 				list ($username, $password) = explode('|', $remember_login_cookie); // 0 - username, 1 - md5(password_hash)
 			}
 
 			$sql = 'SELECT PortalUserId, Password, PasswordHashingMethod
 					FROM ' . TABLE_PREFIX . 'Users
 					WHERE Email = %1$s OR Username = %1$s';
 			$user_info = $this->Conn->GetRow(sprintf($sql, $this->Conn->qstr($username)));
 
 			if ( $user_info ) {
 				if ( $remember_login_cookie ) {
 					return md5($user_info['Password']) == $password;
 				}
 				else {
 					/** @var kPasswordFormatter $password_formatter */
 					$password_formatter = $this->Application->recallObject('kPasswordFormatter');
 
 					$hashing_method = $user_info['PasswordHashingMethod'];
 
 					if ( $password_formatter->checkPassword($password, $user_info['Password'], $hashing_method) ) {
 						if ( $hashing_method != PasswordHashingMethod::PHPPASS ) {
 							$this->_fixUserPassword($user_info['PortalUserId'], $password);
 						}
 
 						return $user_info['PortalUserId'];
 					}
 				}
 			}
 
 			return false;
 		}
 
 		/**
 		 * Apply new password hashing to given user's password
 		 *
 		 * @param int $user_id
 		 * @param string $password
 		 * @return void
 		 * @access protected
 		 */
 		protected function _fixUserPassword($user_id, $password)
 		{
 			/** @var kPasswordFormatter $password_formatter */
 			$password_formatter = $this->Application->recallObject('kPasswordFormatter');
 
 			$fields_hash = Array (
 				'Password' => $password_formatter->hashPassword($password),
 				'PasswordHashingMethod' => PasswordHashingMethod::PHPPASS,
 			);
 
 			$this->Conn->doUpdate($fields_hash, TABLE_PREFIX . 'Users', 'PortalUserId = ' . $user_id);
 		}
 
 		/**
 		 * Process all required data and redirect logged-in user
 		 *
 		 * @param string $username
 		 * @param string $password
 		 * @return void
 		 */
 		protected function _processLoginRedirect($username = null, $password = null)
 		{
 			// 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
 			/** @var UsersSyncronizeManager $sync_manager */
 			$sync_manager = $this->Application->recallObject('UsersSyncronizeManager', null, Array (), Array ('InPortalSyncronize'));
 
 			if ( isset($username) && isset($password) ) {
 				$sync_manager->performAction('LoginUser', $username, $password);
 			}
 			else {
 				$sync_manager->performAction('LogoutUser');
 			}
 		}
 
 		/**
 		 * 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
 		}
 
 		/**
 		 * Injects redirect params into next template, which doesn't happen if next template starts with "external:"
 		 *
 		 * @return void
 		 * @access protected
 		 */
 		protected function _fixNextTemplate()
 		{
 			if ( !MOD_REWRITE || !is_object($this->event) ) {
 				return;
 			}
 
 			// solve problem, when template is "true" instead of actual template name
 			$template = is_string($this->event->redirect) ? $this->event->redirect : '';
 			$url = $this->Application->HREF($template, '', $this->event->getRedirectParams(), $this->event->redirectScript);
 			$vars = $this->Application->parseRewriteUrl($url, 'pass');
 			unset($vars['login'], $vars['logout']);
 
 			// merge back url params, because they were ignored if this was "external:" url
 			$vars = array_merge($vars, $this->getRedirectParams($vars['pass'], 'pass'));
 
 			if ( $template != 'index' ) {
 				// The 'index.html' becomes '', which in turn leads to current page instead of 'index.html'.
 				$template = $vars['t'];
 			}
 
 			unset($vars['is_virtual'], $vars['t']);
 
 			$this->event->redirect = $template;
 			$this->event->setRedirectParams($vars, false);
 		}
 
 		/**
 		 * Returns current event redirect params with given $prefixes injected into 'pass'.
 		 *
 		 * @param array  $prefixes   List of prefixes to inject.
 		 * @param string $pass_name Name of array key in redirect params, containing comma-separated prefixes list.
 		 *
 		 * @return string
 		 * @access protected
 		 */
 		protected function getRedirectParams($prefixes, $pass_name = 'passed')
 		{
 			$redirect_params = $this->event->getRedirectParams();
 
 			if ( isset($redirect_params[$pass_name]) ) {
 				$redirect_prefixes = explode(',', $redirect_params[$pass_name]);
 				$prefixes = array_unique(array_merge($prefixes, $redirect_prefixes));
 			}
 
 			$redirect_params[$pass_name] = implode(',', $prefixes);
 
 			return $redirect_params;
 		}
 
 		/**
 		 * 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
 
 			/** @var UsersItem $object */
 			$object = $this->Application->recallObject($prefix_special, null, Array('skip_autoload' => true));
 
 			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]);
 
 			/** @var UsersItem $object */
 			$object = $this->Application->recallObject('u.email-restore', null, Array ('skip_autoload' => true));
 
 			$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';
 		}
 	}