Your IP : 3.145.39.183


Current Path : /var/www/www-root/data/www/monolith-realty.ru/bitrix/modules/main/classes/general/
Upload File :
Current File : /var/www/www-root/data/www/monolith-realty.ru/bitrix/modules/main/classes/general/user.php

<?php

/**
 * Bitrix Framework
 * @package bitrix
 * @subpackage main
 * @copyright 2001-2023 Bitrix
 */

use Bitrix\Main;
use Bitrix\Main\Config\Option;
use Bitrix\Main\Authentication;
use Bitrix\Main\Authentication\ShortCode;
use Bitrix\Main\Authentication\Device;
use Bitrix\Main\Authentication\ApplicationPasswordTable;
use Bitrix\Main\Authentication\Internal\UserPasswordTable;
use Bitrix\Main\Authentication\Internal\UserDeviceTable;
use Bitrix\Main\Authentication\Internal\UserStoredAuthTable;
use Bitrix\Main\Authentication\Internal\UserHitAuthTable;
use Bitrix\Main\Authentication\Internal\UserDeviceLoginTable;
use Bitrix\Main\Authentication\Policy;
use Bitrix\Main\UserProfileHistoryTable;
use Bitrix\Main\Localization\Loc;
use Bitrix\Main\Security\Random;
use Bitrix\Main\Security\Password;
use Bitrix\Main\GroupTable;

IncludeModuleLangFile(__FILE__);

class CAllUser extends CDBResult
{
	const STATUS_ONLINE = 'online';
	const STATUS_OFFLINE = 'offline';
	//in seconds
	const PHONE_CODE_OTP_INTERVAL = 30;
	const PHONE_CODE_RESEND_INTERVAL = 60;
	public const PASSWORD_SPECIAL_CHARS = ',.<>/?;:\'"[]{}\|`~!@#$%^&*()_+=-';

	public $LAST_ERROR = '';
	protected $admin;
	/** @var Authentication\Context */
	protected $context;
	/** @var Main\Session\SessionInterface */
	protected static $kernelSession;
	protected static $CURRENT_USER = false;
	protected $justAuthorized = false;
	protected static $userGroupCache = [];

	/**
	 * CUser constructor.
	 */
	public function __construct()
	{
		static::$kernelSession = Main\Application::getInstance()->getKernelSession();
		parent::__construct();
	}

	public function GetParam($name)
	{
		if (isset(static::$kernelSession["SESS_AUTH"][$name]))
		{
			return static::$kernelSession["SESS_AUTH"][$name];
		}
		else
		{
			// compatibility
			switch ($name)
			{
				case 'USER_ID':
					return (string)$this->getContext()->getUserId();

				case 'APPLICATION_ID':
					return $this->getContext()->getApplicationId();
			}
		}
		return null;
	}

	public function SetParam($name, $value)
	{
		static::$kernelSession["SESS_AUTH"][$name] = $value;
	}

	public function GetSecurityPolicy()
	{
		if (!is_array($this->GetParam("POLICY")))
		{
			$this->SetParam("POLICY", static::getPolicy($this->GetID())->getValues());
		}
		return $this->GetParam("POLICY");
	}

	public function GetID()
	{
		if (!isset($this))
		{
			trigger_error("Static call CUser::GetID() is deprecated, will be removed soon. Use global \$USER.", E_USER_WARNING);

			global $USER;
			return $USER->GetID();
		}
		return $this->GetParam("USER_ID");
	}

	public function GetLogin()
	{
		if (!isset($this))
		{
			trigger_error("Static call CUser::GetLogin() is deprecated, will be removed soon. Use global \$USER.", E_USER_WARNING);

			global $USER;
			return $USER->GetLogin();
		}
		return $this->GetParam("LOGIN");
	}

	public function GetEmail()
	{
		if (!isset($this))
		{
			trigger_error("Static call CUser::GetEmail() is deprecated, will be removed soon. Use global \$USER.", E_USER_WARNING);

			global $USER;
			return $USER->GetEmail();
		}
		return $this->GetParam("EMAIL");
	}

	public function GetFullName()
	{
		if (!isset($this))
		{
			trigger_error("Static call CUser::GetFullName() is deprecated, will be removed soon. Use global \$USER.", E_USER_WARNING);

			global $USER;
			return $USER->GetFullName();
		}
		return $this->GetParam("NAME");
	}

	public function GetFirstName()
	{
		if (!isset($this))
		{
			trigger_error("Static call CUser::GetFirstName() is deprecated, will be removed soon. Use global \$USER.", E_USER_WARNING);

			global $USER;
			return $USER->GetFirstName();
		}
		return $this->GetParam("FIRST_NAME");
	}

	public function GetLastName()
	{
		if (!isset($this))
		{
			trigger_error("Static call CUser::GetLastName() is deprecated, will be removed soon. Use global \$USER.", E_USER_WARNING);

			global $USER;
			return $USER->GetLastName();
		}
		return $this->GetParam("LAST_NAME");
	}

	public function GetSecondName()
	{
		if (!isset($this))
		{
			trigger_error("Static call CUser::GetSecondName() is deprecated, will be removed soon. Use global \$USER.", E_USER_WARNING);

			global $USER;
			return $USER->GetSecondName();
		}
		return $this->GetParam("SECOND_NAME");
	}

	public function GetFormattedName($bUseBreaks = true, $bHTMLSpec = true)
	{
		return static::FormatName(CSite::GetNameFormat($bUseBreaks),
			[
				"TITLE" => $this->GetParam("TITLE"),
				"NAME" => $this->GetFirstName(),
				"SECOND_NAME" => $this->GetSecondName(),
				"LAST_NAME" => $this->GetLastName(),
				"LOGIN" => $this->GetLogin(),
			],
			true,
			$bHTMLSpec
		);
	}

	/**
	 * @deprecated Does nothing.
	 */
	public static function err_mess()
	{
	}

	public function Add($arFields)
	{
		/** @global CUserTypeManager $USER_FIELD_MANAGER */
		global $DB, $USER_FIELD_MANAGER, $CACHE_MANAGER;

		$ID = 0;
		if (!$this->CheckFields($arFields))
		{
			$Result = false;
			$arFields["RESULT_MESSAGE"] = &$this->LAST_ERROR;
		}
		else
		{
			unset($arFields["ID"]);
			unset($arFields["STORED_HASH"]);

			$arFields['ACTIVE'] = (is_set($arFields, 'ACTIVE') && $arFields['ACTIVE'] != 'Y' ? 'N' : 'Y');
			$arFields['BLOCKED'] = (is_set($arFields, 'BLOCKED') && $arFields['BLOCKED'] == 'Y' ? 'Y' : 'N');
			$arFields['PASSWORD_EXPIRED'] = (is_set($arFields, 'PASSWORD_EXPIRED') && $arFields['PASSWORD_EXPIRED'] == 'Y' ? 'Y' : 'N');

			if (
				!isset($arFields["PERSONAL_GENDER"])
				|| ($arFields["PERSONAL_GENDER"] != "M" && $arFields["PERSONAL_GENDER"] != "F")
			)
			{
				$arFields["PERSONAL_GENDER"] = '';
			}

			$originalPassword = $arFields["PASSWORD"] ?? '';
			$arFields["PASSWORD"] = Password::hash($arFields["PASSWORD"] ?? '');

			$checkword = empty($arFields["CHECKWORD"]) ? Random::getString(32) : $arFields["CHECKWORD"];
			$arFields["CHECKWORD"] = Password::hash($checkword);

			$arFields["~CHECKWORD_TIME"] = $DB->CurrentTimeFunction();

			if (is_set($arFields, "WORK_COUNTRY"))
			{
				$arFields["WORK_COUNTRY"] = intval($arFields["WORK_COUNTRY"]);
			}

			if (is_set($arFields, "PERSONAL_COUNTRY"))
			{
				$arFields["PERSONAL_COUNTRY"] = intval($arFields["PERSONAL_COUNTRY"]);
			}

			if (
				array_key_exists("PERSONAL_PHOTO", $arFields)
				&& is_array($arFields["PERSONAL_PHOTO"])
				&& (
					!array_key_exists("MODULE_ID", $arFields["PERSONAL_PHOTO"])
					|| $arFields["PERSONAL_PHOTO"]["MODULE_ID"] == ''
				)
			)
			{
				$arFields["PERSONAL_PHOTO"]["MODULE_ID"] = 'main';
			}

			CFile::SaveForDB($arFields, "PERSONAL_PHOTO", 'main');

			if (
				array_key_exists("WORK_LOGO", $arFields)
				&& is_array($arFields["WORK_LOGO"])
				&& (
					!array_key_exists("MODULE_ID", $arFields["WORK_LOGO"])
					|| $arFields["WORK_LOGO"]["MODULE_ID"] == ''
				)
			)
			{
				$arFields["WORK_LOGO"]["MODULE_ID"] = 'main';
			}

			CFile::SaveForDB($arFields, "WORK_LOGO", 'main');

			$arInsert = $DB->PrepareInsert("b_user", $arFields);

			if (!is_set($arFields, "DATE_REGISTER"))
			{
				$arInsert[0] .= ", DATE_REGISTER";
				$arInsert[1] .= ", " . $DB->GetNowFunction();
			}

			$strSql = "
				INSERT INTO b_user (
					" . $arInsert[0] . "
				) VALUES (
					" . $arInsert[1] . "
				)
			";
			$DB->Query($strSql);
			$ID = $DB->LastID();

			$USER_FIELD_MANAGER->Update("USER", $ID, $arFields);

			CAccess::RecalculateForUser($ID, CUserAuthProvider::ID);

			if (is_set($arFields, "GROUP_ID"))
			{
				static::SetUserGroup($ID, $arFields["GROUP_ID"], true);
			}

			if (isset($arFields["PHONE_NUMBER"]) && $arFields["PHONE_NUMBER"] != '')
			{
				Main\UserPhoneAuthTable::add([
					"USER_ID" => $ID,
					"PHONE_NUMBER" => $arFields["PHONE_NUMBER"],
				]);
			}

			//update digest hash for http digest authorization
			if (Option::get('main', 'use_digest_auth', 'N') == 'Y')
			{
				static::UpdateDigest($ID, $originalPassword);
			}

			//history of passwords
			UserPasswordTable::add([
				"USER_ID" => $ID,
				"PASSWORD" => $arFields["PASSWORD"],
				"DATE_CHANGE" => new Main\Type\DateTime(),
			]);

			if (Option::get('main', 'user_profile_history') === 'Y')
			{
				UserProfileHistoryTable::addHistory($ID, UserProfileHistoryTable::TYPE_ADD);
			}

			$Result = $ID;
			$arFields["ID"] = &$ID;
			$arFields["CHECKWORD"] = $checkword;
		}

		$arFields["RESULT"] = &$Result;

		foreach (GetModuleEvents('main', 'OnAfterUserAdd', true) as $arEvent)
		{
			ExecuteModuleEventEx($arEvent, [&$arFields]);
		}

		if ($ID > 0 && defined("BX_COMP_MANAGED_CACHE"))
		{
			$isRealUser = empty($arFields['EXTERNAL_AUTH_ID']) || !in_array($arFields['EXTERNAL_AUTH_ID'], Main\UserTable::getExternalUserTypes());

			$CACHE_MANAGER->ClearByTag("USER_CARD_" . intval($ID / TAGGED_user_card_size));
			$CACHE_MANAGER->ClearByTag($isRealUser ? "USER_CARD" : "EXTERNAL_USER_CARD");

			$CACHE_MANAGER->ClearByTag("USER_NAME_" . $ID);
			$CACHE_MANAGER->ClearByTag($isRealUser ? "USER_NAME" : "EXTERNAL_USER_NAME");
		}

		Main\UserTable::indexRecord($ID);

		return $Result;
	}

	public static function GetDropDownList($strSqlSearch = "and ACTIVE='Y'", $strSqlOrder = "ORDER BY ID, NAME, LAST_NAME")
	{
		global $DB;
		$connection = \Bitrix\Main\Application::getConnection();
		$helper = $connection->getSqlHelper();

		$strSql = "
			SELECT
				ID as REFERENCE_ID,
				" . $helper->getConcatFunction("'['", "ID", "'] ('", "LOGIN", "') '", "coalesce(NAME,'')", "' '", "coalesce(LAST_NAME,'')") . " as REFERENCE
			FROM
				b_user
			WHERE
				1=1
			$strSqlSearch
			$strSqlOrder
			";
		$res = $DB->Query($strSql);

		return $res;
	}

	public static function GetList($by = '', $order = '', $arFilter = [], $arParams = [])
	{
		/** @global CUserTypeManager $USER_FIELD_MANAGER */
		global $DB, $USER_FIELD_MANAGER;

		$connection = Main\Application::getConnection();
		$helper = $connection->getSqlHelper();

		$arOrder = is_array($by) ? $by : [$by => $order];

		static $arFields_m = ["ID", "ACTIVE", "LAST_LOGIN", "LOGIN", "EMAIL", "NAME", "LAST_NAME", "SECOND_NAME", "TIMESTAMP_X", "PERSONAL_BIRTHDAY", "IS_ONLINE", "IS_REAL_USER"];
		static $arFields = [
			"DATE_REGISTER", "PERSONAL_PROFESSION", "PERSONAL_WWW", "PERSONAL_ICQ", "PERSONAL_GENDER", "PERSONAL_PHOTO", "PERSONAL_PHONE", "PERSONAL_FAX",
			"PERSONAL_MOBILE", "PERSONAL_PAGER", "PERSONAL_STREET", "PERSONAL_MAILBOX", "PERSONAL_CITY", "PERSONAL_STATE", "PERSONAL_ZIP", "PERSONAL_COUNTRY", "PERSONAL_NOTES",
			"WORK_COMPANY", "WORK_DEPARTMENT", "WORK_POSITION", "WORK_WWW", "WORK_PHONE", "WORK_FAX", "WORK_PAGER", "WORK_STREET", "WORK_MAILBOX", "WORK_CITY", "WORK_STATE",
			"WORK_ZIP", "WORK_COUNTRY", "WORK_PROFILE", "WORK_NOTES", "ADMIN_NOTES", "XML_ID", "LAST_NAME", "SECOND_NAME", "STORED_HASH", "CHECKWORD_TIME", "EXTERNAL_AUTH_ID",
			"CONFIRM_CODE", "LOGIN_ATTEMPTS", "LAST_ACTIVITY_DATE", "AUTO_TIME_ZONE", "TIME_ZONE", "TIME_ZONE_OFFSET", "PASSWORD", "CHECKWORD", "LID", "LANGUAGE_ID", "TITLE",
		];
		$arFields_all = array_merge($arFields_m, $arFields);

		$arSelectFields = [];
		$online_interval = (array_key_exists("ONLINE_INTERVAL", $arParams) && intval($arParams["ONLINE_INTERVAL"]) > 0 ? $arParams["ONLINE_INTERVAL"] : static::GetSecondsForLimitOnline());
		if (!empty($arParams['FIELDS']) && is_array($arParams['FIELDS']) && !in_array("*", $arParams['FIELDS']))
		{
			foreach ($arParams['FIELDS'] as $field)
			{
				$field = strtoupper($field);
				if ($field == 'TIMESTAMP_X' || $field == 'DATE_REGISTER' || $field == 'LAST_LOGIN')
				{
					$arSelectFields[$field] = $DB->DateToCharFunction("U." . $field) . ' ' . $field . ", U." . $field . ' ' . $field . "_DATE";
				}
				elseif ($field == 'PERSONAL_BIRTHDAY')
				{
					$arSelectFields[$field] = $DB->DateToCharFunction("U.PERSONAL_BIRTHDAY", "SHORT") . " PERSONAL_BIRTHDAY, U.PERSONAL_BIRTHDAY PERSONAL_BIRTHDAY_DATE";
				}
				elseif ($field == 'IS_ONLINE')
				{
					$arSelectFields[$field] = 'CASE WHEN U.LAST_ACTIVITY_DATE > ' . $helper->addSecondsToDateTime('(-' . $online_interval . ')') . ' THEN \'Y\' ELSE \'N\' END IS_ONLINE';
				}
				elseif ($field == 'IS_REAL_USER')
				{
					$arSelectFields[$field] = "CASE WHEN U.EXTERNAL_AUTH_ID IN ('" . join("', '", static::GetExternalUserTypes()) . "') THEN 'N' ELSE 'Y' END IS_REAL_USER";
				}
				elseif (in_array($field, $arFields_all))
				{
					$arSelectFields[$field] = 'U.' . $field;
				}
			}
		}
		if (empty($arSelectFields))
		{
			$arSelectFields['*'] = 'U.*';
			$arSelectFields['TIMESTAMP_X'] = $DB->DateToCharFunction("U.TIMESTAMP_X") . " TIMESTAMP_X";
			$arSelectFields['IS_ONLINE'] = 'CASE WHEN U.LAST_ACTIVITY_DATE > ' . $helper->addSecondsToDateTime('(-' . $online_interval . ')') . ' THEN \'Y\' ELSE \'N\' END IS_ONLINE';
			$arSelectFields['DATE_REGISTER'] = $DB->DateToCharFunction("U.DATE_REGISTER") . " DATE_REGISTER";
			$arSelectFields['LAST_LOGIN'] = $DB->DateToCharFunction("U.LAST_LOGIN") . " LAST_LOGIN";
			$arSelectFields['PERSONAL_BIRTHDAY'] = $DB->DateToCharFunction("U.PERSONAL_BIRTHDAY", "SHORT") . " PERSONAL_BIRTHDAY";
		}

		static $obUserFieldsSql;
		if (!isset($obUserFieldsSql))
		{
			$obUserFieldsSql = new CUserTypeSQL;
			$obUserFieldsSql->SetEntity("USER", "U.ID");
			$obUserFieldsSql->obWhere->AddFields([
				"F_LAST_NAME" => [
					"TABLE_ALIAS" => "U",
					"FIELD_NAME" => "U.LAST_NAME",
					"MULTIPLE" => 'N',
					"FIELD_TYPE" => "string",
					"JOIN" => false,
				],
			]);
		}

		$ufSelectFields = $arParams["SELECT"] ?? [];
		$arSqlSearch = [];

		$obUserFieldsSql->SetFilter($arFilter);
		$obUserFieldsSql->SetOrder($arOrder);
		$arSqlSearch[] = $obUserFieldsSql->GetFilter();
		$distinct = $obUserFieldsSql->GetDistinct();

		$strJoin = '';

		if (is_array($arFilter))
		{
			foreach ($arFilter as $key => $val)
			{
				$key = strtoupper($key);
				if (is_array($val))
				{
					if (empty($val))
					{
						continue;
					}
				}
				elseif
				(
					$key != "LOGIN_EQUAL_EXACT"
					&& $key != "CONFIRM_CODE"
					&& $key != "!CONFIRM_CODE"
					&& $key != "LAST_ACTIVITY"
					&& $key != "!LAST_ACTIVITY"
					&& $key != "LAST_LOGIN"
					&& $key != "!LAST_LOGIN"
					&& $key != "EXTERNAL_AUTH_ID"
					&& $key != "!EXTERNAL_AUTH_ID"
					&& $key != "IS_REAL_USER"
				)
				{
					if ((string)$val == '' || $val === "NOT_REF")
					{
						continue;
					}
				}
				$match_value_set = array_key_exists($key . "_EXACT_MATCH", $arFilter);
				switch ($key)
				{
					case "ID":
						$arSqlSearch[] = GetFilterQuery("U.ID", $val, 'N');
						break;
					case ">ID":
						$arSqlSearch[] = "U.ID > " . intval($val);
						break;
					case "!ID":
						$arSqlSearch[] = "U.ID <> " . intval($val);
						break;
					case "ID_EQUAL_EXACT":
						$arSqlSearch[] = "U.ID='" . intval($val) . "'";
						break;
					case "TIMESTAMP_1":
						$arSqlSearch[] = "U.TIMESTAMP_X >= FROM_UNIXTIME('" . MkDateTime(FmtDate($val, "D.M.Y"), "d.m.Y") . "')";
						break;
					case "TIMESTAMP_2":
						$arSqlSearch[] = "U.TIMESTAMP_X <= FROM_UNIXTIME('" . MkDateTime(FmtDate($val, "D.M.Y") . " 23:59:59", "d.m.Y") . "')";
						break;
					case "TIMESTAMP_X_1":
						$arSqlSearch[] = "U.TIMESTAMP_X >= FROM_UNIXTIME('" . MkDateTime(FmtDate($val, "DD.MM.YYYY HH:MI:SS")) . "')";
						break;
					case "TIMESTAMP_X_2":
						$arSqlSearch[] = "U.TIMESTAMP_X <= FROM_UNIXTIME('" . MkDateTime(FmtDate($val, "DD.MM.YYYY HH:MI:SS")) . "')";
						break;
					case "LAST_LOGIN_1":
						$arSqlSearch[] = "U.LAST_LOGIN >= FROM_UNIXTIME('" . MkDateTime(FmtDate($val, "D.M.Y"), "d.m.Y") . "')";
						break;
					case "LAST_LOGIN_2":
						$arSqlSearch[] = "U.LAST_LOGIN <= FROM_UNIXTIME('" . MkDateTime(FmtDate($val, "D.M.Y") . " 23:59:59", "d.m.Y") . "')";
						break;
					case "LAST_LOGIN":
						if ($val === false)
						{
							$arSqlSearch[] = "U.LAST_LOGIN IS NULL";
						}
						break;
					case "!LAST_LOGIN":
						if ($val === false)
						{
							$arSqlSearch[] = "U.LAST_LOGIN IS NOT NULL";
						}
						break;
					case "DATE_REGISTER_1":
						$arSqlSearch[] = "U.DATE_REGISTER >= FROM_UNIXTIME('" . MkDateTime(FmtDate($val, "D.M.Y"), "d.m.Y") . "')";
						break;
					case "DATE_REGISTER_2":
						$arSqlSearch[] = "U.DATE_REGISTER <= FROM_UNIXTIME('" . MkDateTime(FmtDate($val, "D.M.Y") . " 23:59:59", "d.m.Y") . "')";
						break;
					case "ACTIVE":
						$arSqlSearch[] = ($val == 'Y') ? "U.ACTIVE='Y'" : "U.ACTIVE='N'";
						break;
					case "LOGIN_EQUAL":
						$arSqlSearch[] = GetFilterQuery("U.LOGIN", $val, 'N');
						break;
					case "LOGIN":
						$arSqlSearch[] = GetFilterQuery("U.LOGIN", $val);
						break;
					case "EXTERNAL_AUTH_ID":
						if ($val != '')
						{
							$arSqlSearch[] = "U.EXTERNAL_AUTH_ID='" . $DB->ForSQL($val, 255) . "'";
						}
						else
						{
							$arSqlSearch[] = "(U.EXTERNAL_AUTH_ID IS NULL OR U.EXTERNAL_AUTH_ID='')";
						}
						break;
					case "!EXTERNAL_AUTH_ID":
						if (
							is_array($val)
							&& !empty($val)
						)
						{
							$strTmp = '';
							foreach ($val as $authId)
							{
								if ($authId != '')
								{
									$strTmp .= ($strTmp != '' ? ',' : '') . "'" . $DB->ForSQL($authId, 255) . "'";
								}
							}
							if ($strTmp != '')
							{
								$arSqlSearch[] = "U.EXTERNAL_AUTH_ID NOT IN (" . $strTmp . ") OR U.EXTERNAL_AUTH_ID IS NULL";
							}
						}
						elseif (!is_array($val))
						{
							if ($val != '')
							{
								$arSqlSearch[] = "U.EXTERNAL_AUTH_ID <> '" . $DB->ForSql($val, 255) . "' OR U.EXTERNAL_AUTH_ID IS NULL";
							}
							else
							{
								$arSqlSearch[] = "(U.EXTERNAL_AUTH_ID IS NOT NULL AND LENGTH(U.EXTERNAL_AUTH_ID) > 0)";
							}
						}
						break;
					case "LOGIN_EQUAL_EXACT":
						$arSqlSearch[] = "U.LOGIN='" . $DB->ForSql($val) . "'";
						break;
					case "XML_ID":
						$arSqlSearch[] = "U.XML_ID='" . $DB->ForSql($val) . "'";
						break;
					case "CONFIRM_CODE":
						if ($val != '')
						{
							$arSqlSearch[] = "U.CONFIRM_CODE='" . $DB->ForSql($val) . "'";
						}
						else
						{
							$arSqlSearch[] = "(U.CONFIRM_CODE IS NULL OR LENGTH(U.CONFIRM_CODE) <= 0)";
						}
						break;
					case "!CONFIRM_CODE":
						if ($val != '')
						{
							$arSqlSearch[] = "U.CONFIRM_CODE <> '" . $DB->ForSql($val) . "'";
						}
						else
						{
							$arSqlSearch[] = "(U.CONFIRM_CODE IS NOT NULL AND LENGTH(U.CONFIRM_CODE) > 0)";
						}
						break;
					case "COUNTRY_ID":
					case "WORK_COUNTRY":
						$arSqlSearch[] = "U.WORK_COUNTRY=" . intval($val);
						break;
					case "PERSONAL_COUNTRY":
						$arSqlSearch[] = "U.PERSONAL_COUNTRY=" . intval($val);
						break;
					case "NAME":
						$arSqlSearch[] = GetFilterQuery("U.NAME, U.LAST_NAME, U.SECOND_NAME", $val);
						break;
					case "NAME_SEARCH":
						$arSqlSearch[] = GetFilterQuery("U.NAME, U.LAST_NAME, U.SECOND_NAME, U.EMAIL, U.LOGIN", $val);
						break;
					case "EMAIL":
						$arSqlSearch[] = GetFilterQuery("U.EMAIL", $val, 'Y', ["@", "_", ".", "-"]);
						break;
					case "=EMAIL":
						$arSqlSearch[] = "U.EMAIL = '" . $DB->ForSQL(trim($val)) . "'";
						break;
					case "GROUP_MULTI":
					case "GROUPS_ID":
						if (is_numeric($val) && intval($val) > 0)
						{
							$val = [$val];
						}
						if (is_array($val) && !empty($val))
						{
							$ar = [];
							foreach ($val as $id)
							{
								$ar[intval($id)] = intval($id);
							}
							$strJoin .=
								" INNER JOIN (SELECT DISTINCT UG.USER_ID FROM b_user_group UG
							WHERE UG.GROUP_ID in (" . implode(",", $ar) . ")
								and (UG.DATE_ACTIVE_FROM is null or	UG.DATE_ACTIVE_FROM <= " . $DB->CurrentTimeFunction() . ")
								and (UG.DATE_ACTIVE_TO is null or UG.DATE_ACTIVE_TO >= " . $DB->CurrentTimeFunction() . ")
							) UG ON UG.USER_ID=U.ID ";
						}
						break;
					case "PERSONAL_BIRTHDATE_1":
						$arSqlSearch[] = "U.PERSONAL_BIRTHDATE>=" . $DB->CharToDateFunction($val);
						break;
					case "PERSONAL_BIRTHDATE_2":
						$arSqlSearch[] = "U.PERSONAL_BIRTHDATE<=" . $DB->CharToDateFunction($val . " 23:59:59");
						break;
					case "PERSONAL_BIRTHDAY_1":
						$arSqlSearch[] = "U.PERSONAL_BIRTHDAY>=" . $DB->CharToDateFunction($DB->ForSql($val), "SHORT");
						break;
					case "PERSONAL_BIRTHDAY_2":
						$arSqlSearch[] = "U.PERSONAL_BIRTHDAY<=" . $DB->CharToDateFunction($DB->ForSql($val), "SHORT");
						break;
					case "PERSONAL_BIRTHDAY_DATE":
						$arSqlSearch[] = $helper->formatDate('MM-DD', 'U.PERSONAL_BIRTHDAY') . " = '" . $DB->ForSql($val) . "'";
						break;
					case "KEYWORDS":
						$arSqlSearch[] = GetFilterQuery(implode(",", $arFields), $val);
						break;
					case "CHECK_SUBORDINATE":
						if (is_array($val))
						{
							$strSubord = "0";
							foreach ($val as $grp)
							{
								$strSubord .= "," . intval($grp);
							}
							if (intval($arFilter["CHECK_SUBORDINATE_AND_OWN"]) > 0)
							{
								$arSqlSearch[] = "(U.ID=" . intval($arFilter["CHECK_SUBORDINATE_AND_OWN"]) . " OR NOT EXISTS(SELECT 'x' FROM b_user_group UGS WHERE UGS.USER_ID=U.ID AND UGS.GROUP_ID NOT IN (" . $strSubord . ")))";
							}
							else
							{
								$arSqlSearch[] = "NOT EXISTS(SELECT 'x' FROM b_user_group UGS WHERE UGS.USER_ID=U.ID AND UGS.GROUP_ID NOT IN (" . $strSubord . "))";
							}
						}
						break;
					case "NOT_ADMIN":
						if ($val !== true)
						{
							break;
						}
						$arSqlSearch[] = "not exists (SELECT * FROM b_user_group UGNA WHERE UGNA.USER_ID=U.ID AND UGNA.GROUP_ID = 1)";
						break;
					case "LAST_ACTIVITY":
						if ($val === false)
						{
							$arSqlSearch[] = "U.LAST_ACTIVITY_DATE IS NULL";
						}
						elseif (intval($val) > 0)
						{
							$arSqlSearch[] = "U.LAST_ACTIVITY_DATE > " . $helper->addSecondsToDateTime(-intval($val));
						}
						break;
					case "!LAST_ACTIVITY":
						if ($val === false)
						{
							$arSqlSearch[] = "U.LAST_ACTIVITY_DATE IS NOT NULL";
						}
						break;
					case "INTRANET_USERS":
						$arSqlSearch[] = "U.ACTIVE = 'Y' AND U.LAST_LOGIN IS NOT NULL AND EXISTS(SELECT 'x' FROM b_utm_user UF1, b_user_field F1 WHERE F1.ENTITY_ID = 'USER' AND F1.FIELD_NAME = 'UF_DEPARTMENT' AND UF1.FIELD_ID = F1.ID AND UF1.VALUE_ID = U.ID AND UF1.VALUE_INT IS NOT NULL AND UF1.VALUE_INT <> 0)";
						break;
					case "IS_REAL_USER":
						if ($val === true || $val === 'Y')
						{
							$arSqlSearch[] = "U.EXTERNAL_AUTH_ID NOT IN ('" . join("', '", static::GetExternalUserTypes()) . "') OR U.EXTERNAL_AUTH_ID IS NULL";
						}
						else
						{
							$arSqlSearch[] = "U.EXTERNAL_AUTH_ID IN ('" . join("', '", static::GetExternalUserTypes()) . "')";
						}
						break;
					default:
						if (in_array($key, $arFields))
						{
							$arSqlSearch[] = GetFilterQuery('U.' . $key, $val, ($arFilter[$key . "_EXACT_MATCH"] == 'Y' && $match_value_set ? 'N' : 'Y'));
						}
				}
			}
		}

		$arSqlOrder = [];
		foreach ($arOrder as $field => $dir)
		{
			$field = strtoupper($field);
			if (strtolower($dir) != "asc")
			{
				$dir = "desc";
			}

			if ($field == "CURRENT_BIRTHDAY")
			{
				$cur_year = intval(date('Y'));
				$arSelectFields[$field] = "case
					when U.PERSONAL_BIRTHDAY is null then '9999-99-99'
					when " . $helper->formatDate($cur_year . '-MM-DD', 'U.PERSONAL_BIRTHDAY') . ' < ' . $helper->formatDate('YYYY-MM-DD', $helper->addSecondsToDateTime(CTimeZone::GetOffset())) . " then " . $helper->formatDate(($cur_year + 1) . '-MM-DD', 'U.PERSONAL_BIRTHDAY') . "
					else " . $helper->formatDate($cur_year . '-MM-DD', 'U.PERSONAL_BIRTHDAY') . "
				end CURRENT_BIRTHDAY";
				$arSqlOrder[$field] = "CURRENT_BIRTHDAY " . $dir;
			}
			elseif ($field == "IS_ONLINE")
			{
				$arSelectFields[$field] = "case when U.LAST_ACTIVITY_DATE > " . $helper->addSecondsToDateTime(-$online_interval) . " then 'Y' else 'N' end IS_ONLINE";
				$arSqlOrder[$field] = "IS_ONLINE " . $dir;
			}
			elseif (in_array($field, $arFields_all))
			{
				$arSqlOrder[$field] = "U." . $field . ' ' . $dir;

				if ($distinct && !isset($arSelectFields['*']) && !isset($arSelectFields[$field]))
				{
					$arSelectFields[$field] = 'U.' . $field;
				}
			}
			elseif ($s = $obUserFieldsSql->GetOrder($field))
			{
				$arSqlOrder[$field] = strtoupper($s) . ' ' . $dir;

				if ($distinct && !in_array('UF_*', $ufSelectFields) && !in_array($field, $ufSelectFields))
				{
					$ufSelectFields[] = $field;
				}
			}
			elseif (preg_match('/^RATING_(\d+)$/i', $field, $matches))
			{
				$ratingId = intval($matches[1]);
				if ($ratingId > 0)
				{
					$arSqlOrder[$field] = $field . "_ISNULL ASC, " . $field . ' ' . $dir;
					$arParams['SELECT'][] = $field;
				}
				else
				{
					$field = "TIMESTAMP_X";
					$arSqlOrder[$field] = "U." . $field . ' ' . $dir;
					$arSelectFields[$field] = 'U.TIMESTAMP_X';
				}
			}
			elseif ($field == 'FULL_NAME')
			{
				$arSelectFields['LAST_NAME_SRT1'] = "CASE WHEN U.LAST_NAME IS NULL OR U.LAST_NAME = '' THEN 1 ELSE 0 END LAST_NAME_SRT1";
				$arSelectFields['LAST_NAME_SRT2'] = "CASE WHEN U.LAST_NAME IS NULL OR U.LAST_NAME = '' THEN '1' ELSE U.LAST_NAME END LAST_NAME_SRT2";
				$arSelectFields['NAME_SRT1'] = "CASE WHEN U.NAME IS NULL OR U.NAME = '' THEN 1 ELSE 0 END NAME_SRT1";
				$arSelectFields['NAME_SRT2'] = "CASE WHEN U.NAME IS NULL OR U.NAME = '' THEN '1' ELSE U.NAME END NAME_SRT2";
				$arSelectFields['SECOND_NAME_SRT1'] = "CASE WHEN U.SECOND_NAME IS NULL OR U.SECOND_NAME = '' THEN 1 ELSE 0 END SECOND_NAME_SRT1";
				$arSelectFields['SECOND_NAME_SRT2'] = "CASE WHEN U.SECOND_NAME IS NULL OR U.SECOND_NAME = '' THEN '1' ELSE U.SECOND_NAME END SECOND_NAME_SRT2";
				$arSelectFields['LOGIN'] = "U.LOGIN";

				$arSqlOrder[$field] = "LAST_NAME_SRT1 {$dir}, LAST_NAME_SRT2 {$dir}, NAME_SRT1 {$dir}, NAME_SRT2 {$dir}, SECOND_NAME_SRT1 {$dir}, SECOND_NAME_SRT2 {$dir}, U.LOGIN {$dir}";
			}
		}

		$obUserFieldsSql->SetSelect($ufSelectFields);
		$userFieldsSelect = $obUserFieldsSql->GetSelect();
		$strSqlSearch = GetFilterSqlSearch($arSqlSearch);

		$sSelect = ($distinct ? "DISTINCT " : '')
			. implode(', ', $arSelectFields) . "
			" . $userFieldsSelect . "
		";

		if (isset($arParams['SELECT']) && is_array($arParams['SELECT']))
		{
			$arRatingInSelect = [];
			foreach ($arParams['SELECT'] as $column)
			{
				if (preg_match('/^RATING_(\d+)$/i', $column, $matches))
				{
					$ratingId = intval($matches[1]);
					if ($ratingId > 0 && !isset($arRatingInSelect[$ratingId]))
					{
						$sSelect .= ", RR" . $ratingId . ".CURRENT_POSITION IS NULL as RATING_" . $ratingId . "_ISNULL";
						$sSelect .= ", RR" . $ratingId . ".CURRENT_VALUE as RATING_" . $ratingId;
						$sSelect .= ", RR" . $ratingId . ".CURRENT_VALUE as RATING_" . $ratingId . "_CURRENT_VALUE";
						$sSelect .= ", RR" . $ratingId . ".PREVIOUS_VALUE as RATING_" . $ratingId . "_PREVIOUS_VALUE";
						$sSelect .= ", RR" . $ratingId . ".CURRENT_POSITION as RATING_" . $ratingId . "_CURRENT_POSITION";
						$sSelect .= ", RR" . $ratingId . ".PREVIOUS_POSITION as RATING_" . $ratingId . "_PREVIOUS_POSITION";

						$strJoin .= " LEFT JOIN  b_rating_results RR" . $ratingId . "
							ON RR" . $ratingId . ".RATING_ID=" . $ratingId . "
							and RR" . $ratingId . ".ENTITY_TYPE_ID = 'USER'
							and RR" . $ratingId . ".ENTITY_ID = U.ID ";

						$arRatingInSelect[$ratingId] = $ratingId;
					}
				}
			}
		}

		$strFrom = "
			FROM
				b_user U
				" . $obUserFieldsSql->GetJoin("U.ID") . "
				" . $strJoin . "
			WHERE
				" . $strSqlSearch . "
			";

		$strSqlOrder = '';
		if (!empty($arSqlOrder))
		{
			$strSqlOrder = 'ORDER BY ' . implode(', ', $arSqlOrder);
		}

		$strSql = "SELECT " . $sSelect . $strFrom . $strSqlOrder;

		if (isset($arParams["NAV_PARAMS"]) && is_array($arParams["NAV_PARAMS"]))
		{
			$nTopCount = (int)($arParams['NAV_PARAMS']['nTopCount'] ?? 0);
			if ($nTopCount > 0)
			{
				$strSql = $DB->TopSql($strSql, $nTopCount);
				$res = $DB->Query($strSql);
				if ($userFieldsSelect != '')
				{
					$res->SetUserFields($USER_FIELD_MANAGER->GetUserFields("USER"));
				}
			}
			else
			{
				$res_cnt = $DB->Query("SELECT COUNT(" . ($obUserFieldsSql->GetDistinct() ? 'DISTINCT ' : '') . "U.ID) as C " . $strFrom);
				$res_cnt = $res_cnt->Fetch();
				$res = new CDBResult();
				if ($userFieldsSelect != '')
				{
					$res->SetUserFields($USER_FIELD_MANAGER->GetUserFields("USER"));
				}
				$res->NavQuery($strSql, $res_cnt["C"], $arParams["NAV_PARAMS"]);
			}
		}
		else
		{
			$res = $DB->Query($strSql);
			if ($userFieldsSelect != '')
			{
				$res->SetUserFields($USER_FIELD_MANAGER->GetUserFields("USER"));
			}
		}

		$res->is_filtered = IsFiltered($strSqlSearch);

		return $res;
	}

	public static function IsOnLine($id, $interval = null)
	{
		global $DB;
		$connection = \Bitrix\Main\Application::getConnection();
		$helper = $connection->getSqlHelper();

		$id = intval($id);
		if ($id <= 0)
		{
			return false;
		}

		if (is_null($interval))
		{
			$interval = static::GetSecondsForLimitOnline();
		}
		else
		{
			$interval = intval($interval);
			if ($interval <= 0)
			{
				$interval = static::GetSecondsForLimitOnline();
			}
		}

		$dbRes = $DB->Query("SELECT 'x' FROM b_user WHERE ID = " . $id . " AND LAST_ACTIVITY_DATE > " . $helper->addSecondsToDateTime(-$interval));
		return (bool)$dbRes->Fetch();
	}

	public function GetUserGroupArray()
	{
		$groups = $this->GetParam("GROUPS");

		if (!is_array($groups) || empty($groups))
		{
			return [2];
		}

		//always unique and sorted, containing group ID=2
		return $groups;
	}

	public function SetUserGroupArray($arr)
	{
		$arr = array_map("intval", $arr);
		$arr = array_filter($arr);
		$arr[] = 2;
		$arr = array_values(array_unique($arr));
		sort($arr);
		$this->SetParam("GROUPS", $arr);
	}

	public function GetUserGroupString()
	{
		return $this->GetGroups();
	}

	public function GetGroups()
	{
		return implode(",", $this->GetUserGroupArray());
	}

	public static function GetSubordinateGroups(int $userID = null): array
	{
		global $USER;

		static $groupsCache = [];

		if ($userID === null && $USER instanceof self)
		{
			$userID = (int)$USER->getId();

			// groups from the session
			$userGroups = $USER->GetUserGroupArray();
		}
		elseif ($userID > 0)
		{
			// groups from the DB
			$userGroups = static::GetUserGroup($userID);
		}
		else
		{
			return [];
		}

		if (isset($groupsCache[$userID]))
		{
			$result = $groupsCache[$userID];
		}
		else
		{
			$result = CGroup::GetSubordinateGroups($userGroups);

			$groupsCache[$userID] = $result;
		}

		return $result;
	}

	public function RequiredHTTPAuthBasic($Realm = "Bitrix")
	{
		header("WWW-Authenticate: Basic realm=\"{$Realm}\"");
		if (stristr(php_sapi_name(), "cgi") !== false)
		{
			header("Status: 401 Unauthorized");
		}
		else
		{
			header($_SERVER["SERVER_PROTOCOL"] . " 401 Unauthorized");
		}

		return false;
	}

	public function LoginByCookies()
	{
		if (Option::get('main', 'store_password', 'Y') == 'Y')
		{
			if (!isset($_REQUEST["logout"]) || strtolower($_REQUEST["logout"]) != "yes")
			{
				$prefix = Option::get('main', 'cookie_name', 'BITRIX_SM');
				$login = (string)($_COOKIE[$prefix . '_UIDL'] ?? '');
				$password = (string)($_COOKIE[$prefix . '_UIDH'] ?? '');

				if ($login != '' && $password != '')
				{
					if ($login != $this->GetLogin() || $password !== $this->getContext()->getStoredAuthHash())
					{
						$this->LoginByHash($login, $password);
					}
				}
			}
		}
	}

	public function LoginByHash($login, $hash)
	{
		/** @global CMain $APPLICATION */
		global $DB, $APPLICATION;

		$result_message = true;
		$userId = 0;
		$arParams = [
			"LOGIN" => $login,
			"HASH" => $hash,
		];

		$APPLICATION->ResetException();
		$bOk = true;
		foreach (GetModuleEvents('main', 'OnBeforeUserLoginByHash', true) as $arEvent)
		{
			if (ExecuteModuleEventEx($arEvent, [&$arParams]) === false)
			{
				if ($err = $APPLICATION->GetException())
				{
					$result_message = ["MESSAGE" => $err->GetString() . "<br>", "TYPE" => "ERROR"];
				}
				else
				{
					$APPLICATION->ThrowException("Unknown error");
					$result_message = ["MESSAGE" => "Unknown error" . "<br>", "TYPE" => "ERROR"];
				}

				$bOk = false;
				break;
			}
		}

		if ($bOk && $arParams['HASH'] != '')
		{
			$strSql =
				"SELECT U.ID, U.ACTIVE, U.EXTERNAL_AUTH_ID, U.BLOCKED " .
				"FROM b_user U " .
				"WHERE U.LOGIN = '" . $DB->ForSQL($arParams['LOGIN'], 50) . "' ";
			$result = $DB->Query($strSql);

			$found = false;
			while (($arUser = $result->Fetch()))
			{
				$userId = $arUser['ID'];

				//there is no stored auth for external authorization, but domain spread auth should work
				$bExternal = ($arUser["EXTERNAL_AUTH_ID"] != '');
				$bAllowExternalSave = Option::get('main', 'allow_external_auth_stored_hash', 'N') == 'Y';
				$tempHash = $bExternal && !$bAllowExternalSave;

				$context = (new Authentication\Context())
					->setUserId($userId)
				;

				if (static::CheckStoredHash($context, $arParams['HASH'], $tempHash))
				{
					$found = true;
					if ($arUser["ACTIVE"] == 'Y' && $arUser["BLOCKED"] != 'Y')
					{
						$this->Authorize($context, !$tempHash);
					}
					else
					{
						$APPLICATION->ThrowException(GetMessage("LOGIN_BLOCK"));
						$result_message = ["MESSAGE" => GetMessage("LOGIN_BLOCK") . "<br>", "TYPE" => "ERROR"];
					}
					break;
				}
			}

			if (!$found)
			{
				//Delete invalid stored auth cookie
				$spread = (Option::get('main', 'auth_multisite', 'N') === 'Y' ? (Main\Web\Cookie::SPREAD_SITES | Main\Web\Cookie::SPREAD_DOMAIN) : Main\Web\Cookie::SPREAD_DOMAIN);

				$cookie = (new Main\Web\Cookie('UIDH', '', 0))
					->setSpread($spread)
					->setHttpOnly(true)
				;
				Main\Context::getCurrent()->getResponse()->addCookie($cookie);

				$APPLICATION->ThrowException(GetMessage("WRONG_LOGIN"));
				$result_message = ["MESSAGE" => GetMessage("WRONG_LOGIN") . "<br>", "TYPE" => "ERROR"];
			}
		}

		$arParams["USER_ID"] = $userId;
		$arParams["RESULT_MESSAGE"] = $result_message;

		foreach (GetModuleEvents('main', 'OnAfterUserLoginByHash', true) as $arEvent)
		{
			ExecuteModuleEventEx($arEvent, [&$arParams]);
		}

		if ($result_message !== true && Option::get('main', 'event_log_login_fail', 'N') === 'Y')
		{
			CEventLog::Log('SECURITY', 'USER_LOGINBYHASH', 'main', $login, $result_message['MESSAGE']);
		}

		return $arParams["RESULT_MESSAGE"];
	}

	public function LoginByHttpAuth()
	{
		$arAuth = Main\Context::getCurrent()->getServer()->parseAuthRequest();

		foreach (GetModuleEvents('main', 'onBeforeUserLoginByHttpAuth', true) as $arEvent)
		{
			$res = ExecuteModuleEventEx($arEvent, [&$arAuth]);
			if ($res !== null)
			{
				return $res;
			}
		}

		if (isset($arAuth["basic"]) && $arAuth["basic"]["username"] != '' && $arAuth["basic"]["password"] != '')
		{
			// Authorize user, if it is http basic authorization, with no remembering
			if (!$this->IsAuthorized() || $this->GetLogin() != $arAuth["basic"]["username"])
			{
				return $this->Login($arAuth["basic"]["username"], $arAuth["basic"]["password"]);
			}
		}
		elseif (isset($arAuth["digest"]) && $arAuth["digest"]["username"] != '' && Option::get('main', 'use_digest_auth', 'N') == 'Y')
		{
			// Authorize user by http digest authorization
			if (!$this->IsAuthorized() || $this->GetLogin() != $arAuth["digest"]["username"])
			{
				return $this->LoginByDigest($arAuth["digest"]);
			}
		}

		return null;
	}

	public function LoginByDigest($arDigest)
	{
		//array("username"=>"", "nonce"=>"", "uri"=>"", "response"=>"")
		/** @global CMain $APPLICATION */
		global $DB, $APPLICATION;

		$APPLICATION->ResetException();

		$strSql =
			"SELECT U.ID, U.PASSWORD, UD.DIGEST_HA1, U.EXTERNAL_AUTH_ID " .
			"FROM b_user U LEFT JOIN b_user_digest UD ON UD.USER_ID=U.ID " .
			"WHERE U.LOGIN='" . $DB->ForSQL($arDigest["username"]) . "' ";
		$res = $DB->Query($strSql);

		if ($arUser = $res->Fetch())
		{
			$method = ($_SERVER['REDIRECT_REQUEST_METHOD'] ?? $_SERVER['REQUEST_METHOD']);
			$HA2 = md5($method . ':' . $arDigest['uri']);

			if ($arUser["EXTERNAL_AUTH_ID"] == '' && $arUser["DIGEST_HA1"] != '')
			{
				//digest is for internal authentication only
				static::$kernelSession["BX_HTTP_DIGEST_ABSENT"] = false;

				$HA1 = $arUser["DIGEST_HA1"];
				$valid_response = md5($HA1 . ':' . $arDigest['nonce'] . ':' . $HA2);

				if ($arDigest["response"] === $valid_response)
				{
					//regular user password
					return $this->Login($arDigest["username"], $arUser["PASSWORD"], 'N', 'N');
				}
			}

			//check for an application password, including external users
			if (($appPassword = ApplicationPasswordTable::findDigestPassword($arUser["ID"], $arDigest)) !== false)
			{
				return $this->Login($arDigest["username"], $appPassword["PASSWORD"], 'N', 'N');
			}

			if ($arUser["DIGEST_HA1"] == '')
			{
				//this indicates that we still have no user digest hash
				static::$kernelSession["BX_HTTP_DIGEST_ABSENT"] = true;
			}
		}

		$APPLICATION->ThrowException(GetMessage("USER_AUTH_DIGEST_ERR"));
		return ["MESSAGE" => GetMessage("USER_AUTH_DIGEST_ERR") . "<br>", "TYPE" => "ERROR"];
	}

	public static function UpdateDigest($ID, $pass)
	{
		global $DB;
		$ID = intval($ID);

		$res = $DB->Query("
			SELECT U.LOGIN, UD.DIGEST_HA1
			FROM b_user U LEFT JOIN b_user_digest UD on UD.USER_ID=U.ID
			WHERE U.ID=" . $ID
		);
		if ($arRes = $res->Fetch())
		{
			if (defined('BX_HTTP_AUTH_REALM'))
			{
				$realm = BX_HTTP_AUTH_REALM;
			}
			else
			{
				$realm = "Bitrix Site Manager";
			}

			$digest = md5($arRes["LOGIN"] . ':' . $realm . ':' . $pass);

			if ($arRes["DIGEST_HA1"] == '')
			{
				//new digest
				$DB->Query("insert into b_user_digest (user_id, digest_ha1) values('" . $ID . "', '" . $DB->ForSQL($digest) . "')");
			}
			else
			{
				//update digest (login, password or realm were changed)
				if ($arRes["DIGEST_HA1"] !== $digest)
				{
					$DB->Query("update b_user_digest set digest_ha1='" . $DB->ForSQL($digest) . "' where user_id=" . $ID);
				}
			}
		}
	}

	public function LoginHitByHash($hash, $closeSession = true, $delete = false, $remember = false)
	{
		global $APPLICATION;

		$hash = trim($hash);
		if ($hash == '')
		{
			return false;
		}

		$APPLICATION->ResetException();

		$request = Main\Context::getCurrent()->getRequest();
		$url = str_replace('%', '%%', $request->getRequestedPage());

		$connection = Main\Application::getConnection();
		$helper = $connection->getSqlHelper();

		$query = UserHitAuthTable::query()
			->setSelect(['ID', 'USER_ID', 'HASH', 'VALID_UNTIL'])
			->where('USER.ACTIVE', 'Y')
			->where('USER.BLOCKED', 'N')
			->where('HASH', $hash)
			->whereExpr("%s = left('" . $helper->forSql($url) . "', length(%s))", ['URL', 'URL'])
		;

		if (!defined("ADMIN_SECTION") || ADMIN_SECTION !== true)
		{
			$query->where('SITE_ID', SITE_ID);
		}

		if ($hashData = $query->fetch())
		{
			// case sensitive
			if ($hashData['HASH'] === $hash)
			{
				if ($hashData['VALID_UNTIL'] instanceof Main\Type\DateTime)
				{
					if ((new Main\Type\DateTime())->getTimestamp() > $hashData['VALID_UNTIL']->getTimestamp())
					{
						UserHitAuthTable::delete($hashData['ID']);
						return false;
					}
				}

				setSessionExpired($closeSession);

				$context = (new Authentication\Context())
					->setUserId($hashData["USER_ID"])
					->setHitAuthId($hashData["ID"])
				;

				$this->Authorize($context, $remember);

				if ($delete)
				{
					UserHitAuthTable::delete($hashData['ID']);
				}
				else
				{
					UserHitAuthTable::update($hashData['ID'], ['TIMESTAMP_X' => new Main\Type\DateTime()]);
				}

				return true;
			}
		}

		return false;
	}

	public static function AddHitAuthHash($url, $user_id = false, $site_id = false, $ttl = null)
	{
		global $USER;

		if ($url == '')
		{
			return false;
		}

		if (!$user_id)
		{
			$user_id = $USER->GetID();
		}

		if (!$site_id && (!defined("ADMIN_SECTION") || ADMIN_SECTION !== true))
		{
			$site_id = SITE_ID;
		}

		$hash = false;

		if ($user_id)
		{
			$hash = Random::getString(32, true);

			$fields = [
				'USER_ID' => $user_id,
				'URL' => trim($url),
				'HASH' => $hash,
				'SITE_ID' => trim($site_id),
				'TIMESTAMP_X' => new Main\Type\DateTime(),
			];

			if ($ttl > 0)
			{
				$fields['VALID_UNTIL'] = (new Main\Type\DateTime())->add('T' . (int)$ttl . 'S');
			}

			UserHitAuthTable::add($fields);
		}

		return $hash;
	}

	public static function GetHitAuthHash($urlMask, $userID = false, $siteId = null)
	{
		global $USER;

		$urlMask = trim($urlMask);
		if ($urlMask == '')
		{
			return false;
		}

		if (!$userID)
		{
			$userID = $USER->GetID();
		}

		if ($userID <= 0)
		{
			return false;
		}

		$query = UserHitAuthTable::query()
			->setSelect(['ID', 'HASH', 'VALID_UNTIL'])
			->where('URL', $urlMask)
			->where('USER_ID', $userID)
		;

		if ($siteId !== null)
		{
			$query->where('SITE_ID', $siteId);
		}

		if ($hashData = $query->fetch())
		{
			if ($hashData['VALID_UNTIL'] instanceof Main\Type\DateTime)
			{
				if ((new Main\Type\DateTime())->getTimestamp() > $hashData['VALID_UNTIL']->getTimestamp())
				{
					UserHitAuthTable::delete($hashData['ID']);
					return false;
				}
			}

			return $hashData['HASH'];
		}

		return false;
	}

	public static function CleanUpHitAuthAgent()
	{
		$cleanupDays = (int)Option::get('main', 'hit_auth_cleanup_days', 30);
		if ($cleanupDays > 0)
		{
			UserHitAuthTable::deleteByFilter(['<=TIMESTAMP_X' => (new Main\Type\DateTime())->add("-{$cleanupDays}D")]);
		}
		return 'CUser::CleanUpHitAuthAgent();';
	}

	protected function UpdateSessionData(Authentication\Context $context, $onlyActive = true)
	{
		global $DB, $APPLICATION;

		unset(static::$kernelSession["SESS_OPERATIONS"]);
		$APPLICATION->SetNeedCAPTHA(false);

		$strSql =
			"SELECT U.* " .
			"FROM b_user U  " .
			"WHERE U.ID = " . $context->getUserId();

		if ($onlyActive)
		{
			$strSql .= " AND U.ACTIVE = 'Y' AND U.BLOCKED <> 'Y' ";
		}

		$result = $DB->Query($strSql);

		if ($arUser = $result->Fetch())
		{
			$data = [
				"LOGIN" => $arUser["LOGIN"],
				"EMAIL" => $arUser["EMAIL"],
				"TITLE" => $arUser["TITLE"],
				"NAME" => $arUser["NAME"] . ($arUser["NAME"] == '' || $arUser["LAST_NAME"] == '' ? '' : ' ') . $arUser["LAST_NAME"],
				"FIRST_NAME" => $arUser["NAME"],
				"SECOND_NAME" => $arUser["SECOND_NAME"],
				"LAST_NAME" => $arUser["LAST_NAME"],
				"PERSONAL_PHOTO" => $arUser["PERSONAL_PHOTO"],
				"PERSONAL_GENDER" => $arUser["PERSONAL_GENDER"],
				"EXTERNAL_AUTH_ID" => $arUser["EXTERNAL_AUTH_ID"],
				"XML_ID" => $arUser["XML_ID"],
				"ADMIN" => false,
				"POLICY" => static::getPolicy($arUser["ID"])->getValues(),
				"AUTO_TIME_ZONE" => trim((string)$arUser["AUTO_TIME_ZONE"]),
				"TIME_ZONE" => $arUser["TIME_ZONE"],
				"GROUPS" => Main\UserTable::getUserGroupIds($arUser["ID"]),
				"CONTEXT" => json_encode($context),
			];

			foreach ($data["GROUPS"] as $groupId)
			{
				if ($groupId == 1)
				{
					$data["ADMIN"] = true;
					break;
				}
			}

			static::$kernelSession["SESS_AUTH"] = $data;

			// flag for IsAdmin() optimization
			$this->admin = null;

			$this->context = $context;

			return $arUser;
		}
		return false;
	}

	/**
	 * Performs the user authorization:
	 *    fills session parameters;
	 *    remembers auth;
	 *    spreads auth through sites.
	 * @param Authentication\Context|int $context Contains user id.
	 * @param bool $bSave Save authorization in cookies.
	 * @param bool $bUpdate Update last login information in DB.
	 * @param string|null $applicationId An application password ID.
	 * @return bool
	 */
	public function Authorize($context, $bSave = false, $bUpdate = true, $applicationId = null, $onlyActive = true)
	{
		global $DB;

		// compatibility magic
		if (!($context instanceof Authentication\Context))
		{
			$context = (new Authentication\Context())
				->setUserId($context)
				->setApplicationId($applicationId)
			;
		}

		$arUser = $this->UpdateSessionData($context, $onlyActive);

		if ($arUser !== false)
		{
			$regenerateIdAfterLogin = Main\Config\Configuration::getInstance()->get('session')['regenerateIdAfterLogin'] ?? false;
			if ($regenerateIdAfterLogin === true)
			{
				Main\Application::getInstance()->getCompositeSessionManager()->regenerateId();
			}

			self::$CURRENT_USER = false;
			$this->justAuthorized = true;

			//sometimes we don't need to update db (REST)
			if ($bUpdate)
			{
				$tz = '';
				if (CTimeZone::Enabled())
				{
					if (!CTimeZone::IsAutoTimeZone(trim((string)$arUser["AUTO_TIME_ZONE"])) || CTimeZone::getTzCookie() !== null)
					{
						$tz = ', TIME_ZONE_OFFSET = ' . CTimeZone::GetOffset();
					}
				}

				$bxUid = '';
				if (!empty($_COOKIE['BX_USER_ID']) && preg_match('/^[0-9a-f]{32}$/', $_COOKIE['BX_USER_ID']))
				{
					if ($_COOKIE['BX_USER_ID'] != $arUser['BX_USER_ID'])
					{
						// save new bxuid value
						$bxUid = ", BX_USER_ID = '" . $_COOKIE['BX_USER_ID'] . "'";
						$arUser['BX_USER_ID'] = $_COOKIE['BX_USER_ID'];
					}
				}

				$languageId = '';
				if ($arUser['LANGUAGE_ID'] === '')
				{
					$arUser['LANGUAGE_ID'] = LANGUAGE_ID;
					$languageId = ", LANGUAGE_ID='" . $DB->ForSql(LANGUAGE_ID) . "'";
				}

				$DB->Query("
					UPDATE b_user SET
						STORED_HASH = NULL,
						LAST_LOGIN = " . $DB->GetNowFunction() . ",
						TIMESTAMP_X = TIMESTAMP_X,
						LOGIN_ATTEMPTS = 0
						" . $tz . "
						" . $bxUid . "
						" . $languageId . "
					WHERE
						ID=" . $arUser["ID"]
				);

				if ($bSave || Option::get('main', 'auth_multisite', 'N') == 'Y')
				{
					if (($hash = $context->getStoredAuthHash()) === null)
					{
						$hash = Random::getString(32, true);
					}

					$this->setStoredAuthCookies($arUser["LOGIN"], $hash, $bSave);

					$date = new Main\Type\DateTime();
					$ipAddress = new Main\Web\IpAddress(Main\Context::getCurrent()->getServer()->getRemoteAddr());
					$ipExpr = new Main\DB\SqlExpression($ipAddress->toUnsigned());

					if ($context->getStoredAuthId() > 0)
					{
						UserStoredAuthTable::update($context->getStoredAuthId(), [
							'LAST_AUTH' => $date,
							'IP_ADDR' => $ipExpr,
						]);
					}
					else
					{
						UserStoredAuthTable::add([
							'USER_ID' => $arUser["ID"],
							'DATE_REG' => $date,
							'LAST_AUTH' => $date,
							'TEMP_HASH' => ($bSave ? 'N' : 'Y'),
							'IP_ADDR' => $ipExpr,
							'STORED_HASH' => $hash,
						]);
					}
				}

				if (($applicationPassId = $context->getApplicationPasswordId()) !== null)
				{
					//update usage statistics for the application
					ApplicationPasswordTable::update($applicationPassId, [
						'DATE_LOGIN' => new Main\Type\DateTime(),
						'LAST_IP' => $_SERVER["REMOTE_ADDR"],
					]);
				}

				if (Option::get('main', 'event_log_login_success', 'N') === 'Y')
				{
					CEventLog::Log('SECURITY', 'USER_AUTHORIZE', 'main', $arUser['ID'], $context->getApplicationId());
				}

				if (Option::get('main', 'user_device_history', 'N') === 'Y')
				{
					Device::addLogin($context, $arUser);
				}
			}

			$arParams = [
				"user_fields" => $arUser,
				"save" => $bSave,
				"update" => $bUpdate,
				"applicationId" => $context->getApplicationId(),
			];

			foreach (GetModuleEvents('main', 'OnAfterUserAuthorize', true) as $arEvent)
			{
				ExecuteModuleEventEx($arEvent, [$arParams]);
			}

			foreach (GetModuleEvents('main', 'OnUserLogin', true) as $arEvent)
			{
				ExecuteModuleEventEx($arEvent, [$this->GetID(), $arParams]);
			}

			if ($bUpdate)
			{
				Main\Composite\Engine::onUserLogin();
			}

			//we need it mostrly for the $this->justAuthorized flag
			$this->CheckAuthActions();

			return true;
		}
		return false;
	}

	protected function setStoredAuthCookies($login, $hash, $save)
	{
		$context = Main\Context::getCurrent();
		$response = $context->getResponse();
		$request = $context->getRequest();

		$secure = (Option::get('main', 'use_secure_password_cookies', 'N') == 'Y' && $request->isHttps());

		if ($save)
		{
			$period = time() + 60 * 60 * 24 * 30 * 12;
			$spread = Main\Web\Cookie::SPREAD_SITES | Main\Web\Cookie::SPREAD_DOMAIN;
		}
		else
		{
			$period = 0;
			$spread = Main\Web\Cookie::SPREAD_SITES;
		}

		$cookie = new Main\Web\Cookie('UIDH', $hash, $period);

		$cookie->setSecure($secure)
			->setSpread($spread)
			->setHttpOnly(true)
		;

		$response->addCookie($cookie);

		$cookie = new Main\Web\Cookie('UIDL', $login, $period);

		$cookie->setSecure($secure)
			->setSpread($spread)
			->setHttpOnly(true)
		;

		$response->addCookie($cookie);
	}

	/**
	 * @deprecated Does nothing.
	 */
	public function GetSessionHash()
	{
	}

	/**
	 * @deprecated Does nothing.
	 */
	public function GetPasswordHash($PASSWORD_HASH)
	{
	}

	/**
	 * @deprecated Does nothing.
	 */
	public function SavePasswordHash()
	{
	}

	/**
	 * Authenticates the user and then authorizes him
	 * @param string $login
	 * @param string $password
	 * @param string $remember
	 * @param string $password_original
	 * @return array|bool
	 */
	public function Login($login, $password, $remember = 'N', $password_original = 'Y')
	{
		global $APPLICATION;

		$result_message = true;
		$user_id = 0;
		$context = new Authentication\Context();

		$arParams = [
			"LOGIN" => &$login,
			"PASSWORD" => &$password,
			"REMEMBER" => &$remember,
			"PASSWORD_ORIGINAL" => &$password_original,
		];

		unset(static::$kernelSession["SESS_OPERATIONS"]);
		$APPLICATION->SetNeedCAPTHA(false);

		$bOk = true;
		$APPLICATION->ResetException();
		foreach (GetModuleEvents('main', 'OnBeforeUserLogin', true) as $arEvent)
		{
			if (ExecuteModuleEventEx($arEvent, [&$arParams]) === false)
			{
				if ($err = $APPLICATION->GetException())
				{
					$result_message = ["MESSAGE" => $err->GetString() . "<br>", "TYPE" => "ERROR"];
				}
				else
				{
					$APPLICATION->ThrowException("Unknown login error");
					$result_message = ["MESSAGE" => "Unknown login error" . "<br>", "TYPE" => "ERROR"];
				}

				$bOk = false;
				break;
			}
		}

		if ($bOk)
		{
			//external authentication
			foreach (GetModuleEvents('main', 'OnUserLoginExternal', true) as $arEvent)
			{
				$user_id = ExecuteModuleEventEx($arEvent, [&$arParams]);

				if (isset($arParams["RESULT_MESSAGE"]))
				{
					$result_message = $arParams["RESULT_MESSAGE"];
				}
				if ($user_id > 0)
				{
					break;
				}
			}

			if ($user_id <= 0)
			{
				//internal authentication OR application password for external user
				$user_id = static::LoginInternal($arParams, $result_message, $context);

				if ($user_id <= 0)
				{
					//no user found by login - try to find an external user
					foreach (GetModuleEvents('main', 'OnFindExternalUser', true) as $arEvent)
					{
						if (($external_user_id = intval(ExecuteModuleEventEx($arEvent, [$arParams["LOGIN"]]))) > 0)
						{
							//external user authentication
							//let's try to find application password for the external user
							if (($appPassword = ApplicationPasswordTable::findPassword($external_user_id, $arParams["PASSWORD"], ($arParams["PASSWORD_ORIGINAL"] == 'Y'))) !== false)
							{
								//bingo, the user has the application password
								$user_id = $external_user_id;
								$result_message = true;

								$context
									->setApplicationId($appPassword["APPLICATION_ID"])
									->setApplicationPasswordId($appPassword["ID"])
								;
							}
							break;
						}
					}
				}
			}
		}

		// All except Admin
		if ($user_id > 1 && (!isset($arParams["CONTROLLER_ADMIN"]) || $arParams["CONTROLLER_ADMIN"] !== 'Y'))
		{
			if (!static::CheckUsersCount($user_id))
			{
				$user_id = 0;
				$APPLICATION->ThrowException(GetMessage("LIMIT_USERS_COUNT"));
				$result_message = [
					"MESSAGE" => GetMessage("LIMIT_USERS_COUNT") . "<br>",
					"TYPE" => "ERROR",
				];
			}
		}

		$arParams["USER_ID"] = $user_id;

		$doAuthorize = true;

		if ($user_id > 0)
		{
			if ($context->getApplicationId() === null && CModule::IncludeModule("security"))
			{
				/*
				MFA can allow or disallow authorization.
				Allowed if:
				- OTP is not active for the user;
				- correct "OTP" in the $arParams (filled by the OnBeforeUserLogin event handler).
				Disallowed if:
				- OTP is not provided;
				- OTP is not correct.
				When authorization is disallowed the OTP form will be shown on the next hit.
				Note: there is no MFA check for an application password.
				*/

				$arParams["CAPTCHA_WORD"] = $_REQUEST["captcha_word"] ?? '';
				$arParams["CAPTCHA_SID"] = $_REQUEST["captcha_sid"] ?? '';

				$doAuthorize = \Bitrix\Security\Mfa\Otp::verifyUser($arParams);
			}

			if ($doAuthorize)
			{
				$context->setUserId($user_id);

				$this->Authorize($context, ($arParams["REMEMBER"] == 'Y'));
			}
			else
			{
				$result_message = false;
			}

			if ($context->getApplicationId() === null && $arParams["LOGIN"] != '')
			{
				//the cookie is for authentication forms mostly, does not make sense for applications
				$cookie = new Main\Web\Cookie("LOGIN", $arParams["LOGIN"], time() + 60 * 60 * 24 * 30 * 12);
				Main\Context::getCurrent()->getResponse()->addCookie($cookie);
			}
		}
		else
		{
			if (CModule::IncludeModule("security"))
			{
				//disable OTP from if login was incorrect
				\Bitrix\Security\Mfa\Otp::setDeferredParams(null);
			}
		}

		$arParams["RESULT_MESSAGE"] = $result_message;

		$APPLICATION->ResetException();

		foreach (GetModuleEvents('main', 'OnAfterUserLogin', true) as $arEvent)
		{
			ExecuteModuleEventEx($arEvent, [&$arParams]);
		}

		if ($doAuthorize && $result_message !== true && (Option::get('main', 'event_log_login_fail', 'N') === 'Y'))
		{
			CEventLog::Log('SECURITY', 'USER_LOGIN', 'main', $login, $result_message['MESSAGE']);
		}

		return $arParams["RESULT_MESSAGE"];
	}

	/**
	 * Internal authentication by login and password.
	 * @param array $arParams
	 * @param array|bool $result_message
	 * @param Authentication\Context|null $context
	 * @return int User ID on success or 0 on failure. Additionally, $result_message will hold an error.
	 */
	public static function LoginInternal(&$arParams, &$result_message = true, $context = null)
	{
		global $DB, $APPLICATION;

		$user_id = 0;
		$message = GetMessage("WRONG_LOGIN");
		$errorType = "LOGIN";

		$strSql =
			"SELECT U.ID, U.LOGIN, U.ACTIVE, U.BLOCKED, U.PASSWORD, U.PASSWORD_EXPIRED, U.LOGIN_ATTEMPTS, U.CONFIRM_CODE, U.EMAIL " .
			"FROM b_user U  " .
			"WHERE U.LOGIN='" . $DB->ForSQL($arParams["LOGIN"]) . "' ";

		if (isset($arParams["EXTERNAL_AUTH_ID"]) && $arParams["EXTERNAL_AUTH_ID"] != '')
		{
			//external user
			$strSql .= " AND EXTERNAL_AUTH_ID='" . $DB->ForSql($arParams["EXTERNAL_AUTH_ID"]) . "'";
		}
		else
		{
			//internal user (by default)
			$strSql .= " AND (EXTERNAL_AUTH_ID IS NULL OR EXTERNAL_AUTH_ID='') ";
		}

		$result = $DB->Query($strSql);

		if (($arUser = $result->Fetch()))
		{
			$passwordCorrect = false;
			$policy = null;
			$applicationId = null;
			$original = isset($arParams["PASSWORD_ORIGINAL"]) && $arParams["PASSWORD_ORIGINAL"] === 'Y';
			$loginAttempts = intval($arUser["LOGIN_ATTEMPTS"]) + 1;

			if ($arUser["BLOCKED"] != 'Y')
			{
				$policy = static::getPolicy($arUser["ID"]);

				//show captcha after a serial of incorrect login attempts
				$correctCaptcha = true;
				$policyLoginAttempts = (int)$policy->getLoginAttempts();
				if ($policyLoginAttempts > 0 && $loginAttempts > $policyLoginAttempts)
				{
					$APPLICATION->SetNeedCAPTHA(true);
					if (!$APPLICATION->CaptchaCheckCode($_REQUEST["captcha_word"] ?? '', $_REQUEST["captcha_sid"] ?? ''))
					{
						$correctCaptcha = false;
					}
				}

				if ($correctCaptcha)
				{
					$passwordCorrect = Password::equals($arUser["PASSWORD"], $arParams["PASSWORD"], $original);

					if (!$passwordCorrect)
					{
						if (isset($arParams["OTP"]) && $arParams["OTP"] != '' && $original)
						{
							// maybe we have OTP added to the password
							$passwordWithoutOtp = mb_substr($arParams["PASSWORD"], 0, -6);
							$passwordCorrect = Password::equals($arUser["PASSWORD"], $passwordWithoutOtp);
						}
					}
					else
					{
						//this password has no added otp for sure
						$arParams["OTP"] = '';
					}

					if (!$passwordCorrect)
					{
						//let's try to find application password
						if (($appPassword = ApplicationPasswordTable::findPassword($arUser["ID"], $arParams["PASSWORD"], $original)) !== false)
						{
							$passwordCorrect = true;
							$applicationId = $appPassword["APPLICATION_ID"];

							if ($context instanceof Authentication\Context)
							{
								$context
									->setApplicationId($applicationId)
									->setApplicationPasswordId($appPassword["ID"])
								;
							}
						}
					}
				}

				if (!$passwordCorrect)
				{
					//block the user after numerous incorrect login attempts
					$policyBlockAttempts = (int)$policy->getBlockLoginAttempts();
					$policyBlockTime = (int)$policy->getBlockTime();
					if ($policyBlockAttempts > 0 && $policyBlockTime > 0 && $loginAttempts >= $policyBlockAttempts)
					{
						if ($arUser["ACTIVE"] == 'Y')
						{
							static::blockUser($arUser["ID"], $policyBlockTime, $loginAttempts);
						}
					}
				}
			}

			if ($passwordCorrect)
			{
				//applied only to "human" passwords
				if ($applicationId === null)
				{
					//only for original passwords
					if ($original)
					{
						//update the old password hash to the new one with a salt
						if (Password::needRehash($arUser["PASSWORD"]))
						{
							$newPassword = Password::hash($arParams["PASSWORD"]);
							$DB->Query("UPDATE b_user SET PASSWORD='" . $DB->ForSQL($newPassword) . "', TIMESTAMP_X = TIMESTAMP_X WHERE ID = " . intval($arUser["ID"]));
						}

						//update digest hash for http digest authorization
						if (Option::get('main', 'use_digest_auth', 'N') == 'Y')
						{
							static::UpdateDigest($arUser["ID"], $arParams["PASSWORD"]);
						}
					}

					$passwordExpired = false;
					if ($arUser['PASSWORD_EXPIRED'] == 'Y')
					{
						//require to change the password right now
						$passwordExpired = true;
					}
					if (!$passwordExpired && $original && $policy->getPasswordCheckPolicy())
					{
						$passwordErrors = static::CheckPasswordAgainstPolicy($arParams["PASSWORD"], $policy->getValues());
						if (!empty($passwordErrors))
						{
							//require to change the password because it doesn't match the group policy
							$passwordExpired = true;
						}
					}
					if (!$passwordExpired)
					{
						$policyChangeDays = (int)$policy->getPasswordChangeDays();
						if ($policyChangeDays > 0)
						{
							//require to change the password after N days
							if (UserPasswordTable::passwordExpired($arUser["ID"], $policyChangeDays))
							{
								$passwordExpired = true;
							}
						}
					}

					if ($passwordExpired)
					{
						$passwordCorrect = false;
						$message = GetMessage("MAIN_LOGIN_CHANGE_PASSWORD");
						$errorType = "CHANGE_PASSWORD";
					}
				}

				if ($passwordCorrect)
				{
					if ($arUser["ACTIVE"] == 'Y')
					{
						//success
						$user_id = $arUser["ID"];
					}
					else
					{
						//something wrong with the inactive user
						if ($arUser["CONFIRM_CODE"] != '')
						{
							//unconfirmed email registration
							$message = GetMessage("MAIN_LOGIN_EMAIL_CONFIRM", ["#EMAIL#" => $arUser["EMAIL"]]);
						}
						else
						{
							//user deactivated
							$message = GetMessage("LOGIN_BLOCK");

							//or possibly unconfirmed phone registration
							if (Option::get('main', 'new_user_phone_auth', 'N') == 'Y')
							{
								$row = Main\UserPhoneAuthTable::getRowById($arUser["ID"]);
								if ($row && $row["CONFIRMED"] == 'N')
								{
									$message = GetMessage("main_login_need_phone_confirmation", ["#PHONE#" => $row["PHONE_NUMBER"]]);
								}
							}
						}
					}
				}
			}
			else
			{
				//incorrect password
				$DB->Query("UPDATE b_user SET LOGIN_ATTEMPTS = " . $loginAttempts . ", TIMESTAMP_X = TIMESTAMP_X WHERE ID = " . intval($arUser["ID"]));
			}
		}

		if ($user_id == 0)
		{
			$APPLICATION->ThrowException($message);
			$result_message = ["MESSAGE" => $message . "<br>", "TYPE" => "ERROR", "ERROR_TYPE" => $errorType];
		}

		return $user_id;
	}

	protected static function blockUser($userId, $blockTime, $loginAttempts)
	{
		$user = new CUser();
		$user->Update($userId, ["BLOCKED" => 'Y'], false);

		$unblockDate = new Main\Type\DateTime();
		$unblockDate->add("T{$blockTime}M"); //minutes

		CAgent::AddAgent("CUser::UnblockAgent({$userId});", 'main', 'Y', 0, '', 'Y', $unblockDate->toString());

		if (Option::get('main', 'event_log_block_user', 'N') === 'Y')
		{
			CEventLog::Log('SECURITY', 'USER_BLOCKED', 'main', $userId, "Attempts: {$loginAttempts}, Block period: {$blockTime}");
		}
	}

	private static function CheckUsersCount($user_id)
	{
		$user_id = (int)$user_id;
		$license = Main\Application::getInstance()->getLicense();
		$limitUsersCount = $license->getMaxUsers();

		if ($limitUsersCount > 0)
		{
			// users logged in today
			$today = new Main\Type\Date();
			$count = $license->getActiveUsersCount($today);

			if ($count >= $limitUsersCount)
			{
				// additional check for the current user

				$select = ['LAST_LOGIN'];

				$intranet = Main\ModuleManager::isModuleInstalled("intranet");
				if ($intranet)
				{
					$select[] = 'UF_DEPARTMENT';
				}

				// last_login in server time
				CTimeZone::Disable();

				$query = static::GetList('id', 'asc',
					['ID_EQUAL_EXACT' => $user_id],
					['SELECT' => $select]
				);

				CTimeZone::Enable();

				if ($currentUser = $query->Fetch())
				{
					if ($currentUser["LAST_LOGIN"] != '')
					{
						$loginDate = new Main\Type\DateTime($currentUser["LAST_LOGIN"]);
						if ($loginDate->getTimestamp() > $today->getTimestamp())
						{
							// if the user already logged in today, he is allowed
							return true;
						}
					}

					if ($intranet && empty($currentUser["UF_DEPARTMENT"]))
					{
						// only intranet AND extranet users are countable
						if ($license->isExtraCountable())
						{
							$groupId = (int)Option::get('extranet', 'extranet_group');
							if ($groupId > 0 && in_array($groupId, static::GetUserGroup($user_id)))
							{
								return false;
							}
						}
						return true;
					}
				}
				return false;
			}
		}
		return true;
	}

	public function LoginByOtp($otp, $remember_otp = 'N', $captcha_word = '', $captcha_sid = '')
	{
		if (!CModule::IncludeModule("security") || !\Bitrix\Security\Mfa\Otp::isOtpRequired())
		{
			return ["MESSAGE" => GetMessage("USER_LOGIN_OTP_ERROR") . "<br>", "TYPE" => "ERROR"];
		}

		$userParams = \Bitrix\Security\Mfa\Otp::getDeferredParams();

		$userParams["OTP"] = $otp;
		$userParams["OTP_REMEMBER"] = ($remember_otp === 'Y');
		$userParams["CAPTCHA_WORD"] = $captcha_word;
		$userParams["CAPTCHA_SID"] = $captcha_sid;

		if (!\Bitrix\Security\Mfa\Otp::verifyUser($userParams))
		{
			return ["MESSAGE" => GetMessage("USER_LOGIN_OTP_INCORRECT") . "<br>", "TYPE" => "ERROR"];
		}

		$this->Authorize($userParams["USER_ID"], ($userParams["REMEMBER"] == 'Y'));
		return true;
	}

	public function AuthorizeWithOtp($user_id, $bSave = false)
	{
		$doAuthorize = true;

		if (CModule::IncludeModule("security"))
		{
			/*
			MFA can allow or disallow authorization.
			Allowed only if:
			- OTP is not active for the user;
			When authorization is disallowed the OTP form will be shown on the next hit.
			*/
			$doAuthorize = \Bitrix\Security\Mfa\Otp::verifyUser(["USER_ID" => $user_id]);
		}

		if ($doAuthorize)
		{
			return $this->Authorize($user_id, $bSave);
		}

		return false;
	}

	public function ChangePassword($LOGIN, $CHECKWORD, $PASSWORD, $CONFIRM_PASSWORD, $SITE_ID = false, $captcha_word = '', $captcha_sid = 0, $authActions = true, $phoneNumber = '', $currentPassword = '')
	{
		/** @global CMain $APPLICATION */
		global $DB, $APPLICATION;

		$arParams = [
			"LOGIN" => &$LOGIN,
			"CHECKWORD" => &$CHECKWORD,
			"PASSWORD" => &$PASSWORD,
			"CONFIRM_PASSWORD" => &$CONFIRM_PASSWORD,
			"SITE_ID" => &$SITE_ID,
			"PHONE_NUMBER" => &$phoneNumber,
			"CURRENT_PASSWORD" => &$currentPassword,
		];

		$APPLICATION->ResetException();
		foreach (GetModuleEvents('main', 'OnBeforeUserChangePassword', true) as $arEvent)
		{
			if (ExecuteModuleEventEx($arEvent, [&$arParams]) === false)
			{
				if ($err = $APPLICATION->GetException())
				{
					return ["MESSAGE" => $err->GetString() . "<br>", "TYPE" => "ERROR"];
				}
				return ["MESSAGE" => GetMessage("main_change_pass_error") . "<br>", "TYPE" => "ERROR"];
			}
		}

		if (Option::get('main', 'captcha_restoring_password', 'N') == 'Y')
		{
			if (!($APPLICATION->CaptchaCheckCode($captcha_word, $captcha_sid)))
			{
				return ["MESSAGE" => GetMessage("main_user_captcha_error") . "<br>", "TYPE" => "ERROR"];
			}
		}

		$phoneAuth = ($arParams["PHONE_NUMBER"] != '' && Option::get('main', 'new_user_phone_auth', 'N') == 'Y');

		$strAuthError = '';
		if (mb_strlen($arParams["LOGIN"]) < 3 && !$phoneAuth)
		{
			$strAuthError .= GetMessage('MIN_LOGIN') . "<br>";
		}
		if ($arParams["CHECKWORD"] == '' && $arParams["CURRENT_PASSWORD"] == '')
		{
			$strAuthError .= GetMessage("main_change_pass_empty_checkword") . "<br>";
		}
		if ($arParams["PASSWORD"] != $arParams["CONFIRM_PASSWORD"])
		{
			$strAuthError .= GetMessage('WRONG_CONFIRMATION') . "<br>";
		}
		if ($strAuthError != '')
		{
			return ["MESSAGE" => $strAuthError, "TYPE" => "ERROR"];
		}

		$updateFields = [
			"PASSWORD" => $arParams["PASSWORD"],
		];

		$res = [];
		if ($phoneAuth)
		{
			$userId = static::VerifyPhoneCode($arParams["PHONE_NUMBER"], $arParams["CHECKWORD"]);

			if (!$userId)
			{
				return ["MESSAGE" => GetMessage("main_change_pass_code_error"), "TYPE" => "ERROR"];
			}

			//activate user after phone number confirmation
			$updateFields["ACTIVE"] = 'Y';
		}
		else
		{
			CTimeZone::Disable();
			$db_check = $DB->Query(
				"SELECT ID, LID, CHECKWORD, " . $DB->DateToCharFunction("CHECKWORD_TIME") . " as CHECKWORD_TIME, PASSWORD, LOGIN_ATTEMPTS, ACTIVE, BLOCKED " .
				"FROM b_user " .
				"WHERE LOGIN='" . $DB->ForSql($arParams["LOGIN"]) . "'" .
				(
					// $arParams["EXTERNAL_AUTH_ID"] can be changed in the OnBeforeUserChangePassword event
				$arParams["EXTERNAL_AUTH_ID"] != '' ?
					"	AND EXTERNAL_AUTH_ID='" . $DB->ForSQL($arParams["EXTERNAL_AUTH_ID"]) . "' " :
					"	AND (EXTERNAL_AUTH_ID IS NULL OR EXTERNAL_AUTH_ID='') "
				)
			);
			CTimeZone::Enable();

			if (!($res = $db_check->Fetch()))
			{
				return ["MESSAGE" => GetMessage('LOGIN_NOT_FOUND1'), "TYPE" => "ERROR", "FIELD" => "LOGIN"];
			}

			$userId = $res["ID"];
		}

		$policy = static::getPolicy($userId);
		$arPolicy = $policy->getValues();

		$passwordErrors = static::CheckPasswordAgainstPolicy($arParams["PASSWORD"], $arPolicy);
		if (!empty($passwordErrors))
		{
			return ["MESSAGE" => implode("<br>", $passwordErrors) . "<br>", "TYPE" => "ERROR"];
		}

		if (!$phoneAuth)
		{
			if ($arParams["CHECKWORD"] != '')
			{
				//change the password using the checkword
				if ($res["CHECKWORD"] == '' || !Password::equals($res["CHECKWORD"], $arParams["CHECKWORD"]))
				{
					return ["MESSAGE" => GetMessage("CHECKWORD_INCORRECT1") . "<br>", "TYPE" => "ERROR", "FIELD" => "CHECKWORD"];
				}

				$site_format = CSite::GetDateFormat();
				if (time() - $policy->getCheckwordTimeout() * 60 > MakeTimeStamp($res["CHECKWORD_TIME"], $site_format))
				{
					return ["MESSAGE" => GetMessage("CHECKWORD_EXPIRE") . "<br>", "TYPE" => "ERROR", "FIELD" => "CHECKWORD_EXPIRE"];
				}
			}
			else
			{
				//change the password using the current password
				$loginAttempts = intval($res["LOGIN_ATTEMPTS"]) + 1;

				//show captcha after a serial of incorrect login attempts
				$policyLoginAttempts = (int)$policy->getLoginAttempts();
				if ($policyLoginAttempts > 0 && $loginAttempts > $policyLoginAttempts)
				{
					$APPLICATION->SetNeedCAPTHA(true);
					if (!$APPLICATION->CaptchaCheckCode($captcha_word, $captcha_sid))
					{
						return ["MESSAGE" => GetMessage("main_user_captcha_error") . "<br>", "TYPE" => "ERROR"];
					}
				}

				$passwordCorrect = false;

				if ($res["BLOCKED"] != 'Y')
				{
					$passwordCorrect = Password::equals($res["PASSWORD"], $arParams["CURRENT_PASSWORD"]);

					if (!$passwordCorrect)
					{
						//block the user after numerous incorrect login attempts
						$policyBlockAttempts = (int)$policy->getBlockLoginAttempts();
						$policyBlockTime = (int)$policy->getBlockTime();
						if ($policyBlockAttempts > 0 && $policyBlockTime > 0 && $loginAttempts >= $policyBlockAttempts)
						{
							if ($res["ACTIVE"] == 'Y')
							{
								static::blockUser($res["ID"], $policyBlockTime, $loginAttempts);
							}
						}
					}

					if ($passwordCorrect)
					{
						$passwordErrors = static::CheckPasswordAgainstPolicy($arParams["PASSWORD"], $arPolicy, $res["ID"]);
						if (!empty($passwordErrors))
						{
							return ["MESSAGE" => implode("<br>", $passwordErrors) . "<br>", "TYPE" => "ERROR"];
						}

						$APPLICATION->SetNeedCAPTHA(false);
					}
				}

				if (!$passwordCorrect)
				{
					//incorrect password
					$DB->Query("UPDATE b_user SET LOGIN_ATTEMPTS = " . $loginAttempts . ", TIMESTAMP_X = TIMESTAMP_X WHERE ID = " . intval($res["ID"]));

					return ["MESSAGE" => GetMessage("main_change_pass_incorrect_pass") . "<br>", "TYPE" => "ERROR", "FIELD" => "CURRENT_PASSWORD"];
				}
			}

			if ($arParams["SITE_ID"] === false)
			{
				if (defined("ADMIN_SECTION") && ADMIN_SECTION === true)
				{
					$arParams["SITE_ID"] = CSite::GetDefSite($res["LID"]);
				}
				else
				{
					$arParams["SITE_ID"] = SITE_ID;
				}
			}
		}

		// change the password
		$obUser = new CUser;
		$res = $obUser->Update($userId, $updateFields, $authActions);

		if (!$res && $obUser->LAST_ERROR != '')
		{
			return ["MESSAGE" => $obUser->LAST_ERROR . "<br>", "TYPE" => "ERROR"];
		}

		if ($phoneAuth)
		{
			return ["MESSAGE" => GetMessage("main_change_pass_changed") . "<br>", "TYPE" => "OK"];
		}
		else
		{
			static::SendUserInfo($userId, $arParams["SITE_ID"], GetMessage('CHANGE_PASS_SUCC'), true, 'USER_PASS_CHANGED');

			return ["MESSAGE" => GetMessage('PASSWORD_CHANGE_OK') . "<br>", "TYPE" => "OK"];
		}
	}

	public static function GeneratePasswordByPolicy(array $groups)
	{
		$policy = static::getPolicy($groups);

		$passwordChars = Random::ALPHABET_NUM | Random::ALPHABET_ALPHALOWER | Random::ALPHABET_ALPHAUPPER;
		if ($policy->getPasswordPunctuation())
		{
			$passwordChars |= Random::ALPHABET_SPECIAL;
		}

		$length = (int)$policy->getPasswordLength();

		return Random::getStringByAlphabet($length, $passwordChars, true);
	}

	public static function CheckPasswordAgainstPolicy($password, $arPolicy, $userId = null)
	{
		$errors = [];

		$passwordMinLength = intval($arPolicy['PASSWORD_LENGTH']);
		if ($passwordMinLength <= 0)
		{
			$passwordMinLength = 6;
		}
		if (mb_strlen($password) < $passwordMinLength)
		{
			$errors[] = GetMessage('MAIN_FUNCTION_REGISTER_PASSWORD_LENGTH', ['#LENGTH#' => $arPolicy['PASSWORD_LENGTH']]);
		}

		if (($arPolicy['PASSWORD_UPPERCASE'] === 'Y') && !preg_match('/[A-Z]/', $password))
		{
			$errors[] = GetMessage('MAIN_FUNCTION_REGISTER_PASSWORD_UPPERCASE');
		}

		if (($arPolicy['PASSWORD_LOWERCASE'] === 'Y') && !preg_match('/[a-z]/', $password))
		{
			$errors[] = GetMessage('MAIN_FUNCTION_REGISTER_PASSWORD_LOWERCASE');
		}

		if (($arPolicy['PASSWORD_DIGITS'] === 'Y') && !preg_match('/[0-9]/', $password))
		{
			$errors[] = GetMessage('MAIN_FUNCTION_REGISTER_PASSWORD_DIGITS');
		}

		if (($arPolicy['PASSWORD_PUNCTUATION'] === 'Y') && !preg_match('/[' . preg_quote(static::PASSWORD_SPECIAL_CHARS, '/') . ']/', $password))
		{
			$errors[] = GetMessage('MAIN_FUNCTION_REGISTER_PASSWORD_PUNCTUATION', ['#SPECIAL_CHARS#' => static::PASSWORD_SPECIAL_CHARS]);
		}

		if (($arPolicy['PASSWORD_CHECK_WEAK'] === 'Y'))
		{
			if (Option::get('main', 'custom_weak_passwords') === 'Y')
			{
				$uploadDir = Option::get('main', 'upload_dir', 'upload');
				$path = "{$_SERVER['DOCUMENT_ROOT']}/{$uploadDir}/main/weak_passwords";
			}
			else
			{
				$path = "{$_SERVER['DOCUMENT_ROOT']}/bitrix/modules/main/data/weak_passwords";
			}
			if (Policy\WeakPassword::exists($password, $path))
			{
				$errors[] = GetMessage('main_check_password_weak');
			}
		}

		if ($userId !== null && $arPolicy['PASSWORD_UNIQUE_COUNT'] > 0)
		{
			$passwords = UserPasswordTable::getUserPasswords($userId, $arPolicy['PASSWORD_UNIQUE_COUNT']);

			foreach ($passwords as $previousPassword)
			{
				if (Password::equals($previousPassword['PASSWORD'], $password))
				{
					$errors[] = GetMessage('MAIN_FUNCTION_REGISTER_PASSWORD_UNIQUE');
					break;
				}
			}
		}

		return $errors;
	}

	/**
	 * Sends a profile information to email
	 */
	public static function SendUserInfo($ID, $SITE_ID, $MSG, $bImmediate = false, $eventName = "USER_INFO", $checkword = null)
	{
		global $DB;

		$arParams = [
			"ID" => $ID,
			"SITE_ID" => $SITE_ID,
		];

		foreach (GetModuleEvents('main', 'OnBeforeSendUserInfo', true) as $arEvent)
		{
			if (ExecuteModuleEventEx($arEvent, [&$arParams]) === false)
			{
				return;
			}
		}

		$ID = intval($ID);

		if ($checkword === null)
		{
			// change CHECKWORD
			$checkword = Random::getString(32);

			$strSql = "UPDATE b_user SET " .
				"	CHECKWORD = '" . Password::hash($checkword) . "', " .
				"	CHECKWORD_TIME = " . $DB->CurrentTimeFunction() . ", " .
				"	LID = '" . $DB->ForSql($SITE_ID, 2) . "', " .
				"   TIMESTAMP_X = TIMESTAMP_X " .
				"WHERE ID = '" . $ID . "'" .
				(
					// $arParams["EXTERNAL_AUTH_ID"] can be changed in the OnBeforeSendUserInfo event
				isset($arParams["EXTERNAL_AUTH_ID"]) && $arParams["EXTERNAL_AUTH_ID"] != '' ?
					"	AND EXTERNAL_AUTH_ID='" . $DB->ForSQL($arParams["EXTERNAL_AUTH_ID"]) . "' " :
					"	AND (EXTERNAL_AUTH_ID IS NULL OR EXTERNAL_AUTH_ID='') "
				);

			$DB->Query($strSql);
		}

		$res = $DB->Query(
			"SELECT u.* " .
			"FROM b_user u " .
			"WHERE ID='" . $ID . "'" .
			(
			isset($arParams["EXTERNAL_AUTH_ID"]) && $arParams["EXTERNAL_AUTH_ID"] != '' ?
				"	AND EXTERNAL_AUTH_ID='" . $DB->ForSQL($arParams["EXTERNAL_AUTH_ID"]) . "' " :
				"	AND (EXTERNAL_AUTH_ID IS NULL OR EXTERNAL_AUTH_ID='') "
			)
		);

		if ($res_array = $res->Fetch())
		{
			$event = new CEvent;
			$arFields = [
				"USER_ID" => $res_array["ID"],
				"STATUS" => ($res_array["ACTIVE"] == 'Y' ? GetMessage("STATUS_ACTIVE") : GetMessage("STATUS_BLOCKED")),
				"MESSAGE" => $MSG,
				"LOGIN" => $res_array["LOGIN"],
				"URL_LOGIN" => urlencode($res_array["LOGIN"]),
				"CHECKWORD" => $checkword,
				"NAME" => $res_array["NAME"],
				"LAST_NAME" => $res_array["LAST_NAME"],
				"EMAIL" => $res_array["EMAIL"],
			];

			$arParams = [
				"FIELDS" => &$arFields,
				"USER_FIELDS" => $res_array,
				"SITE_ID" => &$SITE_ID,
				"EVENT_NAME" => &$eventName,
			];

			foreach (GetModuleEvents('main', 'OnSendUserInfo', true) as $arEvent)
			{
				ExecuteModuleEventEx($arEvent, [&$arParams]);
			}

			if (!$bImmediate)
			{
				$event->Send($eventName, $SITE_ID, $arFields, 'Y', '', [], $res_array["LANGUAGE_ID"]);
			}
			else
			{
				$event->SendImmediate($eventName, $SITE_ID, $arFields, 'Y', '', [], $res_array["LANGUAGE_ID"]);
			}
		}
	}

	public static function SendPassword($LOGIN, $EMAIL, $SITE_ID = false, $captcha_word = '', $captcha_sid = 0, $phoneNumber = '', $shortCode = false)
	{
		/** @global CMain $APPLICATION */
		global $DB, $APPLICATION;

		$arParams = [
			"LOGIN" => $LOGIN,
			"EMAIL" => $EMAIL,
			"SITE_ID" => $SITE_ID,
			"PHONE_NUMBER" => $phoneNumber,
			"SHORT_CODE" => $shortCode,
		];

		$result_message = ["MESSAGE" => GetMessage('ACCOUNT_INFO_SENT') . "<br>", "TYPE" => "OK"];
		$APPLICATION->ResetException();
		$bOk = true;
		foreach (GetModuleEvents('main', 'OnBeforeUserSendPassword', true) as $arEvent)
		{
			if (ExecuteModuleEventEx($arEvent, [&$arParams]) === false)
			{
				if ($err = $APPLICATION->GetException())
				{
					$result_message = ["MESSAGE" => $err->GetString() . "<br>", "TYPE" => "ERROR"];
				}

				$bOk = false;
				break;
			}
		}

		if ($bOk && !$arParams["SHORT_CODE"] && Option::get('main', 'captcha_restoring_password', 'N') == 'Y')
		{
			if (!($APPLICATION->CaptchaCheckCode($captcha_word, $captcha_sid)))
			{
				$result_message = ["MESSAGE" => GetMessage("main_user_captcha_error") . "<br>", "TYPE" => "ERROR"];
				$bOk = false;
			}
		}

		if ($bOk)
		{
			$found = false;
			if ($arParams["PHONE_NUMBER"] != '')
			{
				//user registered by phone number

				$siteId = ($arParams["SITE_ID"] === false ? null : $arParams["SITE_ID"]);

				$result = static::SendPhoneCode($arParams["PHONE_NUMBER"], "SMS_USER_RESTORE_PASSWORD", $siteId);

				$result_message = ["MESSAGE" => GetMessage("main_user_pass_request_sent") . "<br>", "TYPE" => "OK", "TEMPLATE" => "SMS_USER_RESTORE_PASSWORD"];

				if ($result->isSuccess())
				{
					$found = true;

					if (Option::get('main', 'event_log_password_request', 'N') === 'Y')
					{
						$data = $result->getData();
						CEventLog::Log('SECURITY', 'USER_INFO', 'main', $data["USER_ID"]);
					}
				}
				else
				{
					if ($result->getErrorCollection()->getErrorByCode("ERR_NOT_FOUND") === null)
					{
						//user found but there is another error
						$found = true;
						$result_message = ["MESSAGE" => implode("<br>", $result->getErrorMessages()), "TYPE" => "ERROR"];
					}
				}
			}
			elseif ($arParams["LOGIN"] != '' || $arParams["EMAIL"] != '')
			{
				$confirmation = (Option::get('main', 'new_user_registration_email_confirmation', 'N') == 'Y');

				$strSql = '';
				if ($arParams["LOGIN"] != '')
				{
					$strSql =
						"SELECT ID, LID, ACTIVE, BLOCKED, CONFIRM_CODE, LOGIN, EMAIL, NAME, LAST_NAME, LANGUAGE_ID " .
						"FROM b_user u " .
						"WHERE LOGIN='" . $DB->ForSQL($arParams["LOGIN"]) . "' " .
						"	AND (ACTIVE='Y' OR NOT(CONFIRM_CODE IS NULL OR CONFIRM_CODE='')) " .
						(
							// $arParams["EXTERNAL_AUTH_ID"] can be changed in the OnBeforeUserSendPassword event
						isset($arParams["EXTERNAL_AUTH_ID"]) && $arParams["EXTERNAL_AUTH_ID"] != '' ?
							"	AND EXTERNAL_AUTH_ID='" . $DB->ForSQL($arParams["EXTERNAL_AUTH_ID"]) . "' " :
							"	AND (EXTERNAL_AUTH_ID IS NULL OR EXTERNAL_AUTH_ID='') "
						);
				}
				if ($arParams["EMAIL"] != '')
				{
					if ($strSql != '')
					{
						$strSql .= "\nUNION\n";
					}
					$strSql .=
						"SELECT ID, LID, ACTIVE, BLOCKED, CONFIRM_CODE, LOGIN, EMAIL, NAME, LAST_NAME, LANGUAGE_ID " .
						"FROM b_user u " .
						"WHERE EMAIL='" . $DB->ForSQL($arParams["EMAIL"]) . "' " .
						"	AND (ACTIVE='Y' OR NOT(CONFIRM_CODE IS NULL OR CONFIRM_CODE='')) " .
						(
						isset($arParams["EXTERNAL_AUTH_ID"]) && $arParams["EXTERNAL_AUTH_ID"] != '' ?
							"	AND EXTERNAL_AUTH_ID='" . $DB->ForSQL($arParams["EXTERNAL_AUTH_ID"]) . "' " :
							"	AND (EXTERNAL_AUTH_ID IS NULL OR EXTERNAL_AUTH_ID='') "
						);
				}
				$res = $DB->Query($strSql);

				while ($arUser = $res->Fetch())
				{
					if ($arParams["SITE_ID"] === false)
					{
						if (defined("ADMIN_SECTION") && ADMIN_SECTION === true)
						{
							$arParams["SITE_ID"] = CSite::GetDefSite($arUser["LID"]);
						}
						else
						{
							$arParams["SITE_ID"] = SITE_ID;
						}
					}

					if ($arUser["ACTIVE"] == 'Y')
					{
						if ($arUser["BLOCKED"] != 'Y')
						{
							$found = true;

							if ($arParams["SHORT_CODE"])
							{
								$result = static::SendEmailCode($arUser["ID"], $arParams["SITE_ID"]);

								if ($result->isSuccess())
								{
									$result_message = ["MESSAGE" => GetMessage("main_send_password_email_code") . "<br>", "TYPE" => "OK", "USER_ID" => $arUser["ID"], "RESULT" => $result];
								}
								else
								{
									$result_message = ["MESSAGE" => implode("<br>", $result->getErrorMessages()), "TYPE" => "ERROR", "RESULT" => $result];
								}
							}
							else
							{
								static::SendUserInfo($arUser["ID"], $arParams["SITE_ID"], GetMessage("INFO_REQ"), true, 'USER_PASS_REQUEST');
							}
						}
					}
					elseif ($confirmation)
					{
						$found = true;

						//unconfirmed registration - resend confirmation email
						$arFields = [
							"USER_ID" => $arUser["ID"],
							"LOGIN" => $arUser["LOGIN"],
							"EMAIL" => $arUser["EMAIL"],
							"NAME" => $arUser["NAME"],
							"LAST_NAME" => $arUser["LAST_NAME"],
							"CONFIRM_CODE" => $arUser["CONFIRM_CODE"],
							"USER_IP" => $_SERVER["REMOTE_ADDR"],
							"USER_HOST" => @gethostbyaddr($_SERVER["REMOTE_ADDR"]),
						];

						$event = new CEvent;
						$event->SendImmediate("NEW_USER_CONFIRM", $arParams["SITE_ID"], $arFields, 'Y', '', [], $arUser["LANGUAGE_ID"]);

						$result_message = ["MESSAGE" => GetMessage("MAIN_SEND_PASS_CONFIRM") . "<br>", "TYPE" => "OK"];
					}

					if (Option::get('main', 'event_log_password_request', 'N') === 'Y')
					{
						CEventLog::Log('SECURITY', 'USER_INFO', 'main', $arUser['ID']);
					}
				}
			}
			if (!$found)
			{
				if (Option::get('main', 'event_log_password_request', 'N') === 'Y')
				{
					$userInfo = $arParams["PHONE_NUMBER"] ?: $arParams["LOGIN"] ?: $arParams["EMAIL"];
					CEventLog::Log('SECURITY', 'USER_INFO', 'main', $userInfo, GetMessage('DATA_NOT_FOUND1'));
				}
			}
		}
		return $result_message;
	}

	public function Register($USER_LOGIN, $USER_NAME, $USER_LAST_NAME, $USER_PASSWORD, $USER_CONFIRM_PASSWORD, $USER_EMAIL, $SITE_ID = false, $captcha_word = '', $captcha_sid = 0, $bSkipConfirm = false, $USER_PHONE_NUMBER = '')
	{
		/**
		 * @global CMain $APPLICATION
		 * @global CUserTypeManager $USER_FIELD_MANAGER
		 */
		global $APPLICATION, $DB, $USER_FIELD_MANAGER;

		$APPLICATION->ResetException();
		if (defined("ADMIN_SECTION") && ADMIN_SECTION === true && $SITE_ID !== false)
		{
			$APPLICATION->ThrowException(GetMessage("MAIN_FUNCTION_REGISTER_NA_INADMIN"));
			return ["MESSAGE" => GetMessage("MAIN_FUNCTION_REGISTER_NA_INADMIN"), "TYPE" => "ERROR"];
		}

		$strError = '';

		if (Option::get('main', 'captcha_registration', 'N') == 'Y')
		{
			if (!($APPLICATION->CaptchaCheckCode($captcha_word, $captcha_sid)))
			{
				$strError .= GetMessage("MAIN_FUNCTION_REGISTER_CAPTCHA") . "<br>";
			}
		}

		if ($strError)
		{
			if (Option::get('main', 'event_log_register_fail', 'N') === 'Y')
			{
				CEventLog::Log('SECURITY', 'USER_REGISTER_FAIL', 'main', false, $strError);
			}

			$APPLICATION->ThrowException($strError);
			return ["MESSAGE" => $strError, "TYPE" => "ERROR"];
		}

		if ($SITE_ID === false)
		{
			$SITE_ID = SITE_ID;
		}

		$bConfirmReq = !$bSkipConfirm && (Option::get('main', 'new_user_registration_email_confirmation', 'N') == 'Y' && Option::get('main', 'new_user_email_required', 'Y') != 'N');
		$phoneRegistration = (Option::get('main', 'new_user_phone_auth', 'N') == 'Y');
		$phoneRequired = ($phoneRegistration && Option::get('main', 'new_user_phone_required', 'N') == 'Y');

		$active = ($bConfirmReq || $phoneRequired ? 'N' : 'Y');

		$arFields = [
			"LOGIN" => $USER_LOGIN,
			"NAME" => $USER_NAME,
			"LAST_NAME" => $USER_LAST_NAME,
			"PASSWORD" => $USER_PASSWORD,
			"CHECKWORD" => Random::getString(32),
			"~CHECKWORD_TIME" => $DB->CurrentTimeFunction(),
			"CONFIRM_PASSWORD" => $USER_CONFIRM_PASSWORD,
			"EMAIL" => $USER_EMAIL,
			"PHONE_NUMBER" => $USER_PHONE_NUMBER,
			"ACTIVE" => $active,
			"CONFIRM_CODE" => ($bConfirmReq ? Random::getString(8, true) : ''),
			"SITE_ID" => $SITE_ID,
			"LANGUAGE_ID" => LANGUAGE_ID,
			"USER_IP" => $_SERVER["REMOTE_ADDR"],
			"USER_HOST" => @gethostbyaddr($_SERVER["REMOTE_ADDR"]),
		];
		$USER_FIELD_MANAGER->EditFormAddFields("USER", $arFields);

		$def_group = Option::get('main', 'new_user_registration_def_group');
		if ($def_group != '')
		{
			$arFields["GROUP_ID"] = explode(",", $def_group);
		}

		$bOk = true;
		$result_message = true;
		foreach (GetModuleEvents('main', 'OnBeforeUserRegister', true) as $arEvent)
		{
			if (ExecuteModuleEventEx($arEvent, [&$arFields]) === false)
			{
				if ($err = $APPLICATION->GetException())
				{
					$result_message = ["MESSAGE" => $err->GetString() . "<br>", "TYPE" => "ERROR"];
				}
				else
				{
					$APPLICATION->ThrowException("Unknown error");
					$result_message = ["MESSAGE" => "Unknown error" . "<br>", "TYPE" => "ERROR"];
				}

				$bOk = false;
				break;
			}
		}

		$ID = false;
		$phoneReg = false;
		if ($bOk)
		{
			if ($arFields["SITE_ID"] === false)
			{
				$arFields["SITE_ID"] = CSite::GetDefSite();
			}
			$arFields["LID"] = $arFields["SITE_ID"];

			if ($ID = $this->Add($arFields))
			{
				if ($phoneRegistration && $arFields["PHONE_NUMBER"] != '')
				{
					$phoneReg = true;

					//added the phone number for the user, now sending a confirmation SMS
					[$code, $phoneNumber] = static::GeneratePhoneCode($ID);

					$sms = new Main\Sms\Event(
						"SMS_USER_CONFIRM_NUMBER",
						[
							"USER_PHONE" => $phoneNumber,
							"CODE" => $code,
						]
					);
					$sms->setSite($arFields["SITE_ID"]);
					$smsResult = $sms->send(true);

					$signedData = Main\Controller\PhoneAuth::signData(['phoneNumber' => $phoneNumber]);

					if ($smsResult->isSuccess())
					{
						$result_message = [
							"MESSAGE" => GetMessage("main_register_sms_sent"),
							"TYPE" => "OK",
							"SIGNED_DATA" => $signedData,
							"ID" => $ID,
						];
					}
					else
					{
						$result_message = [
							"MESSAGE" => implode(' ', $smsResult->getErrorMessages()),
							"TYPE" => "ERROR",
							"SIGNED_DATA" => $signedData,
							"ID" => $ID,
						];
					}
				}
				else
				{
					$result_message = [
						"MESSAGE" => GetMessage("USER_REGISTER_OK"),
						"TYPE" => "OK",
						"ID" => $ID,
					];
				}

				$arFields["USER_ID"] = $ID;

				$arEventFields = $arFields;
				unset($arEventFields["PASSWORD"]);
				unset($arEventFields["CONFIRM_PASSWORD"]);
				unset($arEventFields["~CHECKWORD_TIME"]);

				$event = new CEvent;
				$event->SendImmediate("NEW_USER", $arEventFields["SITE_ID"], $arEventFields);
				if ($bConfirmReq)
				{
					$event->SendImmediate("NEW_USER_CONFIRM", $arEventFields["SITE_ID"], $arEventFields);
				}
			}
			else
			{
				$APPLICATION->ThrowException($this->LAST_ERROR);
				$result_message = ["MESSAGE" => $this->LAST_ERROR, "TYPE" => "ERROR"];
			}
		}

		if (is_array($result_message))
		{
			if ($result_message["TYPE"] == "OK")
			{
				if (Option::get('main', 'event_log_register', 'N') === 'Y')
				{
					$res_log["user"] = ($USER_NAME != '' || $USER_LAST_NAME != '') ? trim($USER_NAME . ' ' . $USER_LAST_NAME) : $USER_LOGIN;
					CEventLog::Log('SECURITY', 'USER_REGISTER', 'main', $ID, serialize($res_log));
				}
			}
			else
			{
				if (Option::get('main', 'event_log_register_fail', 'N') === 'Y')
				{
					CEventLog::Log('SECURITY', 'USER_REGISTER_FAIL', 'main', $ID, $result_message['MESSAGE']);
				}
			}
		}

		//authorize succesfully registered user, except email or phone confirmation is required
		$isAuthorize = false;
		if ($ID !== false && $arFields["ACTIVE"] === 'Y' && $phoneReg === false)
		{
			$isAuthorize = $this->Authorize($ID);
		}

		$agreementId = (int)Option::get('main', 'new_user_agreement');
		if ($agreementId && $isAuthorize)
		{
			$agreementObject = new Main\UserConsent\Agreement($agreementId);
			if ($agreementObject->isExist() && $agreementObject->isActive() && $_REQUEST["USER_AGREEMENT"] == 'Y')
			{
				Main\UserConsent\Consent::addByContext($agreementId, "main/reg", "register");
			}
		}

		$arFields["RESULT_MESSAGE"] = $result_message;
		foreach (GetModuleEvents('main', 'OnAfterUserRegister', true) as $arEvent)
		{
			ExecuteModuleEventEx($arEvent, [&$arFields]);
		}

		return $arFields["RESULT_MESSAGE"];
	}

	public function SimpleRegister($USER_EMAIL, $SITE_ID = false)
	{
		/** @global CMain $APPLICATION */
		global $APPLICATION, $DB;

		$APPLICATION->ResetException();
		if (defined("ADMIN_SECTION") && ADMIN_SECTION === true && $SITE_ID === false)
		{
			$APPLICATION->ThrowException(GetMessage("MAIN_FUNCTION_SIMPLEREGISTER_NA_INADMIN"));
			return ["MESSAGE" => GetMessage("MAIN_FUNCTION_SIMPLEREGISTER_NA_INADMIN"), "TYPE" => "ERROR"];
		}

		if ($SITE_ID === false)
		{
			$SITE_ID = SITE_ID;
		}

		global $REMOTE_ADDR;

		$arFields = [
			"CHECKWORD" => Random::getString(32),
			"~CHECKWORD_TIME" => $DB->CurrentTimeFunction(),
			"EMAIL" => $USER_EMAIL,
			"ACTIVE" => 'Y',
			"NAME" => '',
			"LAST_NAME" => '',
			"USER_IP" => $REMOTE_ADDR,
			"USER_HOST" => @gethostbyaddr($REMOTE_ADDR),
			"SITE_ID" => $SITE_ID,
			"LANGUAGE_ID" => LANGUAGE_ID,
		];

		$def_group = Option::get('main', 'new_user_registration_def_group');
		if ($def_group != '')
		{
			$arFields["GROUP_ID"] = explode(",", $def_group);
		}
		else
		{
			$arFields["GROUP_ID"] = [];
		}
		$arFields["PASSWORD"] = $arFields["CONFIRM_PASSWORD"] = static::GeneratePasswordByPolicy($arFields["GROUP_ID"]);

		$bOk = true;
		$result_message = false;
		foreach (GetModuleEvents('main', 'OnBeforeUserSimpleRegister', true) as $arEvent)
		{
			if (ExecuteModuleEventEx($arEvent, [&$arFields]) === false)
			{
				if ($err = $APPLICATION->GetException())
				{
					$result_message = ["MESSAGE" => $err->GetString() . "<br>", "TYPE" => "ERROR"];
				}
				else
				{
					$APPLICATION->ThrowException("Unknown error");
					$result_message = ["MESSAGE" => "Unknown error" . "<br>", "TYPE" => "ERROR"];
				}

				$bOk = false;
				break;
			}
		}

		$bRandLogin = false;
		if (!is_set($arFields, "LOGIN"))
		{
			$arFields["LOGIN"] = Random::getString(50);
			$bRandLogin = true;
		}

		$ID = 0;
		if ($bOk)
		{
			$arFields["LID"] = $arFields["SITE_ID"];

			if ($ID = $this->Add($arFields))
			{
				if ($bRandLogin)
				{
					$this->Update($ID, ["LOGIN" => "user" . $ID]);
					$arFields["LOGIN"] = "user" . $ID;
				}

				$this->Authorize($ID);

				$event = new CEvent;
				$arFields["USER_ID"] = $ID;

				$arEventFields = $arFields;
				unset($arEventFields["PASSWORD"]);
				unset($arEventFields["CONFIRM_PASSWORD"]);

				$event->SendImmediate("NEW_USER", $arEventFields["SITE_ID"], $arEventFields);
				static::SendUserInfo($ID, $arEventFields["SITE_ID"], GetMessage("USER_REGISTERED_SIMPLE"), true);
				$result_message = ["MESSAGE" => GetMessage("USER_REGISTER_OK"), "TYPE" => "OK"];
			}
			else
			{
				$result_message = ["MESSAGE" => $this->LAST_ERROR, "TYPE" => "ERROR"];
			}
		}

		if (is_array($result_message))
		{
			if ($result_message["TYPE"] == "OK")
			{
				if (Option::get('main', 'event_log_register', 'N') === 'Y')
				{
					$res_log["user"] = $arFields["LOGIN"];
					CEventLog::Log('SECURITY', 'USER_REGISTER', 'main', $ID, serialize($res_log));
				}
			}
			else
			{
				if (Option::get('main', 'event_log_register_fail', 'N') === 'Y')
				{
					CEventLog::Log('SECURITY', 'USER_REGISTER_FAIL', 'main', $ID, $result_message['MESSAGE']);
				}
			}
		}

		$arFields["RESULT_MESSAGE"] = $result_message;
		foreach (GetModuleEvents('main', 'OnAfterUserSimpleRegister', true) as $arEvent)
		{
			ExecuteModuleEventEx($arEvent, [&$arFields]);
		}

		return $arFields["RESULT_MESSAGE"];
	}

	public function IsAuthorized()
	{
		if (!isset($this))
		{
			trigger_error("Static call CUser::IsAuthorized() is deprecated, will be removed soon. Use global \$USER.", E_USER_WARNING);

			global $USER;
			return $USER->IsAuthorized();
		}
		return ($this->GetID() > 0);
	}

	public function HasNoAccess()
	{
		if (!$this->IsAuthorized())
		{
			return true;
		}

		$filePath = Main\Context::getCurrent()->getRequest()->getScriptFile();

		return !$this->CanDoFileOperation('fm_view_file', [SITE_ID, $filePath]);
	}

	public function IsJustAuthorized()
	{
		return $this->justAuthorized;
	}

	public function IsJustBecameOnline()
	{
		if (!$this->GetParam('PREV_LAST_ACTIVITY'))
		{
			return true;
		}
		else
		{
			return (($this->GetParam('SET_LAST_ACTIVITY') - $this->GetParam('PREV_LAST_ACTIVITY')) > Main\UserTable::getSecondsForLimitOnline());
		}
	}

	public function IsAdmin()
	{
		if ($this->admin === null)
		{
			if (
				Option::get('main', 'controller_member', 'N') == 'Y'
				&& Option::get('main', '~controller_limited_admin', 'N') == 'Y'
			)
			{
				$this->admin = ($this->GetParam("CONTROLLER_ADMIN") === true);
			}
			else
			{
				$this->admin = ($this->GetParam("ADMIN") === true);
			}
		}
		return $this->admin;
	}

	public function SetControllerAdmin($isAdmin = true)
	{
		$this->SetParam("CONTROLLER_ADMIN", (bool)$isAdmin);
	}

	/**
	 * @param array|true $deleteParms Parameters to delete; if true, delete all
	 * @return string
	 */
	public static function getLogoutParams($deleteParms = [])
	{
		$logout = 'logout=yes';

		if (Option::get('main', 'secure_logout', 'N') == 'Y')
		{
			$logout .= '&' . bitrix_sessid_get();
		}

		if ($deleteParms !== true)
		{
			if (($s = DeleteParam(array_merge($deleteParms, ["logout", "sessid"]))) != '')
			{
				$logout .= '&' . $s;
			}
		}

		return $logout;
	}

	public function Logout()
	{
		/** @global CMain $APPLICATION */
		global $APPLICATION;

		$USER_ID = $this->GetID();

		$arParams = [
			"USER_ID" => &$USER_ID,
		];

		$APPLICATION->ResetException();
		$bOk = true;
		foreach (GetModuleEvents('main', 'OnBeforeUserLogout', true) as $arEvent)
		{
			if (ExecuteModuleEventEx($arEvent, [&$arParams]) === false)
			{
				if (!($APPLICATION->GetException()))
				{
					$APPLICATION->ThrowException("Unknown logout error");
				}

				$bOk = false;
				break;
			}
		}

		if ($bOk)
		{
			foreach (GetModuleEvents('main', 'OnUserLogout', true) as $arEvent)
			{
				ExecuteModuleEventEx($arEvent, [$USER_ID]);
			}

			if (($storedAuthId = $this->getContext()->getStoredAuthId()) > 0)
			{
				UserStoredAuthTable::delete($storedAuthId);
			}

			$this->justAuthorized = false;
			$this->admin = null;
			$this->context = null;

			static::$kernelSession["SESS_AUTH"] = [];
			unset(static::$kernelSession["SESS_AUTH"]);
			unset(static::$kernelSession["SESS_OPERATIONS"]);
			unset(static::$kernelSession['fixed_session_id']);

			$application = Main\Application::getInstance();

			//change session id for security reason after logout
			$compositeSessionManager = $application->getCompositeSessionManager();
			//todo here was session_regenerate_id(true). Should we delete old?
			$compositeSessionManager->regenerateId();

			$response = Main\Context::getCurrent()->getResponse();
			$spread = (Option::get('main', 'auth_multisite', 'N') == 'Y' ? (Main\Web\Cookie::SPREAD_SITES | Main\Web\Cookie::SPREAD_DOMAIN) : Main\Web\Cookie::SPREAD_DOMAIN);

			$cookie = new Main\Web\Cookie("UIDH", '', 0);
			$cookie->setSpread($spread);
			$cookie->setHttpOnly(true);
			$response->addCookie($cookie);

			$cookie = new Main\Web\Cookie("UIDL", '', 0);
			$cookie->setSpread($spread);
			$cookie->setHttpOnly(true);
			$response->addCookie($cookie);

			Main\Composite\Engine::onUserLogout();
		}

		$arParams["SUCCESS"] = $bOk;
		foreach (GetModuleEvents('main', 'OnAfterUserLogout', true) as $arEvent)
		{
			ExecuteModuleEventEx($arEvent, [&$arParams]);
		}

		if (Option::get('main', 'event_log_logout', 'N') === 'Y')
		{
			CEventLog::Log('SECURITY', 'USER_LOGOUT', 'main', $USER_ID);
		}
	}

	public static function GetUserGroup($ID)
	{
		$ID = (int)$ID;
		if (!isset(self::$userGroupCache[$ID]))
		{
			$arr = [];
			$res = static::GetUserGroupEx($ID);
			while ($r = $res->Fetch())
			{
				$arr[] = $r["GROUP_ID"];
			}

			self::$userGroupCache[$ID] = $arr;
		}

		return self::$userGroupCache[$ID];
	}

	public static function GetUserGroupEx($ID)
	{
		global $DB;

		$strSql = "
			SELECT UG.GROUP_ID, G.STRING_ID,
				" . $DB->DateToCharFunction("UG.DATE_ACTIVE_FROM") . " as DATE_ACTIVE_FROM,
				" . $DB->DateToCharFunction("UG.DATE_ACTIVE_TO") . " as DATE_ACTIVE_TO
			FROM b_user_group UG INNER JOIN b_group G ON G.ID=UG.GROUP_ID
			WHERE UG.USER_ID = " . intval($ID) . "
				and ((UG.DATE_ACTIVE_FROM IS NULL) OR (UG.DATE_ACTIVE_FROM <= " . $DB->CurrentTimeFunction() . "))
				and ((UG.DATE_ACTIVE_TO IS NULL) OR (UG.DATE_ACTIVE_TO >= " . $DB->CurrentTimeFunction() . "))
				and G.ACTIVE = 'Y'
			UNION SELECT 2, 'everyone', NULL, NULL ";

		$res = $DB->Query($strSql);

		return $res;
	}

	public static function GetUserGroupList($ID)
	{
		global $DB;

		$strSql = "
			SELECT
				UG.GROUP_ID,
				" . $DB->DateToCharFunction("UG.DATE_ACTIVE_FROM") . " as DATE_ACTIVE_FROM,
				" . $DB->DateToCharFunction("UG.DATE_ACTIVE_TO") . " as DATE_ACTIVE_TO
			FROM
				b_user_group UG
			WHERE
				UG.USER_ID = " . intval($ID) . "
			UNION SELECT 2, NULL, NULL ";

		$res = $DB->Query($strSql);

		return $res;
	}

	public function CheckFields(&$arFields, $ID = false)
	{
		/**
		 * @global CMain $APPLICATION
		 * @global CUserTypeManager $USER_FIELD_MANAGER
		 */
		global $DB, $APPLICATION, $USER_FIELD_MANAGER;

		$this->LAST_ERROR = '';

		$bInternal = true;
		if (is_set($arFields, "EXTERNAL_AUTH_ID"))
		{
			if (trim($arFields["EXTERNAL_AUTH_ID"]) != '')
			{
				$bInternal = false;
			}
		}
		else
		{
			if ($ID > 0)
			{
				$dbr = $DB->Query("SELECT EXTERNAL_AUTH_ID FROM b_user WHERE ID=" . intval($ID));
				if (($ar = $dbr->Fetch()))
				{
					if ($ar['EXTERNAL_AUTH_ID'] != '')
					{
						$bInternal = false;
					}
				}
			}
		}

		if ($bInternal)
		{
			$this->LAST_ERROR .= static::CheckInternalFields($arFields, $ID);
		}
		else
		{
			if (is_set($arFields, "EMAIL"))
			{
				if ($arFields["EMAIL"] != '' && !check_email($arFields["EMAIL"], true))
				{
					$this->LAST_ERROR .= GetMessage("WRONG_EMAIL") . "<br>";
				}
			}
		}

		if (
			is_set($arFields, "PERSONAL_PHOTO")
			&& (!isset($arFields["PERSONAL_PHOTO"]["name"]) || $arFields["PERSONAL_PHOTO"]["name"] == '')
			&& (!isset($arFields["PERSONAL_PHOTO"]["del"]) || $arFields["PERSONAL_PHOTO"]["del"] == '')
		)
		{
			unset($arFields["PERSONAL_PHOTO"]);
		}

		$maxWidth = (int)Option::get('main', 'profile_image_width', 0);
		$maxHeight = (int)Option::get('main', 'profile_image_height', 0);
		$maxSize = (int)Option::get('main', 'profile_image_size', 0);

		if (is_set($arFields, "PERSONAL_PHOTO"))
		{
			$res = CFile::CheckImageFile($arFields["PERSONAL_PHOTO"], $maxSize, $maxWidth, $maxHeight);
			if ($res != '')
			{
				$this->LAST_ERROR .= $res . "<br>";
			}
		}

		if (is_set($arFields, "PERSONAL_BIRTHDAY") && $arFields["PERSONAL_BIRTHDAY"] != '' && !CheckDateTime($arFields["PERSONAL_BIRTHDAY"]))
		{
			$this->LAST_ERROR .= GetMessage("WRONG_PERSONAL_BIRTHDAY") . "<br>";
		}

		if (
			is_set($arFields, "WORK_LOGO")
			&& (!isset($arFields["WORK_LOGO"]["name"]) || $arFields["WORK_LOGO"]["name"] == '')
			&& (!isset($arFields["WORK_LOGO"]["del"]) || $arFields["WORK_LOGO"]["del"] == '')
		)
		{
			unset($arFields["WORK_LOGO"]);
		}

		if (is_set($arFields, "WORK_LOGO"))
		{
			$res = CFile::CheckImageFile($arFields["WORK_LOGO"], $maxSize, $maxWidth, $maxHeight);
			if ($res != '')
			{
				$this->LAST_ERROR .= $res . "<br>";
			}
		}

		if (is_set($arFields, 'LOGIN'))
		{
			$res = $DB->Query(
				"SELECT 'x' FROM b_user "
				. "WHERE LOGIN = '{$DB->ForSql($arFields["LOGIN"], 50)}' "
				. ($ID === false ? '' : ' AND ID <> ' . (int)$ID)
				. (
				!$bInternal
					? " AND EXTERNAL_AUTH_ID = '{$DB->ForSql($arFields["EXTERNAL_AUTH_ID"])}' "
					: " AND (EXTERNAL_AUTH_ID IS NULL OR {$DB->Length("EXTERNAL_AUTH_ID")} <= 0)"
				)
			);

			if ($res->Fetch())
			{
				$this->LAST_ERROR .= str_replace('#LOGIN#', htmlspecialcharsbx($arFields['LOGIN']), GetMessage('USER_EXIST')) . '<br>';
			}
		}

		if (is_object($APPLICATION))
		{
			$APPLICATION->ResetException();

			if ($ID === false)
			{
				$events = GetModuleEvents('main', 'OnBeforeUserAdd', true);
			}
			else
			{
				$arFields["ID"] = $ID;
				$events = GetModuleEvents('main', 'OnBeforeUserUpdate', true);
			}

			foreach ($events as $arEvent)
			{
				$bEventRes = ExecuteModuleEventEx($arEvent, [&$arFields]);
				if ($bEventRes === false)
				{
					if ($err = $APPLICATION->GetException())
					{
						$this->LAST_ERROR .= $err->GetString() . ' ';
					}
					else
					{
						$APPLICATION->ThrowException("Unknown error");
						$this->LAST_ERROR .= "Unknown error. ";
					}
					break;
				}
			}
		}

		if (is_object($APPLICATION))
		{
			$APPLICATION->ResetException();
		}
		if (!$USER_FIELD_MANAGER->CheckFields("USER", $ID, $arFields))
		{
			if (is_object($APPLICATION) && $APPLICATION->GetException())
			{
				$e = $APPLICATION->GetException();
				$this->LAST_ERROR .= $e->GetString();
				$APPLICATION->ResetException();
			}
			else
			{
				$this->LAST_ERROR .= "Unknown error. ";
			}
		}

		if ($this->LAST_ERROR != '')
		{
			return false;
		}

		return true;
	}

	/**
	 * @param array $arFields
	 * @param int|bool $ID
	 * @return string
	 */
	public static function CheckInternalFields($arFields, $ID = false)
	{
		global $DB;

		$resultError = '';

		$emailRequired = (Option::get('main', 'new_user_email_required', 'Y') != 'N');
		$phoneRequired = (Option::get('main', 'new_user_phone_required', 'N') == 'Y');

		if ($ID === false)
		{
			if (!isset($arFields["LOGIN"]))
			{
				$resultError .= GetMessage("user_login_not_set") . "<br>";
			}

			if (!isset($arFields["PASSWORD"]))
			{
				$resultError .= GetMessage("user_pass_not_set") . "<br>";
			}

			if ($emailRequired && !isset($arFields["EMAIL"]))
			{
				$resultError .= GetMessage("user_email_not_set") . "<br>";
			}

			if ($phoneRequired && !isset($arFields["PHONE_NUMBER"]))
			{
				$resultError .= GetMessage("main_user_check_no_phone") . "<br>";
			}
		}
		if (is_set($arFields, "LOGIN") && $arFields["LOGIN"] != trim($arFields["LOGIN"]))
		{
			$resultError .= GetMessage("LOGIN_WHITESPACE") . "<br>";
		}

		if (is_set($arFields, "LOGIN") && mb_strlen($arFields["LOGIN"]) < 3)
		{
			$resultError .= GetMessage("MIN_LOGIN") . "<br>";
		}

		if (is_set($arFields, "PASSWORD"))
		{
			if (is_set($arFields, "CONFIRM_PASSWORD") && $arFields["PASSWORD"] !== $arFields["CONFIRM_PASSWORD"])
			{
				//we shouldn't show that password is correct
				$resultError .= GetMessage("WRONG_CONFIRMATION") . "<br>";
			}
			else
			{
				if (array_key_exists("GROUP_ID", $arFields))
				{
					$arGroups = [];
					if (is_array($arFields["GROUP_ID"]))
					{
						foreach ($arFields["GROUP_ID"] as $arGroup)
						{
							if (is_array($arGroup))
							{
								$arGroups[] = $arGroup["GROUP_ID"];
							}
							else
							{
								$arGroups[] = $arGroup;
							}
						}
					}
					$policy = static::getPolicy($arGroups);
				}
				elseif ($ID !== false)
				{
					$policy = static::getPolicy($ID);
				}
				else
				{
					$policy = static::getPolicy([]);
				}

				$passwordErrors = static::CheckPasswordAgainstPolicy($arFields["PASSWORD"], $policy->getValues(), ($ID !== false ? $ID : null));
				if (!empty($passwordErrors))
				{
					$resultError .= implode("<br>", $passwordErrors) . "<br>";
				}
			}
		}

		if (is_set($arFields, "EMAIL"))
		{
			if (($emailRequired && mb_strlen($arFields["EMAIL"]) < 3) || ($arFields["EMAIL"] != '' && !check_email($arFields["EMAIL"], true)))
			{
				$resultError .= GetMessage("WRONG_EMAIL") . "<br>";
			}
			elseif (Option::get('main', 'new_user_email_uniq_check', 'N') === 'Y')
			{
				if ($arFields["EMAIL"] != '')
				{
					$oldEmail = '';
					if ($ID > 0)
					{
						// the option 'new_user_email_uniq_check' might have been switched on after the DB already contained identical emails,
						// so we let a user have the old email, but not the existing new one
						$dbr = $DB->Query("SELECT EMAIL FROM b_user WHERE ID=" . intval($ID));
						if (($ar = $dbr->Fetch()))
						{
							$oldEmail = $ar['EMAIL'];
						}
					}
					if (!$ID || $arFields["EMAIL"] != $oldEmail)
					{
						$res = static::GetList('', '',
							[
								"=EMAIL" => $arFields["EMAIL"],
								"EXTERNAL_AUTH_ID" => $arFields["EXTERNAL_AUTH_ID"] ?? null,
							],
							[
								"FIELDS" => ["ID"],
							]
						);
						while ($ar = $res->Fetch())
						{
							if (intval($ar["ID"]) !== intval($ID))
							{
								$resultError .= GetMessage("USER_WITH_EMAIL_EXIST", ["#EMAIL#" => htmlspecialcharsbx($arFields["EMAIL"])]) . "<br>";
							}
						}
					}
				}
			}
		}

		if (isset($arFields["PHONE_NUMBER"]))
		{
			if ($phoneRequired && $arFields["PHONE_NUMBER"] == '')
			{
				$resultError .= GetMessage("main_user_check_no_phone") . "<br>";
			}
			elseif ($arFields["PHONE_NUMBER"] != '')
			{
				//normalize the number: we need it normalized for validation
				$phoneNumber = Main\UserPhoneAuthTable::normalizePhoneNumber($arFields["PHONE_NUMBER"]);

				//validation
				$field = Main\UserPhoneAuthTable::getEntity()->getField("PHONE_NUMBER");
				$result = new Main\ORM\Data\Result();
				$primary = ($ID === false ? [] : ["USER_ID" => $ID]);
				$field->validateValue($phoneNumber, $primary, [], $result);
				if (!$result->isSuccess())
				{
					$resultError .= implode("<br>", $result->getErrorMessages());
				}
			}
		}

		if (!empty($arFields["GROUP_ID"]) && is_array($arFields["GROUP_ID"]))
		{
			if (!empty($arFields["GROUP_ID"][0]) && is_array($arFields["GROUP_ID"][0]))
			{
				foreach ($arFields["GROUP_ID"] as $arGroup)
				{
					if ($arGroup["DATE_ACTIVE_FROM"] != '' && !CheckDateTime($arGroup["DATE_ACTIVE_FROM"]))
					{
						$error = str_replace("#GROUP_ID#", $arGroup["GROUP_ID"], GetMessage("WRONG_DATE_ACTIVE_FROM"));
						$resultError .= $error . "<br>";
					}

					if ($arGroup["DATE_ACTIVE_TO"] != '' && !CheckDateTime($arGroup["DATE_ACTIVE_TO"]))
					{
						$error = str_replace("#GROUP_ID#", $arGroup["GROUP_ID"], GetMessage("WRONG_DATE_ACTIVE_TO"));
						$resultError .= $error . "<br>";
					}
				}
			}
		}

		return $resultError;
	}

	public static function GetByID($ID)
	{
		global $USER;

		$ID = intval($ID);

		if ($ID < 1)
		{
			//actually, here should be an exception
			$rs = new CDBResult;
			$rs->InitFromArray([]);
			return $rs;
		}

		$userID = (is_object($USER) ? intval($USER->GetID()) : 0);
		if ($userID > 0 && $ID == $userID && is_array(self::$CURRENT_USER))
		{
			$rs = new CDBResult;
			$rs->InitFromArray(self::$CURRENT_USER);
		}
		else
		{
			$rs = static::GetList('id', 'asc', ["ID_EQUAL_EXACT" => intval($ID)], ["SELECT" => ["UF_*"]]);
			if ($userID > 0 && $ID == $userID)
			{
				self::$CURRENT_USER = [$rs->Fetch()];
				$rs = new CDBResult;
				$rs->InitFromArray(self::$CURRENT_USER);
			}
		}
		return $rs;
	}

	public static function GetByLogin($LOGIN)
	{
		$rs = static::GetList('id', 'asc', ["LOGIN_EQUAL_EXACT" => $LOGIN], ["SELECT" => ["UF_*"]]);
		return $rs;
	}

	public function Update($ID, $arFields, $authActions = true)
	{
		/** @global CUserTypeManager $USER_FIELD_MANAGER */
		global $DB, $USER_FIELD_MANAGER, $CACHE_MANAGER, $USER;

		$ID = intval($ID);

		if ($ID <= 0)
		{
			return false;
		}

		if (!$this->CheckFields($arFields, $ID))
		{
			$result = false;
			$arFields["RESULT_MESSAGE"] = &$this->LAST_ERROR;
		}
		else
		{
			unset($arFields["ID"]);

			if (is_set($arFields, "ACTIVE") && $arFields["ACTIVE"] != 'Y')
			{
				$arFields["ACTIVE"] = 'N';
			}
			if (is_set($arFields, "BLOCKED") && $arFields["BLOCKED"] != 'Y')
			{
				$arFields["BLOCKED"] = 'N';
			}
			if (is_set($arFields, "PASSWORD_EXPIRED") && $arFields["PASSWORD_EXPIRED"] != 'Y')
			{
				$arFields["PASSWORD_EXPIRED"] = 'N';
			}

			if (is_set($arFields, "PERSONAL_GENDER") && ($arFields["PERSONAL_GENDER"] != "M" && $arFields["PERSONAL_GENDER"] != "F"))
			{
				$arFields["PERSONAL_GENDER"] = '';
			}

			$saveHistory = (Option::get('main', 'user_profile_history') === 'Y');

			//we need old values for some actions
			$arUser = null;
			if ((isset($arFields["ACTIVE"]) && $arFields["ACTIVE"] == 'N') || isset($arFields["PASSWORD"]) || $saveHistory)
			{
				$rUser = static::GetByID($ID);
				$arUser = $rUser->Fetch();
			}

			$originalPassword = '';
			$passwordChanged = false;
			if (is_set($arFields, "PASSWORD"))
			{
				$originalPassword = $arFields["PASSWORD"];
				$arFields["PASSWORD"] = Password::hash($arFields["PASSWORD"]);

				if ($arUser)
				{
					if (!Password::equals($arUser["PASSWORD"], $originalPassword))
					{
						//password changed, remove stored authentication
						UserStoredAuthTable::deleteByFilter(['=USER_ID' => $ID]);

						$passwordChanged = true;
					}
				}

				if (Option::get('main', 'event_log_password_change', 'N') === 'Y')
				{
					CEventLog::Log('SECURITY', 'USER_PASSWORD_CHANGED', 'main', $ID);
				}

				if (!isset($arFields['PASSWORD_EXPIRED']))
				{
					// reset the flag on password change
					$arFields['PASSWORD_EXPIRED'] = 'N';
				}
			}
			unset($arFields["STORED_HASH"]);

			$checkword = '';
			if (!is_set($arFields, "CHECKWORD"))
			{
				if (is_set($arFields, "PASSWORD") || is_set($arFields, "EMAIL") || is_set($arFields, "LOGIN") || is_set($arFields, "ACTIVE"))
				{
					$checkword = Random::getString(32);
					$arFields["CHECKWORD"] = Password::hash($checkword);
				}
			}
			else
			{
				$checkword = $arFields["CHECKWORD"];
				$arFields["CHECKWORD"] = Password::hash($checkword);
			}

			if (is_set($arFields, "CHECKWORD") && !is_set($arFields, "CHECKWORD_TIME"))
			{
				$arFields["~CHECKWORD_TIME"] = $DB->CurrentTimeFunction();
			}

			if (is_set($arFields, "WORK_COUNTRY"))
			{
				$arFields["WORK_COUNTRY"] = intval($arFields["WORK_COUNTRY"]);
			}

			if (is_set($arFields, "PERSONAL_COUNTRY"))
			{
				$arFields["PERSONAL_COUNTRY"] = intval($arFields["PERSONAL_COUNTRY"]);
			}

			if (
				array_key_exists("PERSONAL_PHOTO", $arFields)
				&& is_array($arFields["PERSONAL_PHOTO"])
				&& (
					!array_key_exists("MODULE_ID", $arFields["PERSONAL_PHOTO"])
					|| $arFields["PERSONAL_PHOTO"]["MODULE_ID"] == ''
				)
			)
			{
				$arFields["PERSONAL_PHOTO"]["MODULE_ID"] = 'main';
			}

			CFile::SaveForDB($arFields, 'PERSONAL_PHOTO', 'main');

			if (
				array_key_exists("WORK_LOGO", $arFields)
				&& is_array($arFields["WORK_LOGO"])
				&& (
					!array_key_exists("MODULE_ID", $arFields["WORK_LOGO"])
					|| $arFields["WORK_LOGO"]["MODULE_ID"] == ''
				)
			)
			{
				$arFields["WORK_LOGO"]["MODULE_ID"] = 'main';
			}

			CFile::SaveForDB($arFields, 'WORK_LOGO', 'main');

			$strUpdate = $DB->PrepareUpdate("b_user", $arFields);

			if (!is_set($arFields, "TIMESTAMP_X"))
			{
				$strUpdate .= ($strUpdate != '' ? ',' : '') . " TIMESTAMP_X = " . $DB->GetNowFunction();
			}

			$strSql = "UPDATE b_user SET " . $strUpdate . " WHERE ID=" . $ID;

			$DB->Query($strSql);

			$USER_FIELD_MANAGER->Update("USER", $ID, $arFields);

			if (isset($arFields["PHONE_NUMBER"]))
			{
				$numberExists = false;
				if ($arFields["PHONE_NUMBER"] != '')
				{
					$arFields["PHONE_NUMBER"] = Main\UserPhoneAuthTable::normalizePhoneNumber($arFields["PHONE_NUMBER"]);

					$numberExists = Main\UserPhoneAuthTable::getList(["filter" => [
						"=USER_ID" => $ID,
						"=PHONE_NUMBER" => $arFields["PHONE_NUMBER"],
					]])->fetch();
				}
				if ($arFields["PHONE_NUMBER"] == '' || !$numberExists)
				{
					//number changed or added
					Main\UserPhoneAuthTable::delete($ID);

					if ($arFields["PHONE_NUMBER"] != '')
					{
						Main\UserPhoneAuthTable::add([
							"USER_ID" => $ID,
							"PHONE_NUMBER" => $arFields["PHONE_NUMBER"],
						]);
					}
				}
			}

			if (Option::get('main', 'event_log_user_edit', 'N') === 'Y')
			{
				$res_log["user"] = ($arFields["NAME"] != '' || $arFields["LAST_NAME"] != '') ? trim($arFields["NAME"] . ' ' . $arFields["LAST_NAME"]) : $arFields["LOGIN"];
				CEventLog::Log('SECURITY', 'USER_EDIT', 'main', $ID, serialize($res_log));
			}

			if (is_set($arFields, "GROUP_ID"))
			{
				static::SetUserGroup($ID, $arFields["GROUP_ID"]);
			}

			if ($arUser && $passwordChanged)
			{
				if (Option::get('main', 'use_digest_auth', 'N') == 'Y')
				{
					//update digest hash for http digest authorization
					static::UpdateDigest($arUser["ID"], $originalPassword);
				}

				//history of passwords
				UserPasswordTable::add([
					"USER_ID" => $arUser["ID"],
					"PASSWORD" => $arFields["PASSWORD"],
					"DATE_CHANGE" => new Main\Type\DateTime(),
				]);
			}

			if ($arUser && $authActions)
			{
				$authAction = false;
				if (isset($arFields["ACTIVE"]) && $arUser["ACTIVE"] == 'Y' && $arFields["ACTIVE"] == 'N')
				{
					$authAction = true;
				}

				$internalUser = true;
				if (isset($arFields["EXTERNAL_AUTH_ID"]))
				{
					if ($arFields["EXTERNAL_AUTH_ID"] != '')
					{
						$internalUser = false;
					}
				}
				elseif ($arUser["EXTERNAL_AUTH_ID"] != '')
				{
					$internalUser = false;
				}

				if ($internalUser && isset($arFields["PASSWORD"]) && $passwordChanged)
				{
					$authAction = true;
					if (is_object($USER) && $USER->GetID() == $ID)
					{
						//changed password by himself
						$USER->SetParam("AUTH_ACTION_SKIP_LOGOUT", true);
					}
				}

				if ($authAction)
				{
					Main\UserAuthActionTable::addLogoutAction($ID);
				}
			}

			$result = true;
			$arFields["CHECKWORD"] = $checkword;

			//update session information and cache for current user
			if (is_object($USER) && $USER->GetID() == $ID)
			{
				static $arSessFields = [
					'LOGIN' => 'LOGIN',
					'EMAIL' => 'EMAIL',
					'TITLE' => 'TITLE',
					'FIRST_NAME' => 'NAME',
					'SECOND_NAME' => 'SECOND_NAME',
					'LAST_NAME' => 'LAST_NAME',
					'PERSONAL_PHOTO' => 'PERSONAL_PHOTO',
					'PERSONAL_GENDER' => 'PERSONAL_GENDER',
					'AUTO_TIME_ZONE' => 'AUTO_TIME_ZONE',
					'TIME_ZONE' => 'TIME_ZONE',
				];
				foreach ($arSessFields as $key => $val)
				{
					if (isset($arFields[$val]))
					{
						$USER->SetParam($key, $arFields[$val]);
					}
				}
				$name = $USER->GetParam("FIRST_NAME");
				$last_name = $USER->GetParam("LAST_NAME");
				$USER->SetParam("NAME", $name . ($name == '' || $last_name == '' ? '' : ' ') . $last_name);

				//cache for GetByID()
				self::$CURRENT_USER = false;
			}

			if ($saveHistory && $arUser)
			{
				$rUser = static::GetByID($ID);
				$newUser = $rUser->Fetch();

				UserProfileHistoryTable::addHistory($ID, UserProfileHistoryTable::TYPE_UPDATE, $arUser, $newUser);
			}
		}

		$arFields["ID"] = $ID;
		$arFields["RESULT"] = &$result;

		foreach (GetModuleEvents('main', 'OnAfterUserUpdate', true) as $arEvent)
		{
			ExecuteModuleEventEx($arEvent, [&$arFields]);
		}

		if ($arFields["RESULT"])
		{
			if (Main\UserTable::shouldReindex($arFields))
			{
				Main\UserTable::indexRecord($ID);
			}

			if (defined("BX_COMP_MANAGED_CACHE"))
			{
				$userData = Main\UserTable::getById($ID)->fetch();
				$isRealUser = !$userData['EXTERNAL_AUTH_ID'] || !in_array($userData['EXTERNAL_AUTH_ID'], Main\UserTable::getExternalUserTypes());

				$CACHE_MANAGER->ClearByTag("USER_CARD_" . intval($ID / TAGGED_user_card_size));
				$CACHE_MANAGER->ClearByTag($isRealUser ? "USER_CARD" : "EXTERNAL_USER_CARD");

				static $arNameFields = [
					"NAME", "LAST_NAME", "SECOND_NAME",
					"ACTIVE",
					"LOGIN", "EMAIL",
					"PERSONAL_GENDER", "PERSONAL_PHOTO", "WORK_POSITION", "PERSONAL_PROFESSION", "PERSONAL_WWW", "PERSONAL_BIRTHDAY", "TITLE",
					"EXTERNAL_AUTH_ID", "UF_DEPARTMENT",
					"AUTO_TIME_ZONE", "TIME_ZONE", "TIME_ZONE_OFFSET",
				];
				$bClear = false;
				foreach ($arNameFields as $val)
				{
					if (isset($arFields[$val]))
					{
						$bClear = true;
						break;
					}
				}
				if ($bClear)
				{
					$CACHE_MANAGER->ClearByTag("USER_NAME_" . $ID);
					$CACHE_MANAGER->ClearByTag($isRealUser ? "USER_NAME" : "EXTERNAL_USER_NAME");
				}
			}
		}

		return $result;
	}

	public static function SetUserGroup($USER_ID, $arGroups, $newUser = false)
	{
		$connection = Main\Application::getConnection();

		$USER_ID = intval($USER_ID);

		if ($USER_ID === 0)
		{
			return false;
		}

		//remember previous groups of the user
		$aPrevGroups = [];
		$res = static::GetUserGroupList($USER_ID);
		while ($res_arr = $res->Fetch())
		{
			if ($res_arr["GROUP_ID"] != 2)
			{
				$aPrevGroups[$res_arr["GROUP_ID"]] = $res_arr;
			}
		}

		$inserted = [];
		$values = [];
		if (is_array($arGroups))
		{
			foreach ($arGroups as $group)
			{
				if (!is_array($group))
				{
					$group = ["GROUP_ID" => $group];
				}
				//we must preserve fields order for the insertion sql
				$groupFields = [
					"GROUP_ID" => $group["GROUP_ID"],
					"DATE_ACTIVE_FROM" => ($group["DATE_ACTIVE_FROM"] ?? ''),
					"DATE_ACTIVE_TO" => ($group["DATE_ACTIVE_TO"] ?? ''),
				];

				$group_id = intval($groupFields["GROUP_ID"]);
				if ($group_id > 0 && $group_id != 2 && !isset($inserted[$group_id]))
				{
					$arInsert = $GLOBALS['DB']->PrepareInsert("b_user_group", $groupFields);
					$values[] = "(" . $USER_ID . ",	" . $arInsert[1] . ")";
					$inserted[$group_id] = $groupFields;
				}
			}
		}

		$connection->startTransaction();

		$connection->query("DELETE FROM b_user_group WHERE USER_ID=" . $USER_ID);

		if (!empty($values))
		{
			$strSql = $connection->getSqlHelper()->getInsertIgnore("b_user_group", "(USER_ID, GROUP_ID, DATE_ACTIVE_FROM, DATE_ACTIVE_TO)", "VALUES " . implode(", ", $values));
			$connection->query($strSql);
		}

		$connection->commitTransaction();

		static::clearUserGroupCache($USER_ID);

		foreach (GetModuleEvents('main', 'OnAfterSetUserGroup', true) as $arEvent)
		{
			ExecuteModuleEventEx($arEvent, [$USER_ID, $inserted]);
		}

		if ($aPrevGroups != $inserted)
		{
			if (!$newUser)
			{
				$authActionCommon = false;
				$now = new Main\Type\DateTime();
				foreach ($inserted as $group)
				{
					foreach (["DATE_ACTIVE_FROM", "DATE_ACTIVE_TO"] as $field)
					{
						if ($group[$field] != '')
						{
							$date = Main\Type\DateTime::createFromUserTime($group[$field]);
							if ($date->getTimestamp() > $now->getTimestamp())
							{
								//group membership is in the future, we need separate records for each group
								Main\UserAuthActionTable::addUpdateAction($USER_ID, $date);
							}
							else
							{
								$authActionCommon = true;
							}
						}
						else
						{
							$authActionCommon = true;
						}
					}
				}

				if ($authActionCommon)
				{
					//one action for all groups without dates in the future
					Main\UserAuthActionTable::addUpdateAction($USER_ID);
				}
			}

			if (Option::get('main', 'event_log_user_groups', 'N') === 'Y')
			{
				$UserName = '';
				$rsUser = static::GetByID($USER_ID);
				if ($arUser = $rsUser->GetNext())
				{
					$UserName = ($arUser["NAME"] != '' || $arUser["LAST_NAME"] != '') ? trim($arUser["NAME"] . ' ' . $arUser["LAST_NAME"]) : $arUser["LOGIN"];
				}
				$res_log = [
					"groups" => serialize($aPrevGroups) . " => " . serialize($inserted),
					"user" => $UserName,
				];
				CEventLog::Log('SECURITY', 'USER_GROUP_CHANGED', 'main', $USER_ID, serialize($res_log));
			}
		}
		return null;
	}

	/**
	 * Appends groups to the list of existing user's groups.
	 *
	 * @param int $user_id
	 * @param array|int $groups A single number, or an array of numbers, or an array of arrays("GROUP_ID"=>$val, "DATE_ACTIVE_FROM"=>$val, "DATE_ACTIVE_TO"=>$val)
	 */
	public static function AppendUserGroup($user_id, $groups)
	{
		$arGroups = [];
		$res = static::GetUserGroupList($user_id);
		while ($res_arr = $res->Fetch())
		{
			if ($res_arr["GROUP_ID"] != 2)
			{
				$arGroups[(int)$res_arr["GROUP_ID"]] = [
					"GROUP_ID" => $res_arr["GROUP_ID"],
					"DATE_ACTIVE_FROM" => $res_arr["DATE_ACTIVE_FROM"],
					"DATE_ACTIVE_TO" => $res_arr["DATE_ACTIVE_TO"],
				];
			}
		}

		if (!is_array($groups))
		{
			$groups = [$groups];
		}

		$setGroups = false;
		foreach ($groups as $group)
		{
			if (!is_array($group))
			{
				$group = ["GROUP_ID" => $group];
			}
			$groupId = (int)$group["GROUP_ID"];
			if ($groupId != 2)
			{
				if (!isset($arGroups[$groupId]))
				{
					$arGroups[$groupId] = $group;
					$setGroups = true;
				}
			}
		}

		if ($setGroups)
		{
			static::SetUserGroup($user_id, $arGroups);
		}
	}

	public static function GetCount()
	{
		global $DB;
		$r = $DB->Query("SELECT COUNT('x') as C FROM b_user");
		$r = $r->Fetch();
		return intval($r["C"]);
	}

	public static function Delete($ID)
	{
		global $DB, $APPLICATION, $USER_FIELD_MANAGER, $CACHE_MANAGER;

		$ID = intval($ID);

		$rsUser = $DB->Query("
			SELECT ID, LOGIN, NAME, LAST_NAME, EXTERNAL_AUTH_ID, PERSONAL_PHOTO, WORK_LOGO 
			FROM b_user 
			WHERE ID = {$ID} 
				AND ID <> 1
		");
		$arUser = $rsUser->Fetch();
		if (!$arUser)
		{
			return false;
		}

		$events = array_merge(GetModuleEvents('main', 'OnBeforeUserDelete', true), GetModuleEvents('main', 'OnUserDelete', true));

		foreach ($events as $arEvent)
		{
			if (ExecuteModuleEventEx($arEvent, [$ID]) === false)
			{
				$err = GetMessage("MAIN_BEFORE_DEL_ERR1") . ' ' . ($arEvent['TO_MODULE_ID'] ?? '');
				if ($ex = $APPLICATION->GetException())
				{
					$err .= ': ' . $ex->GetString();
				}
				$APPLICATION->throwException($err);
				if (Option::get('main', 'event_log_user_delete', 'N') === 'Y')
				{
					$UserName = ($arUser["NAME"] != '' || $arUser["LAST_NAME"] != '') ? trim($arUser["NAME"] . ' ' . $arUser["LAST_NAME"]) : $arUser["LOGIN"];
					$res_log = [
						"user" => $UserName,
						"err" => $err,
					];
					CEventLog::Log('SECURITY', 'USER_DELETE', 'main', $ID, serialize($res_log));
				}
				return false;
			}
		}

		if ($arUser['PERSONAL_PHOTO'] > 0)
		{
			CFile::Delete($arUser['PERSONAL_PHOTO']);
		}
		if ($arUser['WORK_LOGO'] > 0)
		{
			CFile::Delete($arUser['WORK_LOGO']);
		}

		CAccess::OnUserDelete($ID);

		$DB->Query("DELETE FROM b_user_group WHERE USER_ID=" . $ID);

		$DB->Query("DELETE FROM b_user_digest WHERE USER_ID=" . $ID);

		$userFilter = ['=USER_ID' => $ID];

		ApplicationPasswordTable::deleteByFilter($userFilter);

		Main\UserPhoneAuthTable::delete($ID);

		ShortCode::deleteByUser($ID);

		CHotKeys::GetInstance()->DeleteByUser($ID);

		UserPasswordTable::deleteByFilter($userFilter);

		UserStoredAuthTable::deleteByFilter($userFilter);

		UserHitAuthTable::deleteByFilter($userFilter);

		UserDeviceTable::deleteByFilter($userFilter);

		$USER_FIELD_MANAGER->Delete("USER", $ID);

		if (Option::get('main', 'event_log_user_delete', 'N') === 'Y')
		{
			$res_log["user"] = ($arUser["NAME"] != '' || $arUser["LAST_NAME"] != '') ? trim($arUser["NAME"] . ' ' . $arUser["LAST_NAME"]) : $arUser["LOGIN"];
			CEventLog::Log('SECURITY', 'USER_DELETE', 'main', $arUser['LOGIN'], serialize($res_log));
		}

		if (!$DB->Query("DELETE FROM b_user WHERE ID=" . $ID . " AND ID<>1"))
		{
			return false;
		}

		if (defined("BX_COMP_MANAGED_CACHE"))
		{
			$isRealUser = !$arUser['EXTERNAL_AUTH_ID'] || !in_array($arUser['EXTERNAL_AUTH_ID'], Main\UserTable::getExternalUserTypes());

			$CACHE_MANAGER->ClearByTag("USER_CARD_" . intval($ID / TAGGED_user_card_size));
			$CACHE_MANAGER->ClearByTag($isRealUser ? "USER_CARD" : "EXTERNAL_USER_CARD");

			$CACHE_MANAGER->ClearByTag("USER_NAME_" . $ID);
			$CACHE_MANAGER->ClearByTag($isRealUser ? "USER_NAME" : "EXTERNAL_USER_CARD");
		}

		static::clearUserGroupCache($ID);

		Main\UserAuthActionTable::addLogoutAction($ID);

		UserProfileHistoryTable::deleteByFilter($userFilter);

		if (Option::get('main', 'user_profile_history') === 'Y')
		{
			UserProfileHistoryTable::addHistory($ID, UserProfileHistoryTable::TYPE_DELETE);
		}

		Main\UserTable::deleteIndexRecord($ID);

		foreach (GetModuleEvents('main', 'OnAfterUserDelete', true) as $arEvent)
		{
			ExecuteModuleEventEx($arEvent, [$ID]);
		}

		return true;
	}

	public static function GetExternalAuthList()
	{
		$arAll = [];
		foreach (GetModuleEvents('main', 'OnExternalAuthList', true) as $arEvent)
		{
			$arRes = ExecuteModuleEventEx($arEvent);
			if (is_array($arRes))
			{
				foreach ($arRes as $v)
				{
					$arAll[] = $v;
				}
			}
		}

		$result = new CDBResult;
		$result->InitFromArray($arAll);
		return $result;
	}

	public static function GetGroupPolicy($iUserId)
	{
		$policy = static::getPolicy($iUserId);

		$arPolicy = $policy->getValues();

		$ar = [
			GetMessage("MAIN_GP_PASSWORD_LENGTH", ["#LENGTH#" => (int)$arPolicy["PASSWORD_LENGTH"]]),
		];
		if ($arPolicy["PASSWORD_UPPERCASE"] === 'Y')
		{
			$ar[] = GetMessage("MAIN_GP_PASSWORD_UPPERCASE");
		}
		if ($arPolicy["PASSWORD_LOWERCASE"] === 'Y')
		{
			$ar[] = GetMessage("MAIN_GP_PASSWORD_LOWERCASE");
		}
		if ($arPolicy["PASSWORD_DIGITS"] === 'Y')
		{
			$ar[] = GetMessage("MAIN_GP_PASSWORD_DIGITS");
		}
		if ($arPolicy["PASSWORD_PUNCTUATION"] === 'Y')
		{
			$ar[] = GetMessage("MAIN_GP_PASSWORD_PUNCTUATION", ["#SPECIAL_CHARS#" => static::PASSWORD_SPECIAL_CHARS]);
		}
		$arPolicy["PASSWORD_REQUIREMENTS"] = implode(", ", $ar) . ".";

		return $arPolicy;
	}

	/**
	 * @param mixed $userId User ID or array of groups
	 * @return Policy\RulesCollection
	 */
	public static function getPolicy($userId)
	{
		global $DB, $CACHE_MANAGER;

		static $cache = [];

		$cacheId = md5(serialize($userId));
		if (isset($cache[$cacheId]))
		{
			return $cache[$cacheId];
		}

		$arPolicies = [];

		$res = GroupTable::getList([
			'select' => ['GROUP_ID' => 'ID', 'SECURITY_POLICY'],
			'filter' => ['=ID' => 2],
			'cache' => ['ttl' => 86400],
		]);
		$group2Policy = $res->Fetch();

		if ($group2Policy)
		{
			$arPolicies[] = $group2Policy;
		}

		if (is_array($userId))
		{
			$arGroups = [];
			foreach ($userId as $value)
			{
				$value = intval($value);
				if ($value > 0 && $value != 2)
				{
					$arGroups[$value] = $value;
				}
			}
			if ($arGroups)
			{
				$sql =
					"SELECT G.ID GROUP_ID, G.SECURITY_POLICY " .
					"FROM b_group G " .
					"WHERE G.ID in (" . implode(", ", $arGroups) . ")";
				$rs = $DB->Query($sql);
				while ($ar = $rs->Fetch())
				{
					$arPolicies[] = $ar;
				}
			}
		}
		elseif (intval($userId) > 0)
		{
			$sql =
				"SELECT UG.GROUP_ID, G.SECURITY_POLICY " .
				"FROM b_user_group UG, b_group G " .
				"WHERE UG.USER_ID = " . intval($userId) . ' ' .
				"AND UG.GROUP_ID = G.ID " .
				"AND ((UG.DATE_ACTIVE_FROM IS NULL) OR (UG.DATE_ACTIVE_FROM <= " . $DB->CurrentTimeFunction() . ")) " .
				"AND ((UG.DATE_ACTIVE_TO IS NULL) OR (UG.DATE_ACTIVE_TO >= " . $DB->CurrentTimeFunction() . ")) ";
			$rs = $DB->Query($sql);
			while ($ar = $rs->Fetch())
			{
				$arPolicies[] = $ar;
			}
		}

		// default policy
		$policy = new Policy\RulesCollection();

		foreach ($arPolicies as $ar)
		{
			if ($ar["SECURITY_POLICY"])
			{
				$arGroupPolicy = unserialize($ar["SECURITY_POLICY"], ['allowed_classes' => false]);
			}
			else
			{
				continue;
			}

			if (!is_array($arGroupPolicy))
			{
				continue;
			}

			foreach ($arGroupPolicy as $key => $val)
			{
				$rule = $policy[$key];
				if ($rule !== null)
				{
					if ($rule->assignValue($val))
					{
						// now we know from which group the rule was last applied
						$rule->setGroupId((int)$ar['GROUP_ID']);
					}
				}
			}
		}

		if (count($cache) <= 10)
		{
			$cache[$cacheId] = $policy;
		}

		return $policy;
	}

	public static function CheckStoredHash($context, $hash, $tempHash = false)
	{
		if (!($context instanceof Authentication\Context))
		{
			$context = (new Authentication\Context())
				->setUserId($context)
			;
		}

		$cnt = 0;
		$hashId = false;

		$res = UserStoredAuthTable::query()
			->setSelect(['*'])
			->where('USER_ID', $context->getUserId())
			->setOrder(['LAST_AUTH' => 'DESC'])
			->exec()
		;

		$policy = static::getPolicy($context->getUserId());

		$maxStoreNum = $policy->getMaxStoreNum();
		$storeTimeout = $policy->getStoreTimeout();
		$sessionTimeout = $policy->getSessionTimeout();
		$storeIpMask = ip2long($policy->getStoreIpMask());

		$ipAddress = Main\Context::getCurrent()->getServer()->getRemoteAddr();

		while ($ar = $res->fetch())
		{
			if ($ar["TEMP_HASH"] == 'N')
			{
				$cnt++;
			}

			$lastAuthTime = 0;
			if ($ar["LAST_AUTH"] instanceof Main\Type\DateTime)
			{
				$lastAuthTime = $ar["LAST_AUTH"]->getTimestamp();
			}

			if (
				$cnt > $maxStoreNum
				|| ($ar["TEMP_HASH"] == 'N' && time() - ($storeTimeout * 60) > $lastAuthTime)
				|| ($ar["TEMP_HASH"] == 'Y' && time() - ($sessionTimeout * 60) > $lastAuthTime)
			)
			{
				UserStoredAuthTable::delete($ar['ID']);
			}
			elseif (!$hashId)
			{
				//for domain spreaded external auth we should check only temporary hashes
				if (!$tempHash || $ar["TEMP_HASH"] == 'Y')
				{
					$remote_net = $storeIpMask & ip2long($ipAddress);
					$stored_net = $storeIpMask & (float)$ar["IP_ADDR"];

					if ($hash === $ar["STORED_HASH"] && $remote_net == $stored_net)
					{
						$hashId = $ar["ID"];

						$context
							->setStoredAuthId($hashId)
							->setStoredAuthHash($hash)
						;
					}
				}
			}
		}
		return $hashId;
	}

	public function GetAllOperations($arGroups = false)
	{
		global $DB;

		if (is_array($arGroups))
		{
			$userGroups = "2," . implode(",", array_map("intval", $arGroups));
		}
		else
		{
			$userGroups = $this->GetGroups();
		}

		$sql_str = "
			SELECT O.NAME OPERATION_NAME
			FROM b_group_task GT
				INNER JOIN b_task_operation T_O ON T_O.TASK_ID=GT.TASK_ID
				INNER JOIN b_operation O ON O.ID=T_O.OPERATION_ID
			WHERE GT.GROUP_ID IN(" . $userGroups . ")
			UNION
			SELECT O.NAME OPERATION_NAME
			FROM b_option OP
				INNER JOIN b_task_operation T_O ON T_O.TASK_ID=" . $DB->ToNumber("OP.VALUE") . "
				INNER JOIN b_operation O ON O.ID=T_O.OPERATION_ID
			WHERE OP.NAME='GROUP_DEFAULT_TASK'
			UNION
			SELECT O.NAME OPERATION_NAME
			FROM b_option OP
				INNER JOIN b_task T ON T.MODULE_ID=OP.MODULE_ID AND T.BINDING='module' AND T.LETTER=" . $DB->ToChar("OP.VALUE", 1) . " AND T.SYS='Y'
				INNER JOIN b_task_operation T_O ON T_O.TASK_ID=T.ID
				INNER JOIN b_operation O ON O.ID=T_O.OPERATION_ID
			WHERE OP.NAME='GROUP_DEFAULT_RIGHT'
		";

		$z = $DB->Query($sql_str);
		$arr = [];
		while ($r = $z->Fetch())
		{
			$arr[$r['OPERATION_NAME']] = $r['OPERATION_NAME'];
		}

		return $arr;
	}

	public function CanDoOperation($op_name, $user_id = 0)
	{
		if ($user_id > 0)
		{
			$arGroups = [];
			$rsGroups = $this->GetUserGroupEx($user_id);
			while ($group = $rsGroups->Fetch())
			{
				$arGroups[] = $group["GROUP_ID"];
			}
			if (!$arGroups)
			{
				return false;
			}

			$op = $this->GetAllOperations($arGroups);
			return isset($op[$op_name]);
		}
		else
		{
			if ($this->IsAdmin())
			{
				return true;
			}

			if (!isset(static::$kernelSession["SESS_OPERATIONS"]))
			{
				static::$kernelSession["SESS_OPERATIONS"] = $this->GetAllOperations();
			}

			return isset(static::$kernelSession["SESS_OPERATIONS"][$op_name]);
		}
	}

	public static function GetFileOperations($arPath, $arGroups = false)
	{
		global $APPLICATION;

		$permissions = $APPLICATION->GetFileAccessPermission($arPath, $arGroups, true);

		$arFileOperations = [];
		foreach ($permissions as $taskId)
		{
			$arFileOperations = array_merge($arFileOperations, CTask::GetOperations($taskId, true));
		}
		$arFileOperations = array_values(array_unique($arFileOperations));

		return $arFileOperations;
	}

	public function CanDoFileOperation($op_name, $arPath)
	{
		global $USER;

		if ($this->IsAdmin())
		{
			return true;
		}

		static $arAlowedOperations = ['fm_delete_file', 'fm_rename_folder', 'fm_view_permission'];

		if (mb_substr($arPath[1], -10) == "/.htaccess" && !$USER->CanDoOperation('edit_php') && !in_array($op_name, $arAlowedOperations))
		{
			return false;
		}
		if (mb_substr($arPath[1], -12) == "/.access.php")
		{
			return false;
		}

		static $fileOperations = [];

		$key = $arPath[0] . '|' . $arPath[1];
		if (!isset($fileOperations[$key]))
		{
			$fileOperations[$key] = $this->GetFileOperations($arPath);
		}

		return in_array($op_name, $fileOperations[$key]);
	}

	public static function UserTypeRightsCheck($entity_id)
	{
		global $USER;

		if ($entity_id == "USER" && $USER->CanDoOperation('edit_other_settings'))
		{
			return "W";
		}
		return "D";
	}

	public function CanAccess($arCodes)
	{
		if (!is_array($arCodes) || empty($arCodes))
		{
			return false;
		}

		if (in_array('G2', $arCodes))
		{
			return true;
		}

		if ($this->IsAuthorized() && in_array('AU', $arCodes))
		{
			return true;
		}

		$bEmpty = true;
		foreach ($arCodes as $code)
		{
			if (trim($code) != '')
			{
				$bEmpty = false;
				break;
			}
		}

		if ($bEmpty)
		{
			return false;
		}

		$res = CAccess::GetUserCodes($this->GetID(), ["ACCESS_CODE" => $arCodes]);
		if ($res->Fetch())
		{
			return true;
		}

		return false;
	}

	public function GetAccessCodes()
	{
		if (!$this->IsAuthorized())
		{
			return ['G2'];
		}

		$arCodes = CAccess::GetUserCodesArray($this->GetID());

		if ($this->IsAuthorized())
		{
			$arCodes[] = 'AU';
		}

		return $arCodes;
	}

	public static function CleanUpAgent()
	{
		$cleanup_days = (int)Option::get('main', 'new_user_registration_cleanup_days', 7);
		if ($cleanup_days > 0)
		{
			$date = new Main\Type\Date();
			$date->add("-{$cleanup_days}D");

			if (Option::get('main', 'new_user_registration_email_confirmation', 'N') === 'Y')
			{
				//unconfirmed email confirmations
				$filter = [
					"!CONFIRM_CODE" => false,
					"=ACTIVE" => 'N',
					"<DATE_REGISTER" => $date,
				];
				$users = Main\UserTable::getList([
					"filter" => $filter,
					"select" => ["ID"],
				]);
				while ($user = $users->fetch())
				{
					static::Delete($user["ID"]);
				}
			}

			if (Option::get('main', 'new_user_phone_auth', 'N') === 'Y')
			{
				//unconfirmed phone confirmations
				$filter = [
					'=\Bitrix\Main\UserPhoneAuthTable:USER.CONFIRMED' => 'N',
					"=ACTIVE" => 'N',
					"<DATE_REGISTER" => $date,
				];
				$users = Main\UserTable::getList([
					"filter" => $filter,
					"select" => ["ID"],
				]);
				while ($user = $users->fetch())
				{
					static::Delete($user["ID"]);
				}
			}
		}

		$historyCleanupDays = (int)Option::get('main', 'profile_history_cleanup_days', 0);
		if ($historyCleanupDays > 0)
		{
			$date = new Main\Type\Date();
			$date->add("-{$historyCleanupDays}D");
			UserProfileHistoryTable::deleteByFilter(["<DATE_INSERT" => $date]);
		}

		$deviceCleanupDays = (int)Option::get('main', 'device_history_cleanup_days', 180);
		if ($deviceCleanupDays > 0)
		{
			$date = new Main\Type\Date();
			$date->add("-{$deviceCleanupDays}D");
			UserDeviceLoginTable::deleteByFilter(["<LOGIN_DATE" => $date]);
		}

		return "CUser::CleanUpAgent();";
	}

	public static function DeactivateAgent()
	{
		$blockDays = (int)Option::get('main', 'inactive_users_block_days', 0);
		if ($blockDays > 0)
		{
			$log = (Option::get('main', 'event_log_block_user', 'N') === 'Y');

			$userObj = new CUser();

			$date = new Main\Type\Date();
			$date->add("-{$blockDays}D");

			$filter = [
				"=ACTIVE" => 'Y',
				"=BLOCKED" => 'N',
				"<LAST_LOGIN" => $date,
			];
			$users = Main\UserTable::getList([
				"filter" => $filter,
				"select" => ["ID"],
			]);
			while ($user = $users->fetch())
			{
				$groups = static::GetUserGroup($user["ID"]);
				$admin = in_array(1, $groups);

				//admins shouldn't be blocked
				if (!$admin)
				{
					$userObj->Update($user["ID"], ["BLOCKED" => 'Y'], false);

					if ($log)
					{
						CEventLog::Log('SECURITY', 'USER_BLOCKED', 'main', $user['ID'], "Inactive days: {$blockDays}");
					}
				}
			}
		}
		return "CUser::DeactivateAgent();";
	}

	public static function UnblockAgent($userId)
	{
		$user = new CUser();
		$user->Update($userId, ["BLOCKED" => 'N']);

		return '';
	}

	/**
	 * @deprecated
	 */
	public static function GetActiveUsersCount()
	{
		return Main\Application::getInstance()->getLicense()->getActiveUsersCount();
	}

	public static function SetLastActivityDate($userId = null, $cache = false)
	{
		global $USER;

		if (is_null($userId))
		{
			$userId = $USER->GetId();
		}

		$userId = intval($userId);
		if ($userId <= 0)
		{
			return false;
		}

		if ($userId == $USER->GetId())
		{
			if ($cache)
			{
				if (intval($USER->GetParam('SET_LAST_ACTIVITY')) + 60 > time())
				{
					return false;
				}
			}

			$USER->SetParam('PREV_LAST_ACTIVITY', $USER->GetParam('SET_LAST_ACTIVITY'));
			$USER->SetParam('SET_LAST_ACTIVITY', time());
		}

		static::SetLastActivityDateByArray([$userId], $_SERVER['REMOTE_ADDR']);

		return true;
	}

	public static function SetLastActivityDateByArray($arUsers, $ip = null)
	{
		global $DB;

		if (!is_array($arUsers) || empty($arUsers))
		{
			return false;
		}

		$strSqlPrefix = "UPDATE b_user SET " .
			"TIMESTAMP_X = TIMESTAMP_X, " .
			"LAST_ACTIVITY_DATE = " . $DB->CurrentTimeFunction() . " WHERE ID IN (";
		$strSqlPostfix = ")";
		$maxValuesLen = 2048;
		$strSqlValues = '';

		$arUsers = array_map("intval", $arUsers);
		foreach ($arUsers as $userId)
		{
			$strSqlValues .= ",$userId";
			if (mb_strlen($strSqlValues) > $maxValuesLen)
			{
				$DB->Query($strSqlPrefix . mb_substr($strSqlValues, 1) . $strSqlPostfix, false, '', ["ignore_dml" => true]);
				$strSqlValues = '';
			}
		}

		if ($strSqlValues != '')
		{
			$DB->Query($strSqlPrefix . mb_substr($strSqlValues, 1) . $strSqlPostfix, false, '', ["ignore_dml" => true]);
		}

		$event = new Main\Event('main', 'OnUserSetLastActivityDate', [$arUsers, $ip]);
		$event->send();

		return true;
	}

	public static function GetSecondsForLimitOnline()
	{
		return Main\UserTable::getSecondsForLimitOnline();
	}

	public static function GetExternalUserTypes()
	{
		return Main\UserTable::getExternalUserTypes();
	}

	public static function GetOnlineStatus($userId, $lastseen, $now = false)
	{
		$userId = intval($userId);

		if ($lastseen instanceof Main\Type\DateTime)
		{
			$lastseen = $lastseen->getTimestamp();
		}
		else
		{
			if (is_int($lastseen))
			{
				$lastseen = intval($lastseen);
			}
			else
			{
				$lastseen = 0;
			}
		}

		if ($now === false)
		{
			$now = time();
		}
		else
		{
			if ($now instanceof Main\Type\DateTime)
			{
				$now = $now->getTimestamp();
			}
			else
			{
				$now = intval($now);
			}
		}

		$result = [
			'IS_ONLINE' => false,
			'STATUS' => self::STATUS_OFFLINE,
			'STATUS_TEXT' => GetMessage('USER_STATUS_OFFLINE'),
			'LAST_SEEN' => $lastseen,
			'LAST_SEEN_TEXT' => '',
			'NOW' => $now,
		];

		if ($lastseen === false)
		{
			return $result;
		}

		$result['IS_ONLINE'] = $now - $lastseen <= static::GetSecondsForLimitOnline();
		$result['STATUS'] = $result['IS_ONLINE'] ? self::STATUS_ONLINE : self::STATUS_OFFLINE;
		$result['STATUS_TEXT'] = GetMessage('USER_STATUS_' . strtoupper($result['STATUS']));

		if ($lastseen && $now - $lastseen > 300)
		{
			$result['LAST_SEEN_TEXT'] = static::FormatLastActivityDate($lastseen, $now);
		}

		if ($userId > 0)
		{
			if ($result['IS_ONLINE'])
			{
				foreach (GetModuleEvents('main', 'OnUserOnlineStatusGetCustomOnlineStatus', true) as $arEvent)
				{
					$customStatus = ExecuteModuleEventEx($arEvent, [$userId, $lastseen, $now, self::STATUS_ONLINE]);
					if (is_array($customStatus))
					{
						if (!empty($customStatus['STATUS']) && !empty($customStatus['STATUS_TEXT']))
						{
							$result['STATUS'] = strtolower($customStatus['STATUS']);
							$result['STATUS_TEXT'] = $customStatus['STATUS_TEXT'];
						}
						if (isset($customStatus['LAST_SEEN']) && intval($customStatus['LAST_SEEN']) > 0)
						{
							$result['LAST_SEEN'] = intval($customStatus['LAST_SEEN']);
						}
						if (isset($customStatus['LAST_SEEN_TEXT']))
						{
							$result['LAST_SEEN_TEXT'] = $customStatus['LAST_SEEN_TEXT'];
						}
					}
				}
			}
			else
			{
				foreach (GetModuleEvents('main', 'OnUserOnlineStatusGetCustomOfflineStatus', true) as $arEvent)
				{
					$customStatus = ExecuteModuleEventEx($arEvent, [$userId, $lastseen, $now, self::STATUS_OFFLINE]);
					if (is_array($customStatus))
					{
						if (!empty($customStatus['STATUS']) && !empty($customStatus['STATUS_TEXT']))
						{
							$result['STATUS'] = strtolower($customStatus['STATUS']);
							$result['STATUS_TEXT'] = $customStatus['STATUS_TEXT'];
						}
						if (isset($customStatus['LAST_SEEN']) && intval($customStatus['LAST_SEEN']) > 0)
						{
							$result['LAST_SEEN'] = intval($customStatus['LAST_SEEN']);
						}
						if (isset($customStatus['LAST_SEEN_TEXT']))
						{
							$result['LAST_SEEN_TEXT'] = $customStatus['LAST_SEEN_TEXT'];
						}
					}
				}
			}
		}

		return $result;
	}

	/**
	 * @param int|bool|Main\Type\DateTime $timestamp
	 * @param int|bool|Main\Type\DateTime $now
	 *
	 * @return string
	 */
	public static function FormatLastActivityDate($timestamp, $now = false)
	{
		global $DB;

		if ($timestamp instanceof Main\Type\DateTime)
		{
			$timestamp = $timestamp->getTimestamp();
		}
		else
		{
			if (is_int($timestamp))
			{
				$timestamp = intval($timestamp);
			}
			else
			{
				return '';
			}
		}

		if ($now === false)
		{
			$now = time();
		}
		else
		{
			if ($now instanceof Main\Type\DateTime)
			{
				$now = $now->getTimestamp();
			}
			else
			{
				$now = intval($now);
			}
		}

		$ampm = IsAmPmMode(true);
		$timeFormat = ($ampm === AM_PM_LOWER ? "g:i a" : ($ampm === AM_PM_UPPER ? "g:i A" : "H:i"));

		$formattedDate = FormatDate([
			"tomorrow" => "#01#{$timeFormat}",
			"now" => "#02#",
			"todayFuture" => "#03#{$timeFormat}",
			"yesterday" => "#04#{$timeFormat}",
			"-" => preg_replace('/:s$/', '', $DB->DateFormatToPHP(CSite::GetDateFormat())),
			"s60" => "sago",
			"i60" => "iago",
			"H5" => "Hago",
			"H24" => "#03#{$timeFormat}",
			"d31" => "dago",
			"m12>1" => "mago",
			"m12>0" => "dago",
			'' => "#05#",
		], $timestamp, $now);

		if (preg_match('/^#(\d+)#(.*)/', $formattedDate, $match))
		{
			switch ($match[1])
			{
				case "01":
					$formattedDate = str_replace("#TIME#", $match[2], GetMessage('USER_LAST_SEEN_TOMORROW'));
					break;
				case "02":
					$formattedDate = GetMessage('USER_LAST_SEEN_NOW');
					break;
				case "03":
					$formattedDate = str_replace("#TIME#", $match[2], GetMessage('USER_LAST_SEEN_TODAY'));
					break;
				case "04":
					$formattedDate = str_replace("#TIME#", $match[2], GetMessage('USER_LAST_SEEN_YESTERDAY'));
					break;
				case "05":
					$formattedDate = GetMessage('USER_LAST_SEEN_MORE_YEAR');
					break;
				default:
					$formattedDate = $match[2];
					break;
			}
		}

		return $formattedDate;
	}

	public static function SearchUserByName($arName, $email = '', $bLoginMode = false)
	{
		global $DB;

		$arNameReady = [];
		foreach ($arName as $s)
		{
			$s = trim($s);
			if ($s != '')
			{
				$arNameReady[] = $s;
			}
		}

		if (empty($arNameReady))
		{
			return false;
		}

		$strSqlWhereEMail = (($email != '') ? " AND upper(U.EMAIL) = upper('" . $DB->ForSql($email) . "') " : '');

		if ($bLoginMode)
		{
			if (count($arNameReady) > 3)
			{
				$strSql =
					"SELECT U.ID, U.NAME, U.LAST_NAME, U.SECOND_NAME, U.LOGIN, U.EMAIL " .
					"FROM b_user U " .
					"WHERE (";
				$bFirst = true;
				for ($i = 0; $i < 4; $i++)
				{
					for ($j = 0; $j < 4; $j++)
					{
						if ($i == $j)
						{
							continue;
						}

						for ($k = 0; $k < 4; $k++)
						{
							if ($i == $k || $j == $k)
							{
								continue;
							}

							for ($l = 0; $l < 4; $l++)
							{
								if ($i == $l || $j == $l || $k == $l)
								{
									continue;
								}

								if (!$bFirst)
								{
									$strSql .= " OR ";
								}

								$strSql .= "(U.NAME IS NOT NULL AND upper(U.NAME) LIKE upper('" . $DB->ForSql($arNameReady[$i]) . "%') " .
									"AND U.LAST_NAME IS NOT NULL AND upper(U.LAST_NAME) LIKE upper('" . $DB->ForSql($arNameReady[$j]) . "%') " .
									"AND U.LOGIN IS NOT NULL AND upper(U.LOGIN) LIKE upper('" . $DB->ForSql($arNameReady[$k]) . "%') " .
									"AND U.EMAIL IS NOT NULL AND upper(U.EMAIL) LIKE upper('" . $DB->ForSql($arNameReady[$l]) . "%'))";

								$bFirst = false;
							}
						}
					}
				}
				$strSql .= ")";
			}
			elseif (Count($arNameReady) == 3)
			{
				$strSql =
					"SELECT U.ID, U.NAME, U.LAST_NAME, U.SECOND_NAME, U.LOGIN, U.EMAIL " .
					"FROM b_user U " .
					"WHERE (";
				$bFirst = true;
				for ($i = 0; $i < 3; $i++)
				{
					for ($j = 0; $j < 3; $j++)
					{
						if ($i == $j)
						{
							continue;
						}

						for ($k = 0; $k < 3; $k++)
						{
							if ($i == $k || $j == $k)
							{
								continue;
							}

							if (!$bFirst)
							{
								$strSql .= " OR ";
							}

							$strSql .= "(";
							$strSql .= "(U.NAME IS NOT NULL AND upper(U.NAME) LIKE upper('" . $DB->ForSql($arNameReady[$i]) . "%') " .
								"AND U.LAST_NAME IS NOT NULL AND upper(U.LAST_NAME) LIKE upper('" . $DB->ForSql($arNameReady[$j]) . "%') " .
								"AND U.LOGIN IS NOT NULL AND upper(U.LOGIN) LIKE upper('" . $DB->ForSql($arNameReady[$k]) . "%'))";
							$strSql .= " OR ";
							$strSql .= "(U.NAME IS NOT NULL AND upper(U.NAME) LIKE upper('" . $DB->ForSql($arNameReady[$i]) . "%') " .
								"AND U.LAST_NAME IS NOT NULL AND upper(U.LAST_NAME) LIKE upper('" . $DB->ForSql($arNameReady[$j]) . "%') " .
								"AND U.EMAIL IS NOT NULL AND upper(U.EMAIL) LIKE upper('" . $DB->ForSql($arNameReady[$k]) . "%'))";
							$strSql .= " OR ";
							$strSql .= "(U.NAME IS NOT NULL AND upper(U.NAME) LIKE upper('" . $DB->ForSql($arNameReady[$i]) . "%') " .
								"AND U.LOGIN IS NOT NULL AND upper(U.LOGIN) LIKE upper('" . $DB->ForSql($arNameReady[$j]) . "%') " .
								"AND U.EMAIL IS NOT NULL AND upper(U.EMAIL) LIKE upper('" . $DB->ForSql($arNameReady[$k]) . "%'))";
							$strSql .= " OR ";
							$strSql .= "(U.LAST_NAME IS NOT NULL AND upper(U.LAST_NAME) LIKE upper('" . $DB->ForSql($arNameReady[$i]) . "%') " .
								"AND U.LOGIN IS NOT NULL AND upper(U.LOGIN) LIKE upper('" . $DB->ForSql($arNameReady[$j]) . "%') " .
								"AND U.EMAIL IS NOT NULL AND upper(U.EMAIL) LIKE upper('" . $DB->ForSql($arNameReady[$k]) . "%'))";
							$strSql .= ")";

							$bFirst = false;
						}
					}
				}
				$strSql .= ")";
			}
			elseif (Count($arNameReady) == 2)
			{
				$strSql =
					"SELECT U.ID, U.NAME, U.LAST_NAME, U.SECOND_NAME, U.LOGIN, U.EMAIL " .
					"FROM b_user U " .
					"WHERE (";
				$bFirst = true;
				for ($i = 0; $i < 2; $i++)
				{
					for ($j = 0; $j < 2; $j++)
					{
						if ($i == $j)
						{
							continue;
						}

						if (!$bFirst)
						{
							$strSql .= " OR ";
						}

						$strSql .= "(";
						$strSql .= "(U.NAME IS NOT NULL AND upper(U.NAME) LIKE upper('" . $DB->ForSql($arNameReady[$i]) . "%') " .
							"AND U.LAST_NAME IS NOT NULL AND upper(U.LAST_NAME) LIKE upper('" . $DB->ForSql($arNameReady[$j]) . "%'))";
						$strSql .= " OR ";
						$strSql .= "(U.NAME IS NOT NULL AND upper(U.NAME) LIKE upper('" . $DB->ForSql($arNameReady[$i]) . "%') " .
							"AND U.LOGIN IS NOT NULL AND upper(U.LOGIN) LIKE upper('" . $DB->ForSql($arNameReady[$j]) . "%'))";
						$strSql .= " OR ";
						$strSql .= "(U.LAST_NAME IS NOT NULL AND upper(U.LAST_NAME) LIKE upper('" . $DB->ForSql($arNameReady[$i]) . "%') " .
							"AND U.LOGIN IS NOT NULL AND upper(U.LOGIN) LIKE upper('" . $DB->ForSql($arNameReady[$j]) . "%'))";
						$strSql .= " OR ";
						$strSql .= "(U.LAST_NAME IS NOT NULL AND upper(U.LAST_NAME) LIKE upper('" . $DB->ForSql($arNameReady[$i]) . "%') " .
							"AND U.EMAIL IS NOT NULL AND upper(U.EMAIL) LIKE upper('" . $DB->ForSql($arNameReady[$j]) . "%'))";
						$strSql .= " OR ";
						$strSql .= "(U.NAME IS NOT NULL AND upper(U.NAME) LIKE upper('" . $DB->ForSql($arNameReady[$i]) . "%') " .
							"AND U.EMAIL IS NOT NULL AND upper(U.EMAIL) LIKE upper('" . $DB->ForSql($arNameReady[$j]) . "%'))";
						$strSql .= " OR ";
						$strSql .= "(U.LOGIN IS NOT NULL AND upper(U.LOGIN) LIKE upper('" . $DB->ForSql($arNameReady[$i]) . "%') " .
							"AND U.EMAIL IS NOT NULL AND upper(U.EMAIL) LIKE upper('" . $DB->ForSql($arNameReady[$j]) . "%'))";
						$strSql .= ")";
						$bFirst = false;
					}
				}
				$strSql .= ")";
			}
			else
			{
				$strSql =
					"SELECT U.ID, U.NAME, U.LAST_NAME, U.SECOND_NAME, U.LOGIN, U.EMAIL " .
					"FROM b_user U " .
					"WHERE (U.LAST_NAME IS NOT NULL AND upper(U.LAST_NAME) LIKE upper('" . $DB->ForSql($arNameReady[0]) . "%') " .
					"	OR U.LOGIN IS NOT NULL AND upper(U.LOGIN) LIKE upper('" . $DB->ForSql($arNameReady[0]) . "%') " .
					"	OR U.EMAIL IS NOT NULL AND upper(U.EMAIL) LIKE upper('" . $DB->ForSql($arNameReady[0]) . "%') " .
					"	OR U.NAME IS NOT NULL AND upper(U.NAME) LIKE upper('" . $DB->ForSql($arNameReady[0]) . "%')) ";
			}
			$strSql .= $strSqlWhereEMail;
		}
		else
		{
			if (Count($arNameReady) >= 3)
			{
				$strSql =
					"SELECT U.ID, U.NAME, U.LAST_NAME, U.SECOND_NAME, U.LOGIN, U.EMAIL " .
					"FROM b_user U " .
					"WHERE ";
				$bFirst = true;
				for ($i = 0; $i < 3; $i++)
				{
					for ($j = 0; $j < 3; $j++)
					{
						if ($i == $j)
						{
							continue;
						}

						for ($k = 0; $k < 3; $k++)
						{
							if ($i == $k || $j == $k)
							{
								continue;
							}

							if (!$bFirst)
							{
								$strSql .= " OR ";
							}

							$strSql .= "(U.NAME IS NOT NULL AND upper(U.NAME) LIKE upper('" . $DB->ForSql($arNameReady[$i]) . "%') " .
								"AND U.LAST_NAME IS NOT NULL AND upper(U.LAST_NAME) LIKE upper('" . $DB->ForSql($arNameReady[$j]) . "%') " .
								"AND U.SECOND_NAME IS NOT NULL AND upper(U.SECOND_NAME) LIKE upper('" . $DB->ForSql($arNameReady[$k]) . "%')" . $strSqlWhereEMail . ")";

							$bFirst = false;
						}
					}
				}
			}
			elseif (Count($arNameReady) == 2)
			{
				$strSql =
					"SELECT U.ID, U.NAME, U.LAST_NAME, U.SECOND_NAME, U.LOGIN, U.EMAIL " .
					"FROM b_user U " .
					"WHERE ";
				$bFirst = true;
				for ($i = 0; $i < 2; $i++)
				{
					for ($j = 0; $j < 2; $j++)
					{
						if ($i == $j)
						{
							continue;
						}

						if (!$bFirst)
						{
							$strSql .= " OR ";
						}

						$strSql .= "(U.NAME IS NOT NULL AND upper(U.NAME) LIKE upper('" . $DB->ForSql($arNameReady[$i]) . "%') " .
							"AND U.LAST_NAME IS NOT NULL AND upper(U.LAST_NAME) LIKE upper('" . $DB->ForSql($arNameReady[$j]) . "%')" . $strSqlWhereEMail . ")";

						$bFirst = false;
					}
				}
			}
			else
			{
				$strSql =
					"SELECT U.ID, U.NAME, U.LAST_NAME, U.SECOND_NAME, U.LOGIN, U.EMAIL " .
					"FROM b_user U " .
					"WHERE U.LAST_NAME IS NOT NULL AND upper(U.LAST_NAME) LIKE upper('" . $DB->ForSql($arNameReady[0]) . "%') " .
					$strSqlWhereEMail;
			}
		}

		$dbRes = $DB->Query($strSql);
		return $dbRes;
	}

	public static function FormatName($NAME_TEMPLATE, $arUser, $bUseLogin = false, $bHTMLSpec = true, $enabledEmptyNameStub = true)
	{
		if (isset($arUser["ID"]))
		{
			$ID = intval($arUser['ID']);
		}
		else
		{
			$ID = '';
		}

		$NAME_SHORT = (($arUser['NAME'] ?? '') != '' ? mb_substr($arUser['NAME'], 0, 1) . '.' : '');
		$LAST_NAME_SHORT = (($arUser['LAST_NAME'] ?? '') != '' ? mb_substr($arUser['LAST_NAME'], 0, 1) . '.' : '');
		$SECOND_NAME_SHORT = (($arUser['SECOND_NAME'] ?? '') != '' ? mb_substr($arUser['SECOND_NAME'], 0, 1) . '.' : '');

		$res = str_replace(
			['#TITLE#', '#NAME#', '#LAST_NAME#', '#SECOND_NAME#', '#NAME_SHORT#', '#LAST_NAME_SHORT#', '#SECOND_NAME_SHORT#', '#EMAIL#', '#ID#'],
			[($arUser['TITLE'] ?? ''), ($arUser['NAME'] ?? ''), ($arUser['LAST_NAME'] ?? ''), ($arUser['SECOND_NAME'] ?? ''), $NAME_SHORT, $LAST_NAME_SHORT, $SECOND_NAME_SHORT, ($arUser['EMAIL'] ?? ''), $ID],
			$NAME_TEMPLATE
		);

		while (str_contains($res, '  '))
		{
			$res = str_replace('  ', ' ', $res);
		}
		$res = trim($res);

		$res_check = '';
		if (str_contains($NAME_TEMPLATE, '#NAME#') || str_contains($NAME_TEMPLATE, '#NAME_SHORT#'))
		{
			$res_check .= $arUser['NAME'] ?? '';
		}
		if (str_contains($NAME_TEMPLATE, '#LAST_NAME#') || str_contains($NAME_TEMPLATE, '#LAST_NAME_SHORT#'))
		{
			$res_check .= $arUser['LAST_NAME'] ?? '';
		}
		if (str_contains($NAME_TEMPLATE, '#SECOND_NAME#') || str_contains($NAME_TEMPLATE, '#SECOND_NAME_SHORT#'))
		{
			$res_check .= $arUser['SECOND_NAME'] ?? '';
		}

		if (trim($res_check) == '')
		{
			if ($bUseLogin && !empty($arUser['LOGIN']))
			{
				$res = $arUser['LOGIN'];
			}
			elseif ($enabledEmptyNameStub)
			{
				$res = GetMessage('FORMATNAME_NONAME');
			}
			else
			{
				$res = '';
			}

			if (str_contains($NAME_TEMPLATE, '[#ID#]'))
			{
				$res .= " [" . $ID . "]";
			}
		}

		if ($bHTMLSpec)
		{
			$res = htmlspecialcharsbx($res);
		}

		$res = str_replace(['#NOBR#', '#/NOBR#'], '', $res);

		return $res;
	}

	public static function clearUserGroupCache($ID = false)
	{
		if ($ID === false)
		{
			self::$userGroupCache = [];
		}
		else
		{
			$ID = (int)$ID;
			if (isset(self::$userGroupCache[$ID]))
			{
				unset(self::$userGroupCache[$ID]);
			}
		}
	}

	public function CheckAuthActions()
	{
		if (!$this->IsAuthorized())
		{
			return;
		}

		if (!is_array(static::$kernelSession["AUTH_ACTIONS_PERFORMED"]))
		{
			static::$kernelSession["AUTH_ACTIONS_PERFORMED"] = [];
		}

		$now = new Main\Type\DateTime();

		$actions = Main\UserAuthActionTable::getList([
			"filter" => ["=USER_ID" => $this->getContext()->getUserId()],
			"order" => ["USER_ID" => "ASC", "PRIORITY" => "ASC", "ID" => "DESC"],
			"cache" => ["ttl" => 3600],
		]);

		while ($action = $actions->fetch())
		{
			if (isset(static::$kernelSession["AUTH_ACTIONS_PERFORMED"][$action["ID"]]))
			{
				//already processed the action in this session
				continue;
			}

			if ($action["APPLICATION_ID"] != '' && $this->getContext()->getApplicationId() != $action["APPLICATION_ID"])
			{
				//this action is for the specific application only
				continue;
			}

			/** @var Main\Type\DateTime $actionDate */
			$actionDate = $action["ACTION_DATE"];

			if ($actionDate <= $now)
			{
				//remember that we already did the action
				static::$kernelSession["AUTH_ACTIONS_PERFORMED"][$action["ID"]] = true;

				if ($this->IsJustAuthorized())
				{
					//no need to update the session
					continue;
				}

				switch ($action["ACTION"])
				{
					case Main\UserAuthActionTable::ACTION_LOGOUT:
						if ($this->GetParam("AUTH_ACTION_SKIP_LOGOUT"))
						{
							//user's changed password by himself, skip logout
							$this->SetParam("AUTH_ACTION_SKIP_LOGOUT", false);
							break;
						}
						//redirect is possible
						$this->Logout();
						break;

					case Main\UserAuthActionTable::ACTION_UPDATE:
						$this->UpdateSessionData($this->getContext());
						break;
				}

				//we need to process only the first action by proirity
				break;
			}
		}
	}

	public static function AuthActionsCleanUpAgent()
	{
		$date = new Main\Type\DateTime();
		$date->add("-1D");
		Main\UserAuthActionTable::deleteByFilter(["<ACTION_DATE" => $date]);
		return 'CUser::AuthActionsCleanUpAgent();';
	}

	/**
	 * @param int $userId
	 * @return array|bool [code, phone_number]
	 */
	public static function GeneratePhoneCode($userId)
	{
		$row = Main\UserPhoneAuthTable::getRowById($userId);
		if ($row && $row["OTP_SECRET"] != '')
		{
			$totp = new Main\Security\Mfa\TotpAlgorithm();
			$totp->setInterval(self::PHONE_CODE_OTP_INTERVAL);
			$totp->setSecret($row["OTP_SECRET"]);

			$timecode = $totp->timecode(time());
			$code = $totp->generateOTP($timecode);

			Main\UserPhoneAuthTable::update($userId, [
				"ATTEMPTS" => 0,
				"DATE_SENT" => new Main\Type\DateTime(),
			]);

			return [$code, $row["PHONE_NUMBER"]];
		}
		return false;
	}

	/**
	 * @param string $phoneNumber
	 * @param string $code
	 * @return bool|int User ID on success, false on error
	 */
	public static function VerifyPhoneCode($phoneNumber, $code)
	{
		if ($code == '')
		{
			return false;
		}

		$phoneNumber = Main\UserPhoneAuthTable::normalizePhoneNumber($phoneNumber);

		$row = Main\UserPhoneAuthTable::getList(["filter" => ["=PHONE_NUMBER" => $phoneNumber]])->fetch();
		if ($row && $row["OTP_SECRET"] != '')
		{
			if ($row["ATTEMPTS"] >= 3)
			{
				return false;
			}

			$totp = new Main\Security\Mfa\TotpAlgorithm();
			$totp->setInterval(self::PHONE_CODE_OTP_INTERVAL);
			$totp->setSecret($row["OTP_SECRET"]);

			try
			{
				[$result,] = $totp->verify($code);
			}
			catch (Main\ArgumentException)
			{
				return false;
			}

			$data = [];
			if ($result)
			{
				if ($row["CONFIRMED"] == 'N')
				{
					$data["CONFIRMED"] = 'Y';
				}

				$data['DATE_SENT'] = '';
			}
			else
			{
				$data["ATTEMPTS"] = (int)$row["ATTEMPTS"] + 1;
			}

			if (!empty($data))
			{
				Main\UserPhoneAuthTable::update($row["USER_ID"], $data);
			}

			if ($result)
			{
				return $row["USER_ID"];
			}
		}
		return false;
	}

	/**
	 * @param string $phoneNumber
	 * @param string $smsTemplate
	 * @param string|null $siteId
	 * @return Main\Result
	 */
	public static function SendPhoneCode($phoneNumber, $smsTemplate, $siteId = null)
	{
		$result = new Main\Result();

		$phoneNumber = Main\UserPhoneAuthTable::normalizePhoneNumber($phoneNumber);

		$select = ["USER_ID", "DATE_SENT", "USER.LANGUAGE_ID"];

		if ($siteId === null)
		{
			$context = Main\Context::getCurrent();
			$siteId = $context->getSite();

			if ($siteId === null)
			{
				$select[] = "USER.LID";
			}
		}

		$userPhone = Main\UserPhoneAuthTable::getList([
			"select" => $select,
			"filter" => [
				"=PHONE_NUMBER" => $phoneNumber,
			],
		])->fetchObject();

		if (!$userPhone)
		{
			$result->addError(new Main\Error(Loc::getMessage("main_register_no_user"), "ERR_NOT_FOUND"));
			return $result;
		}

		//alowed only once in a minute
		if ($userPhone->getDateSent())
		{
			$currentDateTime = new Main\Type\DateTime();
			if (($currentDateTime->getTimestamp() - $userPhone->getDateSent()->getTimestamp()) < static::PHONE_CODE_RESEND_INTERVAL)
			{
				$result->addError(new Main\Error(Loc::getMessage("main_register_timeout"), "ERR_TIMEOUT"));
				return $result;
			}
		}

		[$code, $phoneNumber] = static::GeneratePhoneCode($userPhone->getUserId());

		if ($siteId === null)
		{
			$siteId = CSite::GetDefSite($userPhone->getUser()->getLid());
		}
		$language = $userPhone->getUser()->getLanguageId();

		$sms = new Main\Sms\Event(
			$smsTemplate,
			[
				"USER_PHONE" => $phoneNumber,
				"CODE" => $code,
			]
		);

		$sms->setSite($siteId);
		if ($language != '')
		{
			//user preferred language
			$sms->setLanguage($language);
		}

		$result = $sms->send(true);

		$result->setData(["USER_ID" => $userPhone->getUserId()]);

		return $result;
	}

	protected static function SendEmailCode($userId, $siteId)
	{
		$result = new Main\Result();

		$context = new Authentication\Context();
		$context->setUserId($userId);

		$shortCode = new ShortCode($context);

		//alowed only once in a minute
		$check = $shortCode->checkDateSent();

		if ($check->isSuccess())
		{
			$code = $shortCode->generate();

			static::SendUserInfo($userId, $siteId, '', true, 'USER_CODE_REQUEST', $code);

			$shortCode->saveDateSent();
		}
		else
		{
			$result->addError(new Main\Error(Loc::getMessage("main_register_timeout"), "ERR_TIMEOUT"));
		}

		$result->setData($check->getData());

		return $result;
	}

	/**
	 * Returns the current authentication context, stored in the session.
	 * @return Authentication\Context
	 */
	public function getContext()
	{
		if ($this->context === null)
		{
			$this->context = Authentication\Context::jsonDecode((string)$this->GetParam('CONTEXT'));
		}
		return $this->context;
	}
}

class CUser extends CAllUser
{
}