Your IP : 3.14.68.91


Current Path : /home/bitrix/ext_www/crm.klimatlend.ua/bitrix/modules/socialservices/classes/general/
Upload File :
Current File : /home/bitrix/ext_www/crm.klimatlend.ua/bitrix/modules/socialservices/classes/general/apple.php

<?php

use Bitrix\Main\Localization\Loc;
use Bitrix\Main\Web\HttpClient;
use Bitrix\Main\Web\Json;
use Bitrix\Main\Web\JWK;
use Bitrix\Main\Web\JWT;

IncludeModuleLangFile(__FILE__);

class CSocServApple extends CSocServAuth
{
	public const ID = 'apple';
	public const LOGIN_PREFIX = 'apple_';
	private const CONTROLLER_URL = 'https://www.bitrix24.ru/controller';

	protected $entityOAuth;

	public function GetSettings()
	{
		return [
			['apple_client_id', Loc::getMessage('SOCSERV_APPLE_ID'), '', ['text', 40]],
			['apple_key_id', Loc::getMessage('SOCSERV_APPLE_KEY_ID'), '', ['text', 40]],
			['apple_team_id', Loc::getMessage('SOCSERV_APPLE_TEAM_ID'), '', ['text', 40]],
			['apple_key_pem', Loc::getMessage('SOCSERV_APPLE_KEY_PEM'), '', ['textarea', 10, 40]],
			[
				'note' => getMessage('SOCSERV_APPLE_SETT_NOTE_2'),
			],
		];
	}

	/**
	 * @param string|bool $code = false
	 * @return CAppleInterface
	 */
	public function getEntityOAuth($code = false): CAppleInterface
	{
		if (!$this->entityOAuth)
		{
			$this->entityOAuth = new CAppleInterface();
		}

		if ($code !== false)
		{
			$this->entityOAuth->setCode($code);
		}

		return $this->entityOAuth;
	}

	public function GetFormHtml($arParams)
	{
		$phrase = ($arParams['FOR_INTRANET'])
			? GetMessage('SOCSERV_APPLE_NOTE_INTRANET')
			: GetMessage('SOCSERV_APPLE_NOTE');

		return $arParams['FOR_INTRANET']
			? array('ON_CLICK' => 'onclick="'.\CSocServApple::GetOnClickJs($arParams).'"')
			: '<a href="javascript:void(0)" onclick="'.\CSocServApple::GetOnClickJs($arParams).'" class="bx-ss-button apple-button"></a><span class="bx-spacer"></span><span>' . $phrase . '</span>';
	}

	public function GetOnClickJs($arParams): string
	{
		$url = $this->getUrl($arParams);
		$controllerUrl = \Bitrix\Main\Engine\UrlManager::getInstance()->create('socialservices.authflow.signinapple', [])->getUri();

		return "top.location.href = '{$controllerUrl}&url=" . urlencode(CUtil::JSEscape($url)) . "'";
	}

	public function getUrl($arParams): string
	{
		global $APPLICATION;

		CSocServAuthManager::SetUniqueKey();
		if (defined('BX24_HOST_NAME') && IsModuleInstalled('bitrix24'))
		{
			$redirect_uri = static::CONTROLLER_URL . '/redirect.php';
			$state = $this->getEntityOAuth()->GetRedirectURI() . '?check_key=' . $_SESSION['UNIQUE_KEY'] . '&state=';
			$backurl = $APPLICATION->GetCurPageParam('', ['logout', 'auth_service_error', 'auth_service_id', 'backurl']);
			$state .= urlencode('state=' . urlencode('backurl=' . urlencode($backurl) . (isset($arParams['BACKURL']) ? '&redirect_url=' . urlencode($arParams['BACKURL']) : '')));
		}
		else
		{
			$state = 'site_id=' . SITE_ID . '&backurl=' .
				urlencode($APPLICATION->GetCurPageParam('check_key=' . $_SESSION['UNIQUE_KEY'], ['logout', 'auth_service_error', 'auth_service_id', 'backurl'])) .
				(isset($arParams['BACKURL']) ? '&redirect_url=' . urlencode($arParams['BACKURL']) : '');

			$redirect_uri = $this->getEntityOAuth()->GetRedirectURI();
		}

		return $this->getEntityOAuth()->GetAuthUrl($redirect_uri, $state);
	}

	public function addScope($scope): CAppleInterface
	{
		return $this->getEntityOAuth()->addScope($scope);
	}

	public function prepareUser($arUser): array
	{
		$entityOAuth = $this->getEntityOAuth();
		$arFields = [
			'EXTERNAL_AUTH_ID' => self::ID,
			'XML_ID' => $arUser['sub'],
			'LOGIN' => self::LOGIN_PREFIX.$arUser['sub'],
			'EMAIL' => $arUser['email'],
			'OATOKEN' => $entityOAuth->getToken(),
			'OATOKEN_EXPIRES' => $entityOAuth->getAccessTokenExpires(),
		];

		if (isset($arUser['first_name']))
		{
			$arFields['NAME'] =  $arUser['first_name'];
		}

		if (isset($arUser['last_name']))
		{
			$arFields['LAST_NAME'] =  $arUser['last_name'];
		}

		if (SITE_ID != '')
		{
			$arFields['SITE_ID'] = SITE_ID;
		}

		return $arFields;
	}

	public static function CheckUniqueKey($bUnset = true): bool
	{
		$arState = array();

		if (isset($_REQUEST['state']))
		{
			parse_str(html_entity_decode($_REQUEST['state']), $arState);

			if (isset($arState['backurl']))
			{
				InitURLParam($arState['backurl']);
			}
		}

		if (!isset($_REQUEST['check_key']) && isset($_REQUEST['backurl']))
		{
			InitURLParam($_REQUEST['backurl']);
		}

		$checkKey = '';
		if (isset($_REQUEST['check_key']))
		{
			$checkKey = $_REQUEST['check_key'];
		}
		elseif (isset($arState['check_key']))
		{
			$checkKey = $arState['check_key'];
		}

		if ($_SESSION['UNIQUE_KEY'] !== '' && $checkKey !== '' && ($checkKey === $_SESSION['UNIQUE_KEY']))
		{
			if ($bUnset)
			{
				unset($_SESSION['UNIQUE_KEY']);
			}

			return true;
		}
		return false;
	}

	public function Authorize(): void
	{
		global $APPLICATION;
		$APPLICATION->RestartBuffer();

		$authError = SOCSERV_AUTHORISATION_ERROR;

		if (
			isset($_REQUEST['code']) && $_REQUEST['code'] <> ''
			&& self::CheckUniqueKey()
		)
		{
			if (defined('BX24_HOST_NAME') && IsModuleInstalled('bitrix24'))
			{
				$redirect_uri = static::CONTROLLER_URL . '/redirect.php';
			}
			else
			{
				$redirect_uri = $this->getEntityOAuth()->GetRedirectURI();
			}

			$entityOAuth = $this->getEntityOAuth($_REQUEST['code']);
			if ($entityOAuth->GetAccessToken($redirect_uri) !== false)
			{
				$arUser = $entityOAuth->getCurrentUser();
				if (is_array($arUser) && isset($arUser["email"]))
				{
					$arFields = $this->prepareUser($arUser);
					$authError = $this->AuthorizeUser($arFields);
				}
			}
		}

		$bSuccess = $authError === true;

		$url = ($APPLICATION->GetCurDir() == "/login/") ? "" : $APPLICATION->GetCurDir();
		$aRemove = array("logout", "auth_service_error", "auth_service_id", "code", "error_reason", "error", "error_description", "check_key", "current_fieldset");

		if (isset($_REQUEST["state"]))
		{
			$arState = array();
			parse_str(html_entity_decode($_REQUEST["state"]), $arState);

			if (isset($arState['backurl']) || isset($arState['redirect_url']))
			{
				$url = !empty($arState['redirect_url']) ? $arState['redirect_url'] : $arState['backurl'];
				if (substr($url, 0, 1) !== "#")
				{
					$parseUrl = parse_url($url);

					$urlPath = $parseUrl["path"];
					$arUrlQuery = explode('&', $parseUrl["query"]);

					foreach ($arUrlQuery as $key => $value)
					{
						foreach ($aRemove as $param)
						{
							if (strpos($value, $param . "=") === 0)
							{
								unset($arUrlQuery[$key]);
								break;
							}
						}
					}

					$url = (!empty($arUrlQuery)) ? $urlPath . '?' . implode("&", $arUrlQuery) : $urlPath;
				}
			}
		}

		if ($authError === SOCSERV_REGISTRATION_DENY)
		{
			$url = (preg_match("/\?/", $url)) ? $url . '&' : $url . '?';
			$url .= 'auth_service_id=' . self::ID . '&auth_service_error=' . $authError;
		}
		elseif ($bSuccess !== true)
		{
			$url = (isset($urlPath)) ? $urlPath . '?auth_service_id=' . self::ID . '&auth_service_error=' . $authError : $GLOBALS['APPLICATION']->GetCurPageParam(('auth_service_id=' . self::ID . '&auth_service_error=' . $authError), $aRemove);
		}

		if (CModule::IncludeModule("socialnetwork") && strpos($url, "current_fieldset=") === false)
		{
			$url .= ((strpos($url, "?") === false) ? '?' : '&') . "current_fieldset=SOCSERV";
		}
		LocalRedirect($url);
		die();
	}

	public function setUser($userId)
	{
		$this->getEntityOAuth()->setUser($userId);
	}
}

class CAppleInterface extends CSocServOAuthTransport
{
	const SERVICE_ID = "apple";

	const AUTH_URL = 'https://appleid.apple.com/auth/authorize';
	const TOKEN_URL = 'https://appleid.apple.com/auth/token';
	private const PUBLIC_KEYS_URL = 'https://appleid.apple.com/auth/keys';

	private const CLIENT_SECRET_EXPIRATION_TIME = 3600;
	private const DECODE_ALGORITHM = 'RS256';
	private const BITRIX_APP_BUNDLE_ID = 'com.bitrixsoft.cpmobile';

	protected $userId = false;
	protected $responseData = array();
	protected $idToken;

	protected $scope = [
		'name', 'email'
	];

	private $keyId;
	private $teamId;
	private $secretKey;

	public function __construct($appID = false, $appSecret = false, $code = false)
	{
		if ($appID === false)
		{
			$appID = trim(\CSocServAuth::GetOption("apple_client_id"));
		}

		$this->keyId = trim(\CSocServAuth::GetOption('apple_key_id'));
		$this->teamId = trim(\CSocServAuth::GetOption('apple_team_id'));
		$this->secretKey = trim(\CSocServAuth::GetOption('apple_key_pem'));

		parent::__construct($appID, $appSecret, $code);
	}

	public function GetRedirectURI(): string
	{
		return \CHTTP::URN2URI('/bitrix/tools/oauth/apple.php');
	}

	public function GetAuthUrl($redirect_uri, $state = ''): string
	{
		return self::AUTH_URL .
			'?client_id=' . $this->appID .
			'&redirect_uri=' . urlencode($redirect_uri) .
			'&response_type=' . 'code' .
			'&scope=' . $this->getScopeEncode() .
			'&response_mode=' . 'form_post' .
			($state <> '' ? '&state=' . urlencode($state) : '');
	}

	public function getResult()
	{
		return $this->responseData;
	}

	public function GetAccessToken($redirect_uri = ''): bool
	{
		$token = $this->getStorageTokens();
		if (is_array($token))
		{
			$this->access_token = $token['OATOKEN'];
			$this->accessTokenExpires = $token['OATOKEN_EXPIRES'];

			if (!$this->code)
			{
				if ($this->checkAccessToken())
				{
					return true;
				}

				if (isset($token['REFRESH_TOKEN']) && $this->getNewAccessToken($token['REFRESH_TOKEN'], $this->userId, true))
				{
					return true;
				}
			}

			$this->deleteStorageTokens();
		}

		if ($this->code === false)
		{
			return false;
		}

		//case for sign in from Bitrix24 application on iOS device
		$request = \Bitrix\Main\Context::getCurrent()->getRequest()->toArray();
		if ($request['service'] === self::SERVICE_ID && $request['platform'] === 'ios')
		{
			$this->appID = self::BITRIX_APP_BUNDLE_ID;
		}

		$query = [
			'code' => $this->code,
			'grant_type' => 'authorization_code',
			'client_secret' => $this->getClientSecret(),
			'client_id' => $this->appID,
			'redirect_uri' => $redirect_uri,
		];

		$httpClient = new HttpClient([
			'socketTimeout' => $this->httpTimeout,
			'streamTimeout' => $this->httpTimeout,
		]);

		$result = $httpClient->post(self::TOKEN_URL, $query);
		try
		{
			$result = \Bitrix\Main\Web\Json::decode($result);
		}
		catch (\Bitrix\Main\ArgumentException $e)
		{
			$result = [];
		}

		if ((isset($result['access_token']) && $result['access_token'] <> ''))
		{
			$this->access_token = $result['access_token'];
			$this->accessTokenExpires = time() + $result['expires_in'];
			$this->refresh_token = $result['refresh_token'];
			$this->idToken = $result['id_token'];

			$_SESSION["OAUTH_DATA"] = [
				"OATOKEN" => $this->access_token,
				"OATOKEN_EXPIRES" => $this->accessTokenExpires,
				"REFRESH_TOKEN" => $this->refresh_token,
				"ID_TOKEN" => $this->idToken
			];
			return true;
		}

		return false;
	}

	public function getNewAccessToken($refreshToken = false, $userId = 0, $save = false): bool
	{
		if (!$this->appID || !$this->appSecret)
		{
			return false;
		}

		if (!$refreshToken)
		{
			$refreshToken = $this->refresh_token;
		}

		$http = new HttpClient(array(
			'socketTimeout' => $this->httpTimeout,
			'streamTimeout' => $this->httpTimeout,
		));

		$result = $http->post(static::TOKEN_URL, array(
			'refresh_token' => $refreshToken,
			'client_id' => $this->appID,
			'client_secret' => $this->getClientSecret(),
			'grant_type' => 'authorization_code',
		));

		try
		{
			$arResult = Json::decode($result);
		}
		catch (\Bitrix\Main\ArgumentException $e)
		{
			$arResult = array();
		}

		if (!empty($arResult['access_token']))
		{
			$this->access_token = $arResult['access_token'];
			$this->accessTokenExpires = $arResult['expires_in'] + time();
			if ($save && (int)$userId > 0)
			{
				$dbSocservUser = \Bitrix\Socialservices\UserTable::getList(array(
					'filter' => array(
						'=EXTERNAL_AUTH_ID' => static::SERVICE_ID,
						'=USER_ID' => $userId,
					),
					'select' => array('ID')
				));
				if ($arOauth = $dbSocservUser->fetch())
				{
					\Bitrix\Socialservices\UserTable::update($arOauth['ID'], array(
							'OATOKEN' => $this->access_token,
							'OATOKEN_EXPIRES' => $this->accessTokenExpires)
					);
				}
			}

			return true;
		}

		return false;
	}

	public function getCurrentUser()
	{
		if ($this->access_token === false || $this->idToken === false)
		{
			return false;
		}

		try
		{
			$user = $this->decodeIdentityToken($this->idToken);
		}
		catch (Exception $exception)
		{
			return false;
		}

		$user = (array)$user;

		if (!empty($user['sub']) && isset($_REQUEST['user']))
		{
			$userData = json_decode($_REQUEST['user'], true);

			if (!empty($userData))
			{
				$user['first_name'] = $userData['name']['firstName'];
				$user['last_name'] = $userData['name']['lastName'];
			}
		}

		return $user;
	}

	public function GetAppInfo(): array
	{
		$app = [
			'id' => self::BITRIX_APP_BUNDLE_ID
		];

		return $app;
	}

	public function getScopeEncode(): string
	{
		return implode(' ', array_map('urlencode', array_unique($this->getScope())));
	}

	private function getClientSecret(): string
	{
		return $this->generateSignedJWT($this->keyId, $this->teamId, $this->appID, $this->secretKey);
	}

	private function generateSignedJWT(string $keyId, string $teamId, string $clientId, string $secretKey)
	{
		$header = [
			'alg' => 'ES256',
			'kid' => $keyId
		];
		$body = array(
			'iss' => $teamId,
			'iat' => time(),
			'exp' => time() + self::CLIENT_SECRET_EXPIRATION_TIME,
			'aud' => 'https://appleid.apple.com',
			'sub' => $clientId
		);

		$privateKey = openssl_pkey_get_private($secretKey);
		if (!$privateKey)
		{
			return false;
		}

		$payload = JWT::urlsafeB64Encode(json_encode($header)) . '.' . JWT::urlsafeB64Encode(json_encode($body));
		$signature = '';
		$signResult = openssl_sign($payload, $signature, $privateKey, OPENSSL_ALGO_SHA256);
		if (!$signResult)
		{
			return false;
		}

		$rawSignature = self::convertDERSignature($signature, 64);

		return $payload . '.' . JWT::urlsafeB64Encode($rawSignature);
	}

	private static function convertDERSignature(string $der, int $partLength): string
	{
		$hex = unpack('H*', $der)[1];
		if ('30' !== mb_substr($hex, 0, 2, '8bit'))
		{ // SEQUENCE
			throw new \RuntimeException();
		}
		if ('81' === mb_substr($hex, 2, 2, '8bit'))
		{ // LENGTH > 128
			$hex = mb_substr($hex, 6, null, '8bit');
		}
		else
		{
			$hex = mb_substr($hex, 4, null, '8bit');
		}
		if ('02' !== mb_substr($hex, 0, 2, '8bit'))
		{ // INTEGER
			throw new \RuntimeException();
		}
		$Rl = hexdec(mb_substr($hex, 2, 2, '8bit'));
		$R = self::retrievePositiveInteger(mb_substr($hex, 4, $Rl * 2, '8bit'));
		$R = str_pad($R, $partLength, '0', STR_PAD_LEFT);
		$hex = mb_substr($hex, 4 + $Rl * 2, null, '8bit');
		if ('02' !== mb_substr($hex, 0, 2, '8bit'))
		{ // INTEGER
			throw new \RuntimeException();
		}
		$Sl = hexdec(mb_substr($hex, 2, 2, '8bit'));
		$S = self::retrievePositiveInteger(mb_substr($hex, 4, $Sl * 2, '8bit'));
		$S = str_pad($S, $partLength, '0', STR_PAD_LEFT);

		return pack('H*', $R . $S);
	}

	/**
	 * @param string $data
	 *
	 * @return string
	 */
	private static function retrievePositiveInteger(string $data): string
	{
		while ('00' === mb_substr($data, 0, 2, '8bit') && mb_substr($data, 2, 2, '8bit') > '7f')
		{
			$data = mb_substr($data, 2, null, '8bit');
		}
		return $data;
	}

	private function fetchPublicKey()
	{
		$publicKeyDetails = [];

		$http = new HttpClient([
			'socketTimeout' => $this->httpTimeout,
			'streamTimeout' => $this->httpTimeout,
		]);
		$publicKeys = $http->get(self::PUBLIC_KEYS_URL);

		try
		{
			$decodedPublicKeys = json_decode($publicKeys, true);
		}
		catch (Exception $e)
		{
			return false;
		}

		if (!isset($decodedPublicKeys['keys']) || count($decodedPublicKeys['keys']) < 1)
		{
			return false;
		}

		$parsedPublicKeys = JWK::parseKeySet($decodedPublicKeys['keys']);

		foreach ($parsedPublicKeys as $keyId => $publicKey)
		{
			$details = openssl_pkey_get_details($publicKey);
			$publicKeyDetails[$keyId] = $details['key'];
		}

		return $publicKeyDetails;
	}

	private function decodeIdentityToken(string $identityToken)
	{
		$payload = '';

		$publicKeys = $this->fetchPublicKey();
		if (is_array($publicKeys))
		{
			$payload = JWT::decode($identityToken, $publicKeys, [self::DECODE_ALGORITHM]);
		}

		return $payload;
	}

}