<?php
namespace App\Service\Internal;
use App\Model\WsResponse;
use App\Normalizer\QueryParametersNormalizer;
use App\Security\KasUserAuthenticator;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\Routing\RouterInterface;
use Unirest;
use App\Exception\RedirectException;
use Symfony\Component\HttpFoundation\RedirectResponse;
class CoreApiService
{
public const HTTP_CROSS_SITE = 'cross-site';
public const HTTP_SAME_SITE = 'same-site';
public const PUBLIC_ACCESS_TOKEN = 'public_access_token';
public const USER_ACCESS_TOKEN = 'user_access_token';
public const APP_ACCESS_TOKEN = 'app_access_token';
public const APPLICATION_ID = 'application_id';
public const CALLER_TYPE_USER = 'client';
public const CALLER_TYPE_PUBLIC = 'public';
public const CALLER_TYPE_APP = 'app';
public const MAPPING_LOCALES = [
'en' => 'en_US',
'fr' => 'fr_FR',
'it' => 'it_IT',
'ar' => 'ar_AR',
'ja' => 'ja_JP',
'nl' => 'nl_NL',
'eu' => 'eu_ES'
];
public const CALL_FUNCTION_NAME = [
'test' => 'testCall',
'local' => 'realCall',
'dev' => 'realCall',
'preprod' => 'realCall',
'prod' => 'realCall'
];
public const NTW_NOT_LOADED_MSG = 'NETWORK_NOT_LOADED';
/**
* @var ParameterBagInterface
*/
protected $params;
/**
* @var LoggerInterface
*/
private $logger;
/**
* @var string
*/
private $env;
/**
* @var RequestStack
*/
private $requestStack;
/**
* @var RouterInterface
*/
private $router;
/**
* @var QueryParametersNormalizer
*/
private $queryParametersNormalizer;
private bool $accountModule;
public function __construct(
ParameterBagInterface $parameterBag,
LoggerInterface $logger,
string $env,
RequestStack $requestStack,
RouterInterface $router,
QueryParametersNormalizer $queryParameterNormalizer,
bool $accountModule
) {
$this->params = $parameterBag;
$this->logger = $logger;
$this->env = $env;
$this->requestStack = $requestStack;
$this->router = $router;
$this->queryParametersNormalizer = $queryParameterNormalizer;
$this->accountModule = $accountModule;
}
/**
* @param String|null $callerType type of user who is calling this webservice. Used to get the right token
* @param String $method ex: "GET", "POST", ...
* @param String $webServiceRoute ex : "/InstantCore/v1/GetNetwork" : used to get the ApiProvider for unit tests
* @param String $webServicePath ex : "/InstantUser/v1/communities/" ; $config->apiUrl is automatically added
* @param array $headers Content-Type and Authorization are setted to "application/json" and Token::getUserAuthToken() if missing
* @param array $params key is setted to $networkId if missing
* @param array|string $body
* @param array|null $options Available options :
* - debug : boolean (false) : additionnal logs if true
* - rawBody : boolean (false) : if false, $body is an array; it's json encoded; if $true, body is a String and passed as is to webservice
* @return WsResponse WsResponse object with :
* - status = -1 if an error occurs while try to connect, http status code otherwise
* - errorMessage = null if $httpStatusCode is like "2xx"
* - response = null if $httpStatusCode is not like "2xx"
* TODO : add option 'throwExceptionIfError' with default value (true ?)
* @throws \Exception
*/
public function realCall(
$callerType,
string $method,
string $webServiceRoute,
string $webServicePath,
array $headers,
array $params,
$body = null,
array $options = null
): WsResponse {
$debug = isset($options['debug']) ? $options['debug'] : false;
$rawBody = isset($options['rawBody']) ? $options['rawBody'] : false;
$request = $this->requestStack->getCurrentRequest();
$session = $request->getSession();
$xHeaders = $session->get('xHeaders');
$sessionToken = $session->get('token');
$context = $session->get('context');
$applicationId = $session->get(static::APPLICATION_ID);
$sessionCountModes = $session->get('count_modes');
$countModes = isset($sessionCountModes) ? $sessionCountModes : "0";
$journeyWs = strpos($webServicePath, 'v3/journeys');
if ($journeyWs) {
$data = $body;
$json = json_decode($data);
$isJourney = true;
$session->set('count_modes', $countModes + 1);
} else {
$isJourney = false;
}
$secFetchSite = $request->headers->get('sec-fetch-site');
if (
!in_array($secFetchSite, [self::HTTP_CROSS_SITE, self::HTTP_SAME_SITE])
&& 'WIDGET' === $session->get('context')
) {
$session->set('context', 'SIM');
} elseif (in_array($secFetchSite, [self::HTTP_CROSS_SITE, self::HTTP_SAME_SITE])) {
$session->set('context', 'WIDGET');
}
$request = $this->requestStack->getCurrentRequest();
$headers['calling-url'] = $request->getSchemeAndHttpHost() . $request->getRequestUri();
$token = isset($sessionToken) ? $sessionToken : null;
if (
$session->get('context') == 'WIDGET'
|| !$session->get('context')
|| ($session->get('context') == 'WIDGET' && $isJourney)
|| (!$session->get('context') && $isJourney)
|| ($token && $isJourney)
) {
$headers['x-widget-id'] = isset($xHeaders['x-widget-id']) ? $xHeaders['x-widget-id'] : null;
$headers['referer'] = isset($xHeaders['x-referer']) ? $xHeaders['x-referer'] : null;
$headers['origin'] = isset($xHeaders['x-origin']) ? $xHeaders['x-origin'] : null;
if ($isJourney && $sessionCountModes >= $json->countModes - 1) {
$session->remove('token');
$session->remove('count_modes');
}
}
$headers['user-agent'] = $request->headers->get('user-agent');
$locales = $this->params->get('language.supported');
$locale = $this->requestStack->getCurrentRequest()->attributes->get('_locale');
$defaultLocale = $this->requestStack->getCurrentRequest()->getDefaultLocale();
if(!isset($headers['accept-language'])) {
$headers['accept-language'] = self::MAPPING_LOCALES[$defaultLocale];
if (in_array($locale, $locales) && !empty(self::MAPPING_LOCALES[$locale])) {
$headers['accept-language'] = self::MAPPING_LOCALES[$locale];
}
}
if ($applicationId) {
$headers ['application-id'] = $applicationId;
}
if (!isset($headers['Content-Type'])) {
$headers ['Content-Type'] = 'application/json';
}
$headers['X-Consumer-ID'] = 'sim';
// This wont be necessary and even won't work if account module is not activated for this network !
if (!isset($headers['Authorization']) && !is_null($callerType) && $this->accountModule) {
switch ($callerType) {
case self::CALLER_TYPE_USER:
$token = $this->getUserAuthToken();
break;
case self::CALLER_TYPE_PUBLIC:
$token = $this->getPublicAuthToken();
break;
case self::CALLER_TYPE_APP:
$token = $this->getAppAuthToken();
break;
default:
$this->logger->error('[ERROR] WebService : unknown caller type');
throw new \Exception('WebService : unknown caller type');
}
if (isset($token->token_type) && isset($token->access_token)) {
$headers['Authorization'] = $token->token_type . ' ' . $token->access_token;
}
if ($this->params->get('global.login_mode') == KasUserAuthenticator::LOGIN_MODE) {
$userToken = $session->get(CoreApiService::USER_ACCESS_TOKEN);
$cookieValue = $request->cookies->get('applicativeToken');
$applicativeToken = $cookieValue !== null ? json_decode($cookieValue) : null;
$applicativeToken = $applicativeToken ?? $session->get(static::PUBLIC_ACCESS_TOKEN);
if ($userToken) {
$token = $userToken;
$accessToken = $userToken->accessToken;
$tokenType = (empty($userToken->tokenType)) || $userToken->tokenType == "" ? 'Bearer' : $userToken->tokenType;
$headers['Authorization'] = $tokenType . ' ' . $accessToken;
} elseif ($applicativeToken) {
$token = $applicativeToken;
$accessToken = $applicativeToken->accessToken;
$tokenType = (empty($applicativeToken->tokenType)) || $applicativeToken->tokenType == "" ? 'Bearer' : $applicativeToken->tokenType;
$headers['Authorization'] = $tokenType . ' ' . $accessToken;
}
}
if (empty($token)) {
$this->logger->error('[ERROR] WebService : token not found');
}
}
$query = $webServicePath;
$body = (empty($body) ? null : ($rawBody ? $body : json_encode($body)));
if ($debug) {
$this->logger->info("WS $method $query with args : " . print_r($params), []);
}
if (!empty($params)) {
$params = $this->queryParametersNormalizer->normalize($params);
$query .= "?" . http_build_query($params, '', '&', PHP_QUERY_RFC3986);
}
$error_message = "";
$httpStatusCode = "";
$responseBody = "";
try {
$wsResponse = $this->callRest($method, $query, $headers, $body);
if ($wsResponse->code == 503 &&
(isset($wsResponse->body->message) && ($wsResponse->body->message == 'TICKETING_NOT_ACTIVE' ||
$wsResponse->body->message == 'FINE_NOT_ACTIVE'))) {
throw new RedirectException(
new RedirectResponse(
$this->router->generate(
"message_info",
array('message' => $wsResponse->body->message)
)
)
);
}
$responseBody = null;
$httpStatusCode = $wsResponse->code;
if (substr($httpStatusCode, 0, 1) != "2") { // status != 2xx
$responseBody = $wsResponse->body;
$this->logger->error("WS $method $webServicePath (" . $httpStatusCode . ")");
} else {
$responseBody = $wsResponse->body;
}
if ($debug) {
$this->logger->info("WS $method $query returns ($httpStatusCode) " . print_r($responseBody, true));
}
} catch (Unirest\Exception $exc) {
$error_message = $exc;
$this->logger->error("WS $method $webServicePath : error : " . $error_message);
}
if (!empty($responseBody) && (is_array($responseBody) || is_object($responseBody))) {
$responseBody = $this->parseResponse($responseBody);
}
return new WsResponse($responseBody, $httpStatusCode, $error_message);
}
/**
* @return mixed|null|String
* @throws \Exception
*/
public function getUserAuthToken()
{
$result = $this->requestStack->getSession()->get(static::USER_ACCESS_TOKEN);
if (!$result) {
$token = static::getPublicAuthToken();
return $token;
}
return $result;
}
/**
* @return mixed|null|String
* @throws \Exception
*/
public function getPublicAuthToken()
{
$session = $this->requestStack->getSession();
$token = $session->get(static::PUBLIC_ACCESS_TOKEN);
if ($this->params->get('global.login_mode') == KasUserAuthenticator::LOGIN_MODE) {
if ($token) {
$now = time();
$diff = $token->expirationDate - $now;
if ($diff > 30 && $diff < 90) {
$token = $this->getKasAppToken();
} elseif ($diff < 0) {
$session->invalidate();
$token = $this->getKasAppToken();
}
} else {
$token = $this->getKasAppToken();
}
} elseif (!$token) {
$clientId = $this->params->get('ws_client_id');
$secret = $this->params->get('ws_secret');
$hash_auth = base64_encode($clientId . ":" . $secret);
$route = '';
$url = $this->getApiUserUrl() . '/oauth/token';
$headers = ['Authorization' => 'Basic ' . $hash_auth];
$params = ['grant_type' => 'client_credentials'];
$wsResponse = $this->call(
null,
'GET',
$route,
$url,
$headers,
$params
);
if ($wsResponse->isError()) {
$this->logger->error(
'[ERROR] Get public token : ' . $wsResponse->errorMessage . ' (' . $wsResponse->status . ')'
);
return null;
}
$token = $wsResponse->body;
$session->set(static::PUBLIC_ACCESS_TOKEN, $token);
}
return $token;
}
public function getKasAppToken()
{
$networkId = $this->params->get('network_id');
$url = $this->getApiKasUrl() . '/v1/networks/' . $networkId . '/login';
$body = [
'clientId' => $this->params->get('kasClientId'),
'clientSecret' => $this->params->get('kasSecret'),
'grantType' => 'client_credentials'
];
$wsResponse = $this->call(
null,
'POST',
'',
$url,
[],
[],
$body
);
if ($wsResponse->isError()) {
$this->logger->error(
'[ERROR] Get public token : ' . $wsResponse->errorMessage . ' (' . $wsResponse->status . ')'
);
return null;
}
$token = $this->encryptResponseToken($wsResponse->body);
$this->requestStack->getSession()->set(static::PUBLIC_ACCESS_TOKEN, $token);
return $token;
}
private function encryptResponseToken($response)
{
$token = $response->accessToken;
$tokenParts = explode(".", $token);
$tokenPayload = base64_decode($tokenParts[1]);
$jwtPayload = json_decode($tokenPayload);
$response->expirationDate = $jwtPayload->exp;
return $response;
}
/**
* @param String|null $callerType
* @param String $method
* @param String $webServiceRoute
* @param String $webServicePath
* @param array $headers
* @param array $params
* @param array|string $body
* @param array|null $options
*
* @return WsResponse
*
* @throws \Exception
*/
public function call(
$callerType,
string $method,
string $webServiceRoute,
string $webServicePath,
array $headers,
array $params,
$body = null,
array $options = null
): WsResponse {
return call_user_func(
[$this, self::CALL_FUNCTION_NAME[$this->env]],
$callerType,
$method,
$webServiceRoute,
$webServicePath,
$headers,
$params,
$body,
$options
);
}
public function getApiUserUrl()
{
return $this->params->get('api_user_url');
}
/**
* @return mixed|null|String
* @throws \Exception
*/
public function getAppAuthToken()
{
$session = $this->requestStack->getSession();
$token = $session->get(static::APP_ACCESS_TOKEN);
if ($token == null) {
$clientId = "instant-core";
$secret = "Ohsaisoh5yai";
$hash_auth = base64_encode($clientId . ":" . $secret);
$route = '';
$url = $this->getApiUserUrl() . '/oauth/token';
$headers = ['Authorization' => 'Basic ' . $hash_auth];
$params = ['grant_type' => 'client_credentials'];
$wsResponse = $this->call(
null,
'GET',
$route,
$url,
$headers,
$params
);
if ($wsResponse->isError()) {
$this->logger->error(
'[ERROR] Get public token : ' . $wsResponse->errorMessage . ' (' . $wsResponse->status . ')'
);
return null;
}
$token = $wsResponse->body;
$session->set(static::APP_ACCESS_TOKEN, $token);
}
return $token;
}
/**
* @param string $mode
* @param string $url
* @param array $headers
* @param array|string $body
* @return Unirest\Response
*/
static function callRest($mode, $url, $headers, $body)
{
// Disables SSL cert validation temporary
Unirest\Request::verifyPeer(false);
switch (\strtoupper($mode)) {
case "GET":
$response = Unirest\Request::get($url, $headers, $body);
break;
case "POST":
$response = Unirest\Request::post($url, $headers, $body);
break;
case "PUT":
$response = Unirest\Request::put($url, $headers, $body);
break;
case "DELETE":
$response = Unirest\Request::delete($url, $headers, $body);
break;
default:
$response = [];
break;
}
return $response;
}
private function parseResponse($responseBody)
{
return $this->updateDateTimeField($responseBody);
}
private function updateDateTimeField(&$responseBody)
{
$fieldsFound = [];
// Search DateTimeIso8601 field on the body response
foreach ($responseBody as $k => &$v) {
if (is_array($v)) {
$v = $this->updateDateTimeField($v);
} else {
if (preg_match('/(.*DateTimeIso8601.*)/i', $k, $matches)) {
$fieldsFound[] = $matches[1];
}
}
}
// Replace values of xxxDateTime by xxxDateTimeIso8601
if (!empty($fieldsFound)) {
foreach ($fieldsFound as $index) {
$responseBody = $this->replaceDateTimeInsensitive($responseBody, $index);
}
}
return $responseBody;
}
private function replaceDateTimeInsensitive($responseBody, $subject)
{
$replaces = [
str_replace('Iso8601', '', $subject), // Original field is xxxDateTime
str_replace('TimeIso8601', '', $subject), // Sometimes original fields have xxxDate instead of xxxDateTime
str_replace('DateTimeIso8601', '', $subject), // Sometimes original field have xxx instead of xxxDateTime
];
foreach ($replaces as $replaceIndex) {
// Break when found occurence, no need to go further
if (isset($responseBody->{$replaceIndex})) {
// Match case
$responseBody->{$replaceIndex} = $responseBody->{$subject};
break;
} elseif (isset($responseBody->{strtolower($replaceIndex)})) {
// Case insensitive
$responseBody->{strtolower($replaceIndex)} = $responseBody->{$subject};
break;
}
}
return $responseBody;
}
public function getApiCoreUrl()
{
return $this->params->get('api_core_url');
}
public function getApiDataUrl()
{
return $this->params->get('api_data_url');
}
public function getApiNotificationUrl()
{
return $this->params->get('api_notification_url');
}
public function getApiMaasUrl()
{
return $this->params->get('api_maas_url');
}
public function getUserWebUrl()
{
return $this->params->get('userweb_url');
}
public function getApiKasUrl()
{
return $this->params->get('api_kas_url');
}
public function getApiAddressUrl()
{
return $this->params->get('api_address_url');
}
}