Index: branches/5.2.x/core/units/users/users_event_handler.php
===================================================================
--- branches/5.2.x/core/units/users/users_event_handler.php	(revision 16773)
+++ branches/5.2.x/core/units/users/users_event_handler.php	(revision 16774)
@@ -1,1946 +1,1952 @@
 <?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 UsersEventHandler extends kDBEventHandler
 	{
 		/**
 		 * Allows to override standard permission mapping
 		 *
 		 * @return void
 		 * @access protected
 		 * @see kEventHandler::$permMapping
 		 */
 		protected function mapPermissions()
 		{
 			parent::mapPermissions();
 
 			$permissions = Array (
 				// admin
 				'OnSetPersistantVariable'	=>	Array('self' => 'view'), // because setting to logged in user only
 				'OnUpdatePassword'			=>	Array('self' => true),
 				'OnSaveSelected'			=>	Array ('self' => 'view'),
 				'OnGeneratePassword'			=>	Array ('self' => 'view'),
 
 				// front
 				'OnRefreshForm'				=>	Array('self' => true),
 
 				'OnForgotPassword'			=>	Array('self' => true),
 
 				'OnSubscribeQuery'			=>	Array('self' => true),
 				'OnSubscribeUser'			=>	Array('self' => true),
 
 				'OnRecommend'				=>	Array('self' => true),
 
 				'OnItemBuild'				=>	Array('self' => true),
 				'OnMassResetSettings'	=> Array('self' => 'edit'),
 				'OnMassCloneUsers'	=> Array('self' => 'add'),
 			);
 
 			$this->permMapping = array_merge($this->permMapping, $permissions);
 		}
 
 		/**
 		 * Builds item (loads if needed)
 		 *
 		 * Pattern: Prototype Manager
 		 *
 		 * @param kEvent $event
 		 * @access protected
 		 */
 		protected function OnItemBuild(kEvent $event)
 		{
 			parent::OnItemBuild($event);
 
 			/** @var kDBItem $object */
 			$object = $event->getObject();
 
 			if ( $event->Special == 'forgot' || $object->getFormName() == 'registration' ) {
 				$this->_makePasswordRequired($event);
 			}
 		}
 
 		/**
 		 * Shows only admins when required
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 * @see kDBEventHandler::OnListBuild()
 		 */
 		protected function SetCustomQuery(kEvent $event)
 		{
 			parent::SetCustomQuery($event);
 
 			/** @var kDBList $object */
 			$object = $event->getObject();
 
 			if ( $event->Special == 'regular' ) {
 				$object->addFilter('primary_filter', '%1$s.UserType = ' . UserType::USER);
 			}
 
 			if ( $event->Special == 'admins' ) {
 				$object->addFilter('primary_filter', '%1$s.UserType = ' . UserType::ADMIN);
 			}
 
 			if ( !$this->Application->isAdminUser ) {
 				$object->addFilter('status_filter', '%1$s.Status = ' . STATUS_ACTIVE);
 			}
 
 			if ( $event->Special == 'online' ) {
 				$object->addFilter('online_users_filter', 's.PortalUserId IS NOT NULL');
 			}
 
 			if ( $event->Special == 'group' ) {
 				$group_id = $this->Application->GetVar('g_id');
 
 				if ( $group_id !== false ) {
 					// show only users, that user doesn't belong to current group
 					$sql = 'SELECT PortalUserId
 							FROM ' . $this->Application->GetTempName(TABLE_PREFIX . 'UserGroupRelations', 'prefix:g') . '
 							WHERE GroupId = ' . (int)$group_id;
 					$user_ids = $this->Conn->GetCol($sql);
 
 					if ( $user_ids ) {
 						$object->addFilter('already_member_filter', '%1$s.PortalUserId NOT IN (' . implode(',', $user_ids) . ')');
 					}
 				}
 			}
 		}
 
 		/**
 		 * Checks user permission to execute given $event
 		 *
 		 * @param kEvent $event
 		 * @return bool
 		 * @access public
 		 */
 		public function CheckPermission(kEvent $event)
 		{
 			if ( $event->Name == 'OnLogin' || $event->Name == 'OnLoginAjax' || $event->Name == 'OnLogout' ) {
 				// permission is checked in OnLogin event directly
 				return true;
 			}
 
 			if ( $event->Name == 'OnResetRootPassword' ) {
 				return defined('DBG_RESET_ROOT') && DBG_RESET_ROOT;
 			}
 
 			if ( $event->Name == 'OnLoginAs' ) {
 				/** @var Session $admin_session */
 				$admin_session = $this->Application->recallObject('Session.admin');
 
 				return $admin_session->LoggedIn();
 			}
 
 			if ( !$this->Application->isAdminUser ) {
 				$user_id = $this->Application->RecallVar('user_id');
 				$items_info = $this->Application->GetVar($event->getPrefixSpecial(true));
 
 				if ( ($event->Name == 'OnCreate' || $event->Name == 'OnRegisterAjax') && $user_id == USER_GUEST ) {
 					// "Guest" can create new users
 					return true;
 				}
 
 				if ( substr($event->Name, 0, 8) == 'OnUpdate' && $user_id > 0 ) {
 					/** @var UsersItem $user_dummy */
 					$user_dummy = $this->Application->recallObject($event->Prefix . '.-item', null, Array ('skip_autoload' => true));
 
 					foreach ($items_info as $id => $field_values) {
 						if ( $id != $user_id ) {
 							// registered users can update their record only
 							return false;
 						}
 
 						$user_dummy->Load($id);
 						$status_field = $user_dummy->getStatusField();
 
 						if ( $user_dummy->GetDBField($status_field) != STATUS_ACTIVE ) {
 							// not active user is not allowed to update his record (he could not activate himself manually)
 							return false;
 						}
 
 						if ( isset($field_values[$status_field]) && $user_dummy->GetDBField($status_field) != $field_values[$status_field] ) {
 							// user can't change status by himself
 							return false;
 						}
 					}
 
 					return true;
 				}
 
 				if ( $event->Name == 'OnResetLostPassword' && $event->Special == 'forgot' && $user_id == USER_GUEST ) {
 					// non-logged in users can reset their password, when reset code is valid
 					return is_numeric($this->getPassedID($event));
 				}
 
 				if ( substr($event->Name, 0, 8) == 'OnUpdate' && $user_id <= 0 ) {
 					// guests are not allowed to update their record, because they don't have it :)
 					return false;
 				}
 			}
 
 			return parent::CheckPermission($event);
 		}
 
 		/**
 		 * Handles session expiration (redirects to valid template)
 		 *
 		 * @param kEvent $event
 		 */
 		function OnSessionExpire($event)
 		{
 			$this->Application->resetCounters('UserSessions');
 
 			// place 2 of 2 (also in kHTTPQuery::getRedirectParams)
 			$admin_url_params = Array (
 				'm_cat_id' => 0, // category means nothing on admin login screen
 				'm_wid' => '', // remove wid, otherwise parent window may add wid to its name breaking all the frameset (for <a> targets)
 				'pass' => 'm', // don't pass any other (except "m") prefixes to admin session expiration template
 				'expired' => 1, // expiration mark to show special error on login screen
 				'no_pass_through' => 1, // this way kApplication::HREF won't add them again
 			);
 
 			if ($this->Application->isAdmin) {
 				$this->Application->Redirect('index', $admin_url_params, '', 'index.php');
 			}
 
 			if ($this->Application->GetVar('admin') == 1) {
 				// Front-End showed in admin's right frame
 				/** @var Session $session_admin */
 				$session_admin = $this->Application->recallObject('Session.admin');
 
 				if (!$session_admin->LoggedIn()) {
 					// front-end session created from admin session & both expired
 					$this->Application->DeleteVar('admin');
 					$this->Application->Redirect('index', $admin_url_params, '', 'admin/index.php');
 				}
 			}
 
 			// Front-End session expiration
 			$get = $this->Application->HttpQuery->getRedirectParams();
 			$t = $this->Application->GetVar('t');
 			$get['js_redirect'] = $this->Application->ConfigValue('UseJSRedirect');
 			$this->Application->Redirect($t ? $t : 'index', $get);
 		}
 
 		/**
 		 * [SCHEDULED TASK] Deletes expired sessions
 		 *
 		 * @param kEvent $event
 		 */
 		function OnDeleteExpiredSessions($event)
 		{
 			if (defined('IS_INSTALL') && IS_INSTALL) {
 				return ;
 			}
 
 			/** @var SessionStorage $session_storage */
 			$session_storage = $this->Application->recallObject('SessionStorage');
 			$session_storage->DeleteExpired();
 		}
 
 		/**
 		 * Checks user data and logs it in if allowed
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function OnLogin($event)
 		{
 			/** @var kDBItem $object */
 			$object = $event->getObject( Array ('form_name' => 'login') );
 
 			$object->SetFieldsFromHash($this->getSubmittedFields($event));
 			$username = $object->GetDBField('UserLogin');
 			$password = $object->GetDBField('UserPassword');
 			$remember_login = $object->GetDBField('UserRememberLogin') == 1;
 
 			/** @var UserHelper $user_helper */
 			$user_helper = $this->Application->recallObject('UserHelper');
 
 			$user_helper->event =& $event;
 			$result = $user_helper->loginUser($username, $password, false, $remember_login);
 
 			if ($result != LoginResult::OK) {
 				$event->status = kEvent::erFAIL;
 				$object->SetError('UserLogin', $result == LoginResult::NO_PERMISSION ? 'no_permission' : 'invalid_password');
 			}
 
 			if ( is_object($event->MasterEvent) && ($event->MasterEvent->Name == 'OnLoginAjax') ) {
 				// used to insert just logged-in user e-mail on "One Step Checkout" form in "Modern Store" theme
 				$user =& $user_helper->getUserObject();
 				$event->SetRedirectParam('user_email', $user->GetDBField('Email'));
 			}
 		}
 
 		/**
 		 * Performs user login from ajax request
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function OnLoginAjax($event)
 		{
 			/** @var AjaxFormHelper $ajax_form_helper */
 			$ajax_form_helper = $this->Application->recallObject('AjaxFormHelper');
 
 			$ajax_form_helper->transitEvent($event, 'OnLogin');
 		}
 
 		/**
 		 * [HOOK] Auto-Logins Front-End user when "Remember Login" cookie is found
 		 *
 		 * @param kEvent $event
 		 */
 		function OnAutoLoginUser($event)
 		{
 			$remember_login_cookie = $this->Application->GetVar('remember_login');
 
 			if (!$remember_login_cookie || $this->Application->isAdmin || $this->Application->LoggedIn()) {
 				return ;
 			}
 
 			/** @var UserHelper $user_helper */
 			$user_helper = $this->Application->recallObject('UserHelper');
 
 			$user_helper->loginUser('', '', false, false, $remember_login_cookie);
 		}
 
 		/**
 		 * Called when user logs in using old in-portal
 		 *
 		 * @param kEvent $event
 		 */
 		function OnInpLogin($event)
 		{
 			/** @var UsersSyncronizeManager $sync_manager */
 			$sync_manager = $this->Application->recallObject('UsersSyncronizeManager', null, Array(), Array ('InPortalSyncronize'));
 
 			$sync_manager->performAction('LoginUser', $event->getEventParam('user'), $event->getEventParam('pass') );
 
 			if ($event->redirect && is_string($event->redirect)) {
 				// some real template specified instead of true
 				$this->Application->Redirect($event->redirect, $event->getRedirectParams());
 			}
 		}
 
 		/**
 		 * Called when user logs in using old in-portal
 		 *
 		 * @param kEvent $event
 		 */
 		function OnInpLogout($event)
 		{
 			/** @var UsersSyncronizeManager $sync_manager */
 			$sync_manager = $this->Application->recallObject('UsersSyncronizeManager', null, Array(), Array ('InPortalSyncronize'));
 
 			$sync_manager->performAction('LogoutUser');
 		}
 
 		/**
 		 * Performs user logout
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function OnLogout($event)
 		{
 			/** @var UserHelper $user_helper */
 			$user_helper = $this->Application->recallObject('UserHelper');
 
 			$user_helper->event =& $event;
 			$user_helper->logoutUser();
 		}
 
 		/**
 		 * Redirects user after successful registration to confirmation template (on Front only)
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function OnAfterItemCreate(kEvent $event)
 		{
 			parent::OnAfterItemCreate($event);
 
 			$this->afterItemChanged($event);
 
 			$this->assignToPrimaryGroup($event);
 		}
 
 		/**
 		 * Performs user registration
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function OnCreate(kEvent $event)
 		{
 			if ( $this->Application->isAdmin ) {
 				parent::OnCreate($event);
 
 				return ;
 			}
 
 			/** @var UsersItem $object */
 			$object = $event->getObject( Array('form_name' => 'registration') );
 
 			$field_values = $this->getSubmittedFields($event);
 			$user_email = getArrayValue($field_values, 'Email');
 			$subscriber_id = $user_email ? $this->getSubscriberByEmail($user_email) : false;
 
 			if ( $subscriber_id ) {
 				// update existing subscriber
 				$object->Load($subscriber_id);
 				$object->SetDBField('PrimaryGroupId', $this->Application->ConfigValue('User_NewGroup'));
 				$this->Application->SetVar($event->getPrefixSpecial(true), Array ($object->GetID() => $field_values));
 			}
 
 			$object->SetFieldsFromHash($field_values);
 			$event->setEventParam('form_data', $field_values);
 
 			$status = $object->isLoaded() ? $object->Update() : $object->Create();
 
 			if ( !$status ) {
 				$event->status = kEvent::erFAIL;
 				$event->redirect = false;
 				$object->setID( (int)$object->GetID() );
 			}
 
 			$this->setNextTemplate($event, true);
 
 			if ( ($event->status == kEvent::erSUCCESS) && $event->redirect ) {
 				$this->assignToPrimaryGroup($event);
 
 				$object->sendEmails();
 				$this->autoLoginUser($event);
 			}
 		}
 
 		/**
 		 * Processes user registration from ajax request
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function OnRegisterAjax(kEvent $event)
 		{
 			/** @var AjaxFormHelper $ajax_form_helper */
 			$ajax_form_helper = $this->Application->recallObject('AjaxFormHelper');
 
 			$ajax_form_helper->transitEvent($event, 'OnCreate', Array ('do_refresh' => 1));
 		}
 
 		/**
 		 * Returns subscribed user ID by given e-mail address
 		 *
 		 * @param string $email
 		 * @return int|bool
 		 * @access protected
 		 */
 		protected function getSubscriberByEmail($email)
 		{
 			/** @var UsersItem $verify_user */
 			$verify_user = $this->Application->recallObject('u.verify', null, Array ('skip_autoload' => true));
 
 			$verify_user->Load($email, 'Email');
 
 			return $verify_user->isLoaded() && $verify_user->isSubscriberOnly() ? $verify_user->GetID() : false;
 		}
 
 		/**
 		 * Login user if possible, if not then redirect to corresponding template
 		 *
 		 * @param kEvent $event
 		 */
 		function autoLoginUser($event)
 		{
 			/** @var UsersItem $object */
 			$object = $event->getObject();
 
 			if ( $object->GetDBField('Status') == STATUS_ACTIVE ) {
 				/** @var UserHelper $user_helper */
 				$user_helper = $this->Application->recallObject('UserHelper');
 
 				$user =& $user_helper->getUserObject();
 				$user->Load($object->GetID());
 
 				if ( $user_helper->checkLoginPermission() ) {
 					$user_helper->loginUserById( $user->GetID() );
 				}
 			}
 		}
 
 		/**
 		 * Set's new unique resource id to user
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function OnBeforeItemCreate(kEvent $event)
 		{
 			parent::OnBeforeItemCreate($event);
 
 			$this->beforeItemChanged($event);
 
 			/** @var kCountryStatesHelper $cs_helper */
 			$cs_helper = $this->Application->recallObject('CountryStatesHelper');
 
 			/** @var UsersItem $object */
 			$object = $event->getObject();
 
 			if ( !$object->isSubscriberOnly() ) {
 				// don't check state-to-country relations for subscribers
 				$cs_helper->CheckStateField($event, 'State', 'Country');
 			}
 
 			if ( $object->getFormName() != 'login' ) {
 				$this->_makePasswordRequired($event);
 			}
 
 			$cs_helper->PopulateStates($event, 'State', 'Country');
 
 			$this->setUserGroup($object);
 
 			/** @var UserHelper $user_helper */
 			$user_helper = $this->Application->recallObject('UserHelper');
 
 			if ( !$user_helper->checkBanRules($object) ) {
 				$object->SetError('Username', 'banned');
 			}
 
 			$object->SetDBField('IPAddress', $this->Application->getClientIp());
 
 			if ( !$this->Application->isAdmin ) {
 				$object->SetDBField('FrontLanguage', $this->Application->GetVar('m_lang'));
 			}
 		}
 
 		/**
 		 * Sets primary group of the user
 		 *
 		 * @param kDBItem $object
 		 */
 		protected function setUserGroup(&$object)
 		{
 			if ($object->Special == 'subscriber') {
 				$object->SetDBField('PrimaryGroupId', $this->Application->ConfigValue('User_SubscriberGroup'));
 
 				return ;
 			}
 
 			// set primary group to user
 			if ( !$this->Application->isAdminUser ) {
 				$group_id = $object->GetDBField('PrimaryGroupId');
 
 				if ($group_id) {
 					// check, that group is allowed for Front-End
 					$sql = 'SELECT GroupId
 							FROM ' . TABLE_PREFIX . 'UserGroups
 							WHERE GroupId = ' . (int)$group_id . ' AND FrontRegistration = 1';
 					$group_id = $this->Conn->GetOne($sql);
 				}
 
 				if (!$group_id) {
 					// when group not selected OR not allowed -> use default group
 					$object->SetDBField('PrimaryGroupId', $this->Application->ConfigValue('User_NewGroup'));
 				}
 			}
 		}
 
 		/**
 		 * Assigns a user to it's primary group
 		 *
 		 * @param kEvent $event
 		 */
 		protected function assignToPrimaryGroup($event)
 		{
 			/** @var kDBItem $object */
 			$object = $event->getObject();
 
 			$primary_group_id = $object->GetDBField('PrimaryGroupId');
 
 			if ($primary_group_id) {
 				$ug_table = TABLE_PREFIX . 'UserGroupRelations';
 
 				if ( $object->IsTempTable() ) {
 					$ug_table = $this->Application->GetTempName($ug_table, 'prefix:' . $event->Prefix);
 				}
 
 				$fields_hash = Array (
 					'PortalUserId' => $object->GetID(),
 					'GroupId' => $primary_group_id,
 				);
 
 				$this->Conn->doInsert($fields_hash, $ug_table, 'REPLACE');
 			}
 		}
 
 		/**
 		 * Set's new unique resource id to user
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function OnAfterItemValidate(kEvent $event)
 		{
 			/** @var kDBItem $object */
 			$object = $event->getObject();
 
 			$resource_id = $object->GetDBField('ResourceId');
 
 			if ( !$resource_id ) {
 				$object->SetDBField('ResourceId', $this->Application->NextResourceId());
 			}
 		}
 
 		/**
 		 * Enter description here...
 		 *
 		 * @param kEvent $event
 		 */
 		function OnRecommend($event)
 		{
 			/** @var kDBItem $object */
 			$object = $event->getObject( Array ('form_name' => 'recommend') );
 
 			$object->SetFieldsFromHash($this->getSubmittedFields($event));
 
 			if ( !$object->ValidateField('RecommendEmail') ) {
 				$event->status = kEvent::erFAIL;
 
 				return ;
 			}
 
 	    	$send_params = Array (
 		    	'to_email' => $object->GetDBField('RecommendEmail'),
 				'to_name' => $object->GetDBField('RecommendEmail'),
 	    	);
 
 			$user_id = $this->Application->RecallVar('user_id');
 			$email_sent = $this->Application->emailUser('USER.SUGGEST', $user_id, $send_params);
 			$this->Application->emailAdmin('USER.SUGGEST');
 
 			if ( $email_sent ) {
 				$event->SetRedirectParam('pass', 'all');
 				$event->redirect = $this->Application->GetVar('template_success');
 			}
 			else {
 				$event->status = kEvent::erFAIL;
 				$object->SetError('RecommendEmail', 'send_error');
 			}
 		}
 
 		/**
 		 * Saves address changes and mades no redirect
 		 *
 		 * @param kEvent $event
 		 */
 		function OnUpdateAddress($event)
 		{
 			/** @var kDBItem $object */
 			$object = $event->getObject(Array ('skip_autoload' => true));
 
 			$items_info = $this->Application->GetVar($event->getPrefixSpecial(true));
 
 			if ( $items_info ) {
 				$id = key($items_info);
 				$field_values = $items_info[$id];
 
 				if ( $id > 0 ) {
 					$object->Load($id);
 				}
 
 				$object->setID($id);
 				$object->SetFieldsFromHash($field_values);
 				$event->setEventParam('form_data', $field_values);
 
 				$object->Validate();
 			}
 
 			/** @var kCountryStatesHelper $cs_helper */
 			$cs_helper = $this->Application->recallObject('CountryStatesHelper');
 
 			$cs_helper->PopulateStates($event, 'State', 'Country');
 
 			$event->redirect = false;
 		}
 
 		/**
 		 * Validate subscriber's email & store it to session -> redirect to confirmation template
 		 *
 		 * @param kEvent $event
 		 */
 		function OnSubscribeQuery($event)
 		{
 			/** @var UsersItem $object */
 			$object = $event->getObject( Array ('form_name' => 'subscription') );
 
 			$object->SetFieldsFromHash($this->getSubmittedFields($event));
 
 			if ( !$object->ValidateField('SubscriberEmail') ) {
 				$event->status = kEvent::erFAIL;
 
 				return ;
 			}
 
 			$user_email = $object->GetDBField('SubscriberEmail');
 			$object->Load($user_email, 'Email');
 			$event->SetRedirectParam('subscriber_email', $user_email);
 
 			if ( $object->isLoaded() && $object->isSubscribed() ) {
 				$event->redirect = $this->Application->GetVar('unsubscribe_template');
 			}
 			else {
 				$event->redirect = $this->Application->GetVar('subscribe_template');
 			}
 
 			$event->SetRedirectParam('pass', 'm');
 		}
 
 		/**
 		 * Subscribe/Unsubscribe user based on email stored in previous step
 		 *
 		 * @param kEvent $event
 		 */
 		function OnSubscribeUser($event)
 		{
 			/** @var UsersItem $object */
 			$object = $event->getObject( Array ('form_name' => 'subscription') );
 
 			$user_email = $this->Application->GetVar('subscriber_email');
 			$object->SetDBField('SubscriberEmail', $user_email);
 
 			if ( !$object->ValidateField('SubscriberEmail') ) {
 				$event->status = kEvent::erFAIL;
 
 				return ;
 			}
 
 			$username_required = $object->isRequired('Username');
 			$this->RemoveRequiredFields($object);
 			$object->Load($user_email, 'Email');
 
 			if ( $object->isLoaded() ) {
 				if ( $object->isSubscribed() ) {
 					if ( $event->getEventParam('no_unsubscribe') ) {
 						// for customization code from FormsEventHandler
 						return ;
 					}
 
 					if ( $object->isSubscriberOnly() ) {
 						/** @var kTempTablesHandler $temp_handler */
 						$temp_handler = $this->Application->recallObject($event->Prefix . '_TempHandler', 'kTempTablesHandler');
 
 						$temp_handler->DeleteItems($event->Prefix, '', Array($object->GetID()));
 					}
 					else {
 						$this->RemoveSubscriberGroup( $object->GetID() );
 					}
 
 					$event->redirect = $this->Application->GetVar('unsubscribe_ok_template');
 				}
 				else {
 					$this->AddSubscriberGroup($object);
 					$event->redirect = $this->Application->GetVar('subscribe_ok_template');
 				}
 			}
 			else {
 				$object->generatePassword();
 				$object->SetDBField('Email', $user_email);
 
 				if ( $username_required )	{
 					$object->SetDBField('Username', str_replace('@', '_at_', $user_email));
 				}
 
 				$object->SetDBField('Status', STATUS_ACTIVE); // make user subscriber Active by default
 
 				if ( $object->Create() ) {
 					$this->AddSubscriberGroup($object);
 					$event->redirect = $this->Application->GetVar('subscribe_ok_template');
 				}
 			}
 		}
 
 		/**
 		 * Adding user to subscribers group
 		 *
 		 * @param UsersItem $object
 		 */
 		function AddSubscriberGroup(&$object)
 		{
 			if ( !$object->isSubscriberOnly() ) {
 				$fields_hash = Array (
 					'PortalUserId' => $object->GetID(),
 					'GroupId' => $this->Application->ConfigValue('User_SubscriberGroup'),
 				);
 
 				$this->Conn->doInsert($fields_hash, TABLE_PREFIX . 'UserGroupRelations');
 			}
 
 			$this->Application->emailAdmin('USER.SUBSCRIBE');
 			$this->Application->emailUser('USER.SUBSCRIBE', $object->GetID());
 		}
 
 		/**
 		 * Removing user from subscribers group
 		 *
 		 * @param int $user_id
 		 */
 		function RemoveSubscriberGroup($user_id)
 		{
 			$group_id = $this->Application->ConfigValue('User_SubscriberGroup');
 
 			$sql = 'DELETE FROM ' . TABLE_PREFIX . 'UserGroupRelations
 					WHERE PortalUserId = ' . $user_id . ' AND GroupId = ' . $group_id;
 			$this->Conn->Query($sql);
 
 			$this->Application->emailAdmin('USER.UNSUBSCRIBE');
 			$this->Application->emailUser('USER.UNSUBSCRIBE', $user_id);
 		}
 
 		/**
 		 * Validates forgot password form and sends password reset confirmation e-mail
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 */
 		function OnForgotPassword($event)
 		{
 			/** @var kDBItem $object */
 			$object = $event->getObject( Array ('form_name' => 'forgot_password') );
 
 			$object->SetFieldsFromHash($this->getSubmittedFields($event));
 
 			/** @var UsersItem $user */
 			$user = $this->Application->recallObject('u.tmp', null, Array ('skip_autoload' => true));
 
 			$found = $allow_reset = false;
 			$email_or_username = $object->GetDBField('ForgotLogin');
 			$is_email = strpos($email_or_username, '@') !== false;
 
 			if ( strlen($email_or_username) ) {
 				$user->Load($email_or_username, $is_email ? 'Email' : 'Username');
 			}
 
 			if ( $user->isLoaded() ) {
 				$min_pwd_reset_delay = $this->Application->ConfigValue('Users_AllowReset');
 				$found = ($user->GetDBField('Status') == STATUS_ACTIVE) && strlen($user->GetDBField('Password'));
 
 				if ( !$user->GetDBField('PwResetConfirm') ) {
 					// no reset made -> allow
 					$allow_reset = true;
 				}
 				else {
 					// reset made -> wait N minutes, then allow
 					$allow_reset = TIMENOW > $user->GetDBField('PwRequestTime') + $min_pwd_reset_delay;
 				}
 			}
 
 			if ( $found && $allow_reset ) {
 				$this->Application->emailUser('USER.PSWDC', $user->GetID());
 				$event->redirect = $this->Application->GetVar('template_success');
 
 				return;
 			}
 
 			if ( strlen($email_or_username) ) {
 				$object->SetError('ForgotLogin', $found ? 'reset_denied' : ($is_email ? 'unknown_email' : 'unknown_username'));
 			}
 
 			if ( !$object->ValidateField('ForgotLogin') ) {
 				$event->status = kEvent::erFAIL;
 			}
 		}
 
 		/**
 		 * Updates kDBItem
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function OnUpdate(kEvent $event)
 		{
 			parent::OnUpdate($event);
 
 			if ( !$this->Application->isAdmin ) {
 				$this->setNextTemplate($event);
 			}
 		}
 
 		/**
 		 * Updates kDBItem via AJAX.
 		 *
 		 * @param kEvent $event Event.
 		 *
 		 * @return void
 		 */
 		protected function OnUpdateAjax(kEvent $event)
 		{
 			/** @var AjaxFormHelper $ajax_form_helper */
 			$ajax_form_helper = $this->Application->recallObject('AjaxFormHelper');
 			$ajax_form_helper->transitEvent($event, 'OnUpdate');
 		}
 
 		/**
 		 * Checks state against country
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function OnBeforeItemUpdate(kEvent $event)
 		{
 			parent::OnBeforeItemUpdate($event);
 
 			$this->beforeItemChanged($event);
 
 			/** @var kCountryStatesHelper $cs_helper */
 			$cs_helper = $this->Application->recallObject('CountryStatesHelper');
 
 			$cs_helper->CheckStateField($event, 'State', 'Country');
 			$cs_helper->PopulateStates($event, 'State', 'Country');
 
 			/** @var kDBItem $object */
 			$object = $event->getObject();
 
 			if ( $event->Special == 'forgot' ) {
 				$object->SetDBField('PwResetConfirm', '');
 				$object->SetDBField('PwRequestTime_date', NULL);
 				$object->SetDBField('PwRequestTime_time', NULL);
 			}
 
 			$changed_fields = array_keys($object->GetChangedFields());
 
 			if ( $changed_fields && !in_array('Modified', $changed_fields) ) {
 				$object->SetDBField('Modified_date', adodb_mktime());
 				$object->SetDBField('Modified_time', adodb_mktime());
 			}
 
 			if ( !$this->Application->isAdmin && in_array('Email', $changed_fields) && ($event->Special != 'email-restore') ) {
 				$object->SetDBField('EmailVerified', 0);
 			}
 		}
 
 		/**
 		 * Occurs before item is changed
 		 *
 		 * @param kEvent $event
 		 */
 		function beforeItemChanged($event)
 		{
 			/** @var UsersItem $object */
 			$object = $event->getObject();
 
 			if ( !$this->Application->isAdmin && $object->getFormName() == 'registration' ) {
 				// sets new user's status based on config options
 				$status_map = Array (1 => STATUS_ACTIVE, 2 => STATUS_DISABLED, 3 => STATUS_PENDING, 4 => STATUS_PENDING);
 				$object->SetDBField('Status', $status_map[ $this->Application->ConfigValue('User_Allow_New') ]);
 
 				if ( $this->Application->ConfigValue('User_Password_Auto') ) {
 					$object->generatePassword( rand(5, 8) );
 				}
 
 				if ( $this->Application->ConfigValue('RegistrationCaptcha') ) {
 					/** @var kCaptchaHelper $captcha_helper */
 					$captcha_helper = $this->Application->recallObject('CaptchaHelper');
 
 					$captcha_helper->validateCode($event, false);
 				}
 
 				if ( $event->Name == 'OnBeforeItemUpdate' ) {
 					// when a subscriber-only users performs normal registration, then assign him to Member group
 					$this->setUserGroup($object);
 				}
 			}
 		}
 
 		/**
 		 * Sets redirect template based on user status & user request contents
 		 *
 		 * @param kEvent $event
 		 * @param bool $for_registration
 		 */
 		function setNextTemplate($event, $for_registration = false)
 		{
 			$event->SetRedirectParam('opener', 's');
 
 			/** @var UsersItem $object */
 			$object = $event->getObject();
 
 			$next_template = false;
 
 			if ( $object->GetDBField('Status') == STATUS_ACTIVE && $this->Application->GetVar('next_template') ) {
 				$next_template = $this->Application->GetVar('next_template');
 			}
 			elseif ( $for_registration ) {
 				switch ( $this->Application->ConfigValue('User_Allow_New') ) {
 					case 1: // Immediate
 						$next_template = $this->Application->GetVar('registration_confirm_template');
 						break;
 
 					case 3: // Upon Approval
 					case 4: // Email Activation
 						$next_template = $this->Application->GetVar('registration_confirm_pending_template');
 						break;
 				}
 			}
 
 			if ($next_template) {
 				$event->redirect = $next_template;
 			}
 		}
 
 		/**
 		 * Delete users from groups if their membership is expired
 		 *
 		 * @param kEvent $event
 		 */
 		function OnCheckExpiredMembership($event)
 		{
 			// send pre-expiration reminders: begin
 			$pre_expiration = adodb_mktime() + $this->Application->ConfigValue('User_MembershipExpirationReminder') * 3600 * 24;
 			$sql = 'SELECT PortalUserId, GroupId
 					FROM '.TABLE_PREFIX.'UserGroupRelations
 					WHERE (MembershipExpires IS NOT NULL) AND (ExpirationReminderSent = 0) AND (MembershipExpires < '.$pre_expiration.')';
 
 			$skip_clause = $event->getEventParam('skip_clause');
 			if ($skip_clause) {
 				$sql .= ' AND !('.implode(') AND !(', $skip_clause).')';
 			}
 
 			$records = $this->Conn->Query($sql);
 			if ($records) {
 				$conditions = Array();
 				foreach ($records as $record) {
 					$this->Application->emailUser('USER.MEMBERSHIP.EXPIRATION.NOTICE', $record['PortalUserId']);
 					$this->Application->emailAdmin('USER.MEMBERSHIP.EXPIRATION.NOTICE');
 					$conditions[] = '(PortalUserId = '.$record['PortalUserId'].' AND GroupId = '.$record['GroupId'].')';
 				}
 				$sql = 'UPDATE '.TABLE_PREFIX.'UserGroupRelations
 						SET ExpirationReminderSent = 1
 						WHERE '.implode(' OR ', $conditions);
 				$this->Conn->Query($sql);
 			}
 			// send pre-expiration reminders: end
 
 			// remove users from groups with expired membership: begin
 			$sql = 'SELECT PortalUserId
 					FROM '.TABLE_PREFIX.'UserGroupRelations
 					WHERE (MembershipExpires IS NOT NULL) AND (MembershipExpires < '.adodb_mktime().')';
 			$user_ids = $this->Conn->GetCol($sql);
 			if ($user_ids) {
 				foreach ($user_ids as $id) {
 					$this->Application->emailUser('USER.MEMBERSHIP.EXPIRED', $id);
 					$this->Application->emailAdmin('USER.MEMBERSHIP.EXPIRED');
 				}
 			}
 			$sql = 'DELETE FROM '.TABLE_PREFIX.'UserGroupRelations
 					WHERE (MembershipExpires IS NOT NULL) AND (MembershipExpires < '.adodb_mktime().')';
 			$this->Conn->Query($sql);
 			// remove users from groups with expired membership: end
 		}
 
 		/**
 		 * Used to keep user registration form data, while showing affiliate registration form fields
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function OnRefreshForm($event)
 		{
 			$event->redirect = false;
 			$item_info = $this->Application->GetVar( $event->getPrefixSpecial(true) );
 			$id = key($item_info);
 			$field_values = $item_info[$id];
 
 			/** @var kDBItem $object */
 			$object = $event->getObject( Array ('skip_autoload' => true) );
 
 			$object->IgnoreValidation = true;
 
 			$object->setID($id);
 			$object->SetFieldsFromHash($field_values);
 			$event->setEventParam('form_data', $field_values);
 		}
 
 		/**
 		 * Sets persistant variable
 		 *
 		 * @param kEvent $event
 		 */
 		function OnSetPersistantVariable($event)
 		{
 			$field =  $this->Application->GetVar('field');
 			$value = $this->Application->GetVar('value');
 			$this->Application->StorePersistentVar($field, $value);
 
 			$force_tab = $this->Application->GetVar('SetTab');
 			if ($force_tab) {
 				$this->Application->StoreVar('force_tab', $force_tab);
 			}
 		}
 
 		/**
 		 * Return user from order by special .ord
 		 *
 		 * @param kEvent $event
 		 * @return int
 		 * @access public
 		 */
 		public function getPassedID(kEvent $event)
 		{
 			switch ($event->Special) {
 				case 'ord':
 					/** @var OrdersItem $order */
 					$order = $this->Application->recallObject('ord');
 
 					return $order->GetDBField('PortalUserId');
 					break;
 
 				case 'profile':
 					$id = $this->Application->GetVar('user_id');
 
 					if ( $id ) {
 						$event->setEventParam(kEvent::FLAG_ID_FROM_REQUEST, true);
 
 						return $id;
 					}
 
 					// If none user_id given use current user id.
 					return $this->Application->RecallVar('user_id');
 					break;
 
 				case 'forgot':
 					/** @var UserHelper $user_helper */
 					$user_helper = $this->Application->recallObject('UserHelper');
 
 					$id = $user_helper->validateUserCode($this->Application->GetVar('user_key'), 'forgot_password');
 
 					if ( is_numeric($id) ) {
 						return $id;
 					}
 					break;
 			}
 
 			if ( preg_match('/^(login|register|recommend|subscribe|forgot)/', $event->Special) ) {
 				// this way we can have 2+ objects stating with same special, e.g. "u.login-sidebox" and "u.login-main"
 				return USER_GUEST;
 			}
 			elseif ( preg_match('/^(update|delete)/', $event->Special) ) {
 				// This way we can have 2+ objects stating with same special, e.g. "u.update-sidebox" and "u.update-profile".
 				return $this->Application->RecallVar('user_id');
 			}
 
 			return parent::getPassedID($event);
 		}
 
 		/**
 		 * Allows to change root password
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function OnUpdatePassword($event)
 		{
 			$items_info = $this->Application->GetVar($event->getPrefixSpecial(true));
 			if ( !$items_info ) {
 				return;
 			}
 
 			$id = key($items_info);
 			$field_values = $items_info[$id];
 			$user_id = $this->Application->RecallVar('user_id');
 
 			if ( $id == $user_id && ($user_id > 0 || $user_id == USER_ROOT) ) {
 				/** @var kDBItem $user_dummy */
 				$user_dummy = $this->Application->recallObject($event->Prefix . '.-item', null, Array ('skip_autoload' => true));
 
 				$user_dummy->Load($id);
 				$status_field = $user_dummy->getStatusField();
 
 				if ( $user_dummy->GetDBField($status_field) != STATUS_ACTIVE ) {
 					// not active user is not allowed to update his record (he could not activate himself manually)
 					return ;
 				}
 			}
 
 			if ( $user_id == USER_ROOT ) {
 				/** @var UsersItem $object */
 				$object = $event->getObject(Array ('skip_autoload' => true));
 
 				// this is internal hack to allow root/root passwords for dev
 				if ( $this->Application->isDebugMode() && $field_values['RootPassword'] == 'root' ) {
 					$object->SetFieldOption('RootPassword', 'min_length', 4);
 				}
 
 				$this->RemoveRequiredFields($object);
 				$object->SetDBField('RootPassword', $this->Application->ConfigValue('RootPass'));
 
 				$object->setID(-1);
 				$object->SetFieldsFromHash($field_values);
 				$event->setEventParam('form_data', $field_values);
 
 				if ( $object->Validate() ) {
 					// validation on, password match too
 					$fields_hash = Array ('VariableValue' => $object->GetDBField('RootPassword'));
 					$conf_table = $this->Application->getUnitOption('conf', 'TableName');
 					$this->Conn->doUpdate($fields_hash, $conf_table, 'VariableName = "RootPass"');
 					$event->SetRedirectParam('opener', 'u');
 				}
 				else {
 					$event->status = kEvent::erFAIL;
 					$event->redirect = false;
 					return ;
 				}
 			}
 			else {
 				/** @var kDBItem $object */
 				$object = $event->getObject();
 
 				$object->SetFieldsFromHash($field_values);
 				$event->setEventParam('form_data', $field_values);
 
 				if ( !$object->Update() ) {
 					$event->status = kEvent::erFAIL;
 					$event->redirect = false;
 				}
 			}
 
 			$event->SetRedirectParam('opener', 'u');
 		}
 
 		/**
 		 * Resets grid settings, remembered in each user record
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function OnMassResetSettings($event)
 		{
 			if ( $this->Application->CheckPermission('SYSTEM_ACCESS.READONLY', 1) ) {
 				$event->status = kEvent::erFAIL;
 				return;
 			}
 
 			$ids = $this->StoreSelectedIDs($event);
 
 			$default_user_id = $this->Application->ConfigValue('DefaultSettingsUserId');
 			if ( in_array($default_user_id, $ids) ) {
 				array_splice($ids, array_search($default_user_id, $ids), 1);
 			}
 
 			if ( $ids ) {
 				$q = 'DELETE FROM ' . TABLE_PREFIX . 'UserPersistentSessionData WHERE PortalUserId IN (' . join(',', $ids) . ') AND
 							 (VariableName LIKE "%_columns_%"
 							 OR
 							 VariableName LIKE "%_filter%"
 							 OR
 							 VariableName LIKE "%_PerPage%")';
 				$this->Conn->Query($q);
 			}
 
 			$this->clearSelectedIDs($event);
 		}
 
 		/**
 		 * Checks, that currently loaded item is allowed for viewing (non permission-based)
 		 *
 		 * @param kEvent $event
 		 * @return bool
 		 * @access protected
 		 */
 		protected function checkItemStatus(kEvent $event)
 		{
 			/** @var kDBItem $object */
 			$object = $event->getObject();
 
 			if ( !$object->isLoaded() ) {
 				return true;
 			}
 
 			$virtual_users = Array (USER_ROOT, USER_GUEST);
 
 			return ($object->GetDBField('Status') == STATUS_ACTIVE) || in_array($object->GetID(), $virtual_users);
 		}
 
 		/**
 		 * Sends approved/declined email event on user status change
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function OnAfterItemUpdate(kEvent $event)
 		{
 			parent::OnAfterItemUpdate($event);
 
 			$this->afterItemChanged($event);
 
 			/** @var UsersItem $object */
 			$object = $event->getObject();
 
 			if ( !$this->Application->isAdmin && ($event->Special != 'email-restore') ) {
 				$this->sendEmailChangeEvent($event);
 			}
 
 			if ( !$this->Application->isAdmin || $object->IsTempTable() ) {
 				return;
 			}
 
 			$this->sendStatusChangeEvent($object->GetID(), $object->GetOriginalField('Status'), $object->GetDBField('Status'));
 		}
 
 		/**
 		 * Occurs, after item is changed
 		 *
 		 * @param kEvent $event
 		 */
 		protected function afterItemChanged($event)
 		{
 			$this->saveUserImages($event);
 
 			/** @var UsersItem $object */
 			$object = $event->getObject();
 
 			if ( $object->GetDBField('EmailPassword') && $object->GetDBField('Password_plain') ) {
 				$email_passwords = $this->Application->RecallVar('email_passwords');
 				$email_passwords = $email_passwords ? unserialize($email_passwords) : Array ();
 
 				$email_passwords[ $object->GetID() ] = $object->GetDBField('Password_plain');
 				$this->Application->StoreVar('email_passwords', serialize($email_passwords));
 			}
 
 			// update user subscription status (via my profile or new user registration)
 			if ( !$this->Application->isAdmin && !$object->isSubscriberOnly() ) {
 				if ( $object->GetDBField('SubscribeToMailing') && !$object->isSubscribed() ) {
 					$this->AddSubscriberGroup($object);
 				}
 				elseif ( !$object->GetDBField('SubscribeToMailing') && $object->isSubscribed() ) {
 					$this->RemoveSubscriberGroup( $object->GetID() );
 				}
 			}
 		}
 
 		/**
 		 * Stores user's original Status before overwriting with data from temp table
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function OnBeforeDeleteFromLive(kEvent $event)
 		{
 			parent::OnBeforeDeleteFromLive($event);
 
 			$user_id = $event->getEventParam('id');
 			$user_status = $this->Application->GetVar('user_status', Array ());
 
 			if ( $user_id > 0 ) {
 				$user_status[$user_id] = $this->getUserStatus($user_id);
 				$this->Application->SetVar('user_status', $user_status);
 			}
 		}
 
 		/**
 		 * Sends approved/declined email event on user status change (in temp tables during editing)
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function OnAfterCopyToLive(kEvent $event)
 		{
 			parent::OnAfterCopyToLive($event);
 
 			$temp_id = $event->getEventParam('temp_id');
 			$email_passwords = $this->Application->RecallVar('email_passwords');
 
 			if ( $email_passwords ) {
 				$email_passwords = unserialize($email_passwords);
 
 				if ( isset($email_passwords[$temp_id]) ) {
 					/** @var kDBItem $object */
 					$object = $event->getObject();
 
 					$object->SwitchToLive();
 					$object->Load( $event->getEventParam('id') );
 					$object->SetField('Password', $email_passwords[$temp_id]);
 					$object->SetField('VerifyPassword', $email_passwords[$temp_id]);
 
 					$this->Application->emailUser($temp_id > 0 ? 'USER.NEW.PASSWORD': 'USER.ADD.BYADMIN', $object->GetID());
 
 					unset($email_passwords[$temp_id]);
 					$this->Application->StoreVar('email_passwords', serialize($email_passwords));
 				}
 			}
 
 			if ( $temp_id > 0 ) {
 				// only send status change e-mail on user update
 				$new_status = $this->getUserStatus($temp_id);
 				$user_status = $this->Application->GetVar('user_status');
 
 				$this->sendStatusChangeEvent($temp_id, $user_status[$temp_id], $new_status);
 			}
 		}
 
 		/**
 		 * Returns user status (active, pending, disabled) based on ID and temp mode setting
 		 *
 		 * @param int $user_id
 		 * @return int
 		 */
 		function getUserStatus($user_id)
 		{
 			$id_field = $this->Application->getUnitOption($this->Prefix, 'IDField');
 			$table_name = $this->Application->getUnitOption($this->Prefix, 'TableName');
 
 			$sql = 'SELECT Status
 					FROM '.$table_name.'
 					WHERE '.$id_field.' = '.$user_id;
 			return $this->Conn->GetOne($sql);
 		}
 
 		/**
 		 * Sends approved/declined email event on user status change
 		 *
 		 * @param int $user_id
 		 * @param int $prev_status
 		 * @param int $new_status
 		 */
 		function sendStatusChangeEvent($user_id, $prev_status, $new_status)
 		{
 			$status_events = Array (
 				STATUS_ACTIVE	=>	'USER.APPROVE',
 				STATUS_DISABLED	=>	'USER.DENY',
 			);
 			$email_event = isset($status_events[$new_status]) ? $status_events[$new_status] : false;
 
 			if (($prev_status != $new_status) && $email_event) {
 				$this->Application->emailUser($email_event, $user_id);
 				$this->Application->emailAdmin($email_event);
 			}
 
 			// deletes sessions from users, that are no longer active
 			if (($prev_status != $new_status) && ($new_status != STATUS_ACTIVE)) {
 				$sql = 'SELECT SessionKey
 						FROM ' . TABLE_PREFIX . 'UserSessions
 						WHERE PortalUserId = ' . $user_id;
 				$session_ids = $this->Conn->GetCol($sql);
 
 				$this->Application->Session->DeleteSessions($session_ids);
 			}
 		}
 
 		/**
 		 * Sends restore/validation email event on user email change
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function sendEmailChangeEvent(kEvent $event)
 		{
 			/** @var UsersItem $object */
 			$object = $event->getObject();
 
 			$new_email = $object->GetDBField('Email');
 			$prev_email = $object->GetOriginalField('Email');
 
 			if ( !$new_email || ($prev_email == $new_email) ) {
 				return;
 			}
 
 			$prev_emails = $object->GetDBField('PrevEmails');
 			$prev_emails = $prev_emails ? unserialize($prev_emails) : Array ();
 
 			$fields_hash = Array (
 				'PrevEmails' => serialize($prev_emails),
 				'EmailVerified' => 0,
 			);
 
 			$user_id = $object->GetID();
 
 			if ( $prev_email ) {
 				$hash = md5(TIMENOW + $user_id);
 				$prev_emails[$hash] = $prev_email;
 				$fields_hash['PrevEmails'] = serialize($prev_emails);
 
 				$send_params = Array (
 					'hash' => $hash,
 					'to_email' => $prev_email,
 					'to_name' => trim($object->GetDBField('FirstName') . ' ' . $object->GetDBField('LastName')),
 				);
 
 				$this->Application->emailUser('USER.EMAIL.CHANGE.UNDO', null, $send_params);
 			}
 
 			if ( $new_email ) {
 				$this->Application->emailUser('USER.EMAIL.CHANGE.VERIFY', $user_id);
 			}
 
 			// direct DB update, since USER.EMAIL.CHANGE.VERIFY puts verification code in user record, that we don't want to loose
 			$this->Conn->doUpdate($fields_hash, $object->TableName, 'PortalUserId = ' . $user_id);
 		}
 
 		/**
 		 * OnAfterConfigRead for users
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function OnAfterConfigRead(kEvent $event)
 		{
 			parent::OnAfterConfigRead($event);
 
 			$forms = $this->Application->getUnitOption($event->Prefix, 'Forms');
 			$form_fields =& $forms['default']['Fields'];
 
 			// 1. arrange user registration countries
 			/** @var SiteHelper $site_helper */
 			$site_helper = $this->Application->recallObject('SiteHelper');
 
 			$first_country = $site_helper->getDefaultCountry('', false);
 
 			if ($first_country === false) {
 				$first_country = $this->Application->ConfigValue('User_Default_Registration_Country');
 			}
 
 			if ($first_country) {
 				// update user country dropdown sql
 				$form_fields['Country']['options_sql'] = preg_replace('/ORDER BY (.*)/', 'ORDER BY IF (CountryStateId = '.$first_country.', 1, 0) DESC, \\1', $form_fields['Country']['options_sql']);
 			}
 
 			// 2. set default user registration group
 			$form_fields['PrimaryGroupId']['default'] = $this->Application->ConfigValue('User_NewGroup');
 
 			// 3. allow avatar upload on Front-End
 			/** @var FileHelper $file_helper */
 			$file_helper = $this->Application->recallObject('FileHelper');
 
 			$file_helper->createItemFiles($event->Prefix, true); // create image fields
 
 			if ($this->Application->isAdminUser) {
 				// 4. when in administrative console, then create all users with Active status
 				$form_fields['Status']['default'] = STATUS_ACTIVE;
 
 				// 5. remove groups tab on editing forms when AdvancedUserManagement config variable not set
 				if (!$this->Application->ConfigValue('AdvancedUserManagement')) {
 					$edit_tab_presets = $this->Application->getUnitOption($event->Prefix, 'EditTabPresets');
 
 					foreach ($edit_tab_presets as $preset_name => $preset_tabs) {
 						if (array_key_exists('groups', $preset_tabs)) {
 							unset($edit_tab_presets[$preset_name]['groups']);
 
 							if (count($edit_tab_presets[$preset_name]) == 1) {
 								// only 1 tab left -> remove it too
 								$edit_tab_presets[$preset_name] = Array ();
 							}
 						}
 					}
 
 					$this->Application->setUnitOption($event->Prefix, 'EditTabPresets', $edit_tab_presets);
 				}
 			}
 
 			if ( $this->Application->ConfigValue('RegistrationUsernameRequired') ) {
 				// Username becomes required only, when it's used in registration process
 				$max_username = $this->Application->ConfigValue('MaxUserName');
 
 				$form_fields['Username']['required'] = 1;
 				$form_fields['Username']['min_len'] = $this->Application->ConfigValue('Min_UserName');
 				$form_fields['Username']['max_len'] = $max_username ? $max_username : 255;
 			}
 
 			$this->Application->setUnitOption($event->Prefix, 'Forms', $forms);
 		}
 
 		/**
 		 * OnMassCloneUsers
 		 *
 		 * @param kEvent $event
 		 */
 		function OnMassCloneUsers($event)
 		{
 			if ($this->Application->CheckPermission('SYSTEM_ACCESS.READONLY', 1)) {
 				$event->status = kEvent::erFAIL;
 				return;
 			}
 
 			/** @var kTempTablesHandler $temp_handler */
 			$temp_handler = $this->Application->recallObject($event->Prefix.'_TempHandler', 'kTempTablesHandler');
 
 			$ids = $this->StoreSelectedIDs($event);
 			$temp_handler->CloneItems($event->Prefix, '', $ids);
 			$this->clearSelectedIDs($event);
 		}
 
 		/**
 		 * When cloning users, reset password (set random)
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function OnBeforeClone(kEvent $event)
 		{
 			parent::OnBeforeClone($event);
 
 			/** @var UsersItem $object */
 			$object = $event->getObject();
 
 			$object->generatePassword();
 			$object->SetDBField('ResourceId', 0); // this will reset it
 
 			// change email because it should be unique
 			$object->NameCopy(Array (), $object->GetID(), 'Email', 'copy%1$s.%2$s');
 		}
 
 		/**
 		 * Saves selected ids to session
 		 *
 		 * @param kEvent $event
 		 */
 		function OnSaveSelected($event)
 		{
 			$this->StoreSelectedIDs($event);
 
 			// remove current ID, otherwise group selector will use it in filters
 			$this->Application->DeleteVar($event->getPrefixSpecial(true) . '_id');
 		}
 
 		/**
 		 * Sets primary group of selected users
 		 *
 		 * @param kEvent $event
 		 */
 		function OnProcessSelected($event)
 		{
 			$event->SetRedirectParam('opener', 'u');
 			$user_ids = $this->getSelectedIDs($event, true);
 			$this->clearSelectedIDs($event);
 
 			$dst_field = $this->Application->RecallVar('dst_field');
 			if ( $dst_field != 'PrimaryGroupId' ) {
 				return;
 			}
 
 			$group_ids = array_keys($this->Application->GetVar('g'));
 			$primary_group_id = $group_ids ? array_shift($group_ids) : false;
 
 			if ( !$user_ids || !$primary_group_id ) {
 				return;
 			}
 
 			$table_name = $this->Application->getUnitOption('ug', 'TableName');
 
 			// 1. mark group as primary
 			$sql = 'UPDATE ' . TABLE_PREFIX . 'Users
 					SET PrimaryGroupId = ' . $primary_group_id . '
 					WHERE PortalUserId IN (' . implode(',', $user_ids) . ')';
 			$this->Conn->Query($sql);
 
+			foreach ( $user_ids as $user_id ) {
+				$this->Application->incrementCacheSerial('u', $user_id);
+			}
+
+			$this->Application->incrementCacheSerial('u');
+
 			$sql = 'SELECT PortalUserId
 					FROM ' . $table_name . '
 					WHERE (GroupId = ' . $primary_group_id . ') AND (PortalUserId IN (' . implode(',', $user_ids) . '))';
 			$existing_members = $this->Conn->GetCol($sql);
 
 			// 2. add new members to a group
 			$new_members = array_diff($user_ids, $existing_members);
 
 			foreach ($new_members as $user_id) {
 				$fields_hash = Array (
 					'GroupId' => $primary_group_id,
 					'PortalUserId' => $user_id,
 				);
 
 				$this->Conn->doInsert($fields_hash, $table_name);
 			}
 		}
 
 		/**
 		 * Loads user images
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function OnAfterItemLoad(kEvent $event)
 		{
 			parent::OnAfterItemLoad($event);
 
 			// linking existing images for item with virtual fields
 			/** @var ImageHelper $image_helper */
 			$image_helper = $this->Application->recallObject('ImageHelper');
 
 			/** @var UsersItem $object */
 			$object = $event->getObject();
 
 			$image_helper->LoadItemImages($object);
 
 			/** @var kCountryStatesHelper $cs_helper */
 			$cs_helper = $this->Application->recallObject('CountryStatesHelper');
 
 			$cs_helper->PopulateStates($event, 'State', 'Country');
 
 			// get user subscription status
 			$object->SetDBField('SubscribeToMailing', $object->isSubscribed() ? 1 : 0);
 
 			if ( !$this->Application->isAdmin ) {
 				$object->SetFieldOption('FrontLanguage', 'options', $this->getEnabledLanguages());
 			}
 		}
 
 		/**
 		 * Returns list of enabled languages with their names
 		 *
 		 * @return Array
 		 * @access protected
 		 */
 		protected function getEnabledLanguages()
 		{
 			$cache_key = 'user_languages[%LangSerial%]';
 
 			$ret = $this->Application->getCache($cache_key);
 
 			if ( $ret === false ) {
 				/** @var kDBList $languages */
 				$languages = $this->Application->recallObject('lang.enabled', 'lang_List');
 
 				$ret = Array ();
 
 				foreach ($languages as $language_info) {
 					$ret[$languages->GetID()] = $language_info['LocalName'];
 				}
 
 				$this->Application->setCache($cache_key, $ret);
 			}
 
 			return $ret;
 		}
 
 		/**
 		 * Save user images
 		 *
 		 * @param kEvent $event
 		 */
 		function saveUserImages($event)
 		{
 			if (!$this->Application->isAdmin) {
 				/** @var ImageHelper $image_helper */
 				$image_helper = $this->Application->recallObject('ImageHelper');
 
 				/** @var kDBItem $object */
 				$object = $event->getObject();
 
 				// process image upload in virtual fields
 				$image_helper->SaveItemImages($object);
 			}
 		}
 
 		/**
 		 * Makes password required for new users
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function OnPreCreate(kEvent $event)
 		{
 			parent::OnPreCreate($event);
 
 			if ( $event->status != kEvent::erSUCCESS ) {
 				return;
 			}
 
 			/** @var kDBItem $object */
 			$object = $event->getObject();
 
 			$user_type = $this->Application->GetVar('user_type');
 
 			if ( $user_type ) {
 				$object->SetDBField('UserType', $user_type);
 
 				if ( $user_type == UserType::ADMIN ) {
 					$object->SetDBField('PrimaryGroupId', $this->Application->ConfigValue('User_AdminGroup'));
 				}
 			}
 
 			if ( $this->Application->ConfigValue('User_Password_Auto') ) {
 				$object->SetDBField('EmailPassword', 1);
 			}
 
 			$this->_makePasswordRequired($event);
 		}
 
 		/**
 		 * Makes password required for new users
 		 *
 		 * @param kEvent $event
 		 */
 		function _makePasswordRequired($event)
 		{
 			/** @var kDBItem $object */
 			$object = $event->getObject();
 
 			$required_fields = Array ('Password', 'Password_plain', 'VerifyPassword', 'VerifyPassword_plain');
 			$object->setRequired($required_fields);
 		}
 
 		/**
 		 * Load item if id is available
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function LoadItem(kEvent $event)
 		{
 			$id = $this->getPassedID($event);
 
 			if ( $id < 0 ) {
 				// when root, guest and so on
 
 				/** @var kDBItem $object */
 				$object = $event->getObject();
 
 				$object->Clear($id);
 				return;
 			}
 
 			parent::LoadItem($event);
 		}
 
 		/**
 		 * Occurs just after login (for hooking)
 		 *
 		 * @param kEvent $event
 		 */
 		function OnAfterLogin($event)
 		{
 			if ( is_object($event->MasterEvent) && !$this->Application->isAdmin ) {
 				$event->MasterEvent->SetRedirectParam('login', 1);
 			}
 		}
 
 		/**
 		 * Occurs just before logout (for hooking)
 		 *
 		 * @param kEvent $event
 		 */
 		function OnBeforeLogout($event)
 		{
 			if ( is_object($event->MasterEvent) && !$this->Application->isAdmin ) {
 				$event->MasterEvent->SetRedirectParam('logout', 1);
 			}
 		}
 
 		/**
 		 * Generates password
 		 *
 		 * @param kEvent $event
 		 */
 		function OnGeneratePassword($event)
 		{
 			$event->status = kEvent::erSTOP;
 
 			if ( $this->Application->isAdminUser ) {
 				echo kUtil::generatePassword();
 			}
 		}
 
 		/**
 		 * Changes user's password and logges him in
 		 *
 		 * @param kEvent $event
 		 */
 		function OnResetLostPassword($event)
 		{
 			/** @var kDBItem $object */
 			$object = $event->getObject();
 
 			$event->CallSubEvent('OnUpdate');
 
 			if ( $event->status == kEvent::erSUCCESS ) {
 				/** @var UserHelper $user_helper */
 				$user_helper = $this->Application->recallObject('UserHelper');
 
 			    $user =& $user_helper->getUserObject();
 				$user->Load( $object->GetID() );
 
 				if ( $user_helper->checkLoginPermission() ) {
 		    		$user_helper->loginUserById( $user->GetID() );
 				}
 			}
 		}
 
 		/**
 		 * Generates new Root password and email it
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function OnResetRootPassword($event)
 		{
 			/** @var kPasswordFormatter $password_formatter */
 			$password_formatter = $this->Application->recallObject('kPasswordFormatter');
 
 			$new_root_password = kUtil::generatePassword();
 
 			$this->Application->SetConfigValue('RootPass', $password_formatter->hashPassword($new_root_password));
 			$this->Application->emailAdmin('ROOT.RESET.PASSWORD', null, Array ('password' => $new_root_password));
 
 			$event->SetRedirectParam('reset', 1);
 			$event->SetRedirectParam('pass', 'm');
 		}
 
 		/**
 		 * Perform login of user, selected in Admin Console, on Front-End in a separate window
 		 *
 		 * @param kEvent $event
 		 * @return void
 		 * @access protected
 		 */
 		protected function OnLoginAs(kEvent $event)
 		{
 			/** @var UserHelper $user_helper */
 			$user_helper = $this->Application->recallObject('UserHelper');
 
 			$user =& $user_helper->getUserObject();
 			$user->Load( $this->Application->GetVar('user_id') );
 
 			if ( !$user->isLoaded() ) {
 				return ;
 			}
 
 			if ( $user_helper->checkLoginPermission() ) {
 				$user_helper->loginUserById( $user->GetID() );
 			}
 		}
 	}
Index: branches/5.2.x/core/units/helpers/user_helper.php
===================================================================
--- branches/5.2.x/core/units/helpers/user_helper.php	(revision 16773)
+++ branches/5.2.x/core/units/helpers/user_helper.php	(revision 16774)
@@ -1,725 +1,753 @@
 <?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 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 ' . (strpos($username, '@') === false ? 'Username' : 'Email') . ' = %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 ? $user_info['PortalUserId'] : false;
 				}
 				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';
 		}
+
+		/**
+		 * Returns user's primary group.
+		 *
+		 * @param integer $user_id User ID.
+		 *
+		 * @return integer
+		 */
+		public function getPrimaryGroup($user_id)
+		{
+			if ( $user_id <= 0 ) {
+				return $this->Application->ConfigValue('User_LoggedInGroup');
+			}
+
+			$cache_key = 'user' . $user_id . '_primary_group[%UIDSerial:' . $user_id . '%]';
+			$cache_value = $this->Application->getCache($cache_key);
+
+			if ( $cache_value === false ) {
+				$sql = 'SELECT PrimaryGroupId
+						FROM ' . TABLE_PREFIX . 'Users
+						WHERE PortalUserId = ' . $user_id;
+				$cache_value = $this->Conn->GetOne($sql);
+				$this->Application->setCache($cache_key, $cache_value);
+			}
+
+			return $cache_value;
+		}
+
 	}