src/Service/Internal/CoreApiService.php line 172

Open in your IDE?
  1. <?php
  2. namespace App\Service\Internal;
  3. use App\Model\WsResponse;
  4. use App\Normalizer\QueryParametersNormalizer;
  5. use App\Security\KasUserAuthenticator;
  6. use Psr\Log\LoggerInterface;
  7. use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
  8. use Symfony\Component\HttpFoundation\RequestStack;
  9. use Symfony\Component\Routing\RouterInterface;
  10. use Unirest;
  11. use App\Exception\RedirectException;
  12. use Symfony\Component\HttpFoundation\RedirectResponse;
  13. class CoreApiService
  14. {
  15. public const HTTP_CROSS_SITE = 'cross-site';
  16. public const HTTP_SAME_SITE = 'same-site';
  17. public const PUBLIC_ACCESS_TOKEN = 'public_access_token';
  18. public const USER_ACCESS_TOKEN = 'user_access_token';
  19. public const APP_ACCESS_TOKEN = 'app_access_token';
  20. public const APPLICATION_ID = 'application_id';
  21. public const CALLER_TYPE_USER = 'client';
  22. public const CALLER_TYPE_PUBLIC = 'public';
  23. public const CALLER_TYPE_APP = 'app';
  24. public const MAPPING_LOCALES = [
  25. 'en' => 'en_US',
  26. 'fr' => 'fr_FR',
  27. 'it' => 'it_IT',
  28. 'ar' => 'ar_AR',
  29. 'ja' => 'ja_JP',
  30. 'nl' => 'nl_NL',
  31. 'eu' => 'eu_ES',
  32. 'sk' => 'sk_SK'
  33. ];
  34. public const CALL_FUNCTION_NAME = [
  35. 'test' => 'testCall',
  36. 'local' => 'realCall',
  37. 'dev' => 'realCall',
  38. 'preprod' => 'realCall',
  39. 'prod' => 'realCall'
  40. ];
  41. public const NTW_NOT_LOADED_MSG = 'NETWORK_NOT_LOADED';
  42. /**
  43. * @var ParameterBagInterface
  44. */
  45. protected $params;
  46. /**
  47. * @var LoggerInterface
  48. */
  49. private $logger;
  50. /**
  51. * @var string
  52. */
  53. private $env;
  54. /**
  55. * @var RequestStack
  56. */
  57. private $requestStack;
  58. /**
  59. * @var RouterInterface
  60. */
  61. private $router;
  62. /**
  63. * @var QueryParametersNormalizer
  64. */
  65. private $queryParametersNormalizer;
  66. private bool $accountModule;
  67. public function __construct(
  68. ParameterBagInterface $parameterBag,
  69. LoggerInterface $logger,
  70. string $env,
  71. RequestStack $requestStack,
  72. RouterInterface $router,
  73. QueryParametersNormalizer $queryParameterNormalizer,
  74. bool $accountModule
  75. ) {
  76. $this->params = $parameterBag;
  77. $this->logger = $logger;
  78. $this->env = $env;
  79. $this->requestStack = $requestStack;
  80. $this->router = $router;
  81. $this->queryParametersNormalizer = $queryParameterNormalizer;
  82. $this->accountModule = $accountModule;
  83. }
  84. /**
  85. * @param String|null $callerType type of user who is calling this webservice. Used to get the right token
  86. * @param String $method ex: "GET", "POST", ...
  87. * @param String $webServiceRoute ex : "/InstantCore/v1/GetNetwork" : used to get the ApiProvider for unit tests
  88. * @param String $webServicePath ex : "/InstantUser/v1/communities/" ; $config->apiUrl is automatically added
  89. * @param array $headers Content-Type and Authorization are setted to "application/json" and Token::getUserAuthToken() if missing
  90. * @param array $params key is setted to $networkId if missing
  91. * @param array|string $body
  92. * @param array|null $options Available options :
  93. * - debug : boolean (false) : additionnal logs if true
  94. * - 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
  95. * @return WsResponse WsResponse object with :
  96. * - status = -1 if an error occurs while try to connect, http status code otherwise
  97. * - errorMessage = null if $httpStatusCode is like "2xx"
  98. * - response = null if $httpStatusCode is not like "2xx"
  99. * TODO : add option 'throwExceptionIfError' with default value (true ?)
  100. * @throws \Exception
  101. */
  102. public function realCall(
  103. $callerType,
  104. string $method,
  105. string $webServiceRoute,
  106. string $webServicePath,
  107. array $headers,
  108. array $params,
  109. $body = null,
  110. array $options = null
  111. ): WsResponse {
  112. $debug = isset($options['debug']) ? $options['debug'] : false;
  113. $rawBody = isset($options['rawBody']) ? $options['rawBody'] : false;
  114. $request = $this->requestStack->getCurrentRequest();
  115. $session = $request->getSession();
  116. $xHeaders = $session->get('xHeaders');
  117. $sessionToken = $session->get('token');
  118. $context = $session->get('context');
  119. $applicationId = $session->get(static::APPLICATION_ID);
  120. $sessionCountModes = $session->get('count_modes');
  121. $countModes = isset($sessionCountModes) ? $sessionCountModes : "0";
  122. $journeyWs = strpos($webServicePath, 'v3/journeys');
  123. if ($journeyWs) {
  124. $data = $body;
  125. $json = json_decode($data);
  126. $isJourney = true;
  127. $session->set('count_modes', $countModes + 1);
  128. } else {
  129. $isJourney = false;
  130. }
  131. $secFetchSite = $request->headers->get('sec-fetch-site');
  132. if (
  133. !in_array($secFetchSite, [self::HTTP_CROSS_SITE, self::HTTP_SAME_SITE])
  134. && 'WIDGET' === $session->get('context')
  135. ) {
  136. $session->set('context', 'SIM');
  137. } elseif (in_array($secFetchSite, [self::HTTP_CROSS_SITE, self::HTTP_SAME_SITE])) {
  138. $session->set('context', 'WIDGET');
  139. }
  140. $request = $this->requestStack->getCurrentRequest();
  141. $headers['calling-url'] = $request->getSchemeAndHttpHost() . $request->getRequestUri();
  142. $token = isset($sessionToken) ? $sessionToken : null;
  143. if (
  144. $session->get('context') == 'WIDGET'
  145. || !$session->get('context')
  146. || ($session->get('context') == 'WIDGET' && $isJourney)
  147. || (!$session->get('context') && $isJourney)
  148. || ($token && $isJourney)
  149. ) {
  150. $headers['x-widget-id'] = isset($xHeaders['x-widget-id']) ? $xHeaders['x-widget-id'] : null;
  151. $headers['referer'] = isset($xHeaders['x-referer']) ? $xHeaders['x-referer'] : null;
  152. $headers['origin'] = isset($xHeaders['x-origin']) ? $xHeaders['x-origin'] : null;
  153. if ($isJourney && $sessionCountModes >= $json->countModes - 1) {
  154. $session->remove('token');
  155. $session->remove('count_modes');
  156. }
  157. }
  158. $headers['user-agent'] = $request->headers->get('user-agent');
  159. $locales = $this->params->get('language.supported');
  160. $locale = $this->requestStack->getCurrentRequest()->attributes->get('_locale');
  161. $defaultLocale = $this->requestStack->getCurrentRequest()->getDefaultLocale();
  162. if(!isset($headers['accept-language'])) {
  163. $headers['accept-language'] = self::MAPPING_LOCALES[$defaultLocale];
  164. if (in_array($locale, $locales) && !empty(self::MAPPING_LOCALES[$locale])) {
  165. $headers['accept-language'] = self::MAPPING_LOCALES[$locale];
  166. }
  167. }
  168. if ($applicationId) {
  169. $headers ['application-id'] = $applicationId;
  170. }
  171. if (!isset($headers['Content-Type'])) {
  172. $headers ['Content-Type'] = 'application/json';
  173. }
  174. $headers['X-Consumer-ID'] = 'sim';
  175. // This wont be necessary and even won't work if account module is not activated for this network !
  176. if (!isset($headers['Authorization']) && !is_null($callerType) && $this->accountModule) {
  177. switch ($callerType) {
  178. case self::CALLER_TYPE_USER:
  179. $token = $this->getUserAuthToken();
  180. break;
  181. case self::CALLER_TYPE_PUBLIC:
  182. $token = $this->getPublicAuthToken();
  183. break;
  184. case self::CALLER_TYPE_APP:
  185. $token = $this->getAppAuthToken();
  186. break;
  187. default:
  188. $this->logger->error('[ERROR] WebService : unknown caller type');
  189. throw new \Exception('WebService : unknown caller type');
  190. }
  191. if (isset($token->token_type) && isset($token->access_token)) {
  192. $headers['Authorization'] = $token->token_type . ' ' . $token->access_token;
  193. }
  194. if ($this->params->get('global.login_mode') == KasUserAuthenticator::LOGIN_MODE) {
  195. $userToken = $session->get(CoreApiService::USER_ACCESS_TOKEN);
  196. $applicativeToken = json_decode($request->cookies->get('applicativeToken')) ?? $session->get(static::PUBLIC_ACCESS_TOKEN);
  197. if ($userToken) {
  198. $token = $userToken;
  199. $accessToken = $userToken->accessToken;
  200. $tokenType = (empty($userToken->tokenType)) || $userToken->tokenType == "" ? 'Bearer' : $userToken->tokenType;
  201. $headers['Authorization'] = $tokenType . ' ' . $accessToken;
  202. } elseif ($applicativeToken) {
  203. $token = $applicativeToken;
  204. $accessToken = $applicativeToken->accessToken;
  205. $tokenType = (empty($applicativeToken->tokenType)) || $applicativeToken->tokenType == "" ? 'Bearer' : $applicativeToken->tokenType;
  206. $headers['Authorization'] = $tokenType . ' ' . $accessToken;
  207. }
  208. }
  209. if (empty($token)) {
  210. $this->logger->error('[ERROR] WebService : token not found');
  211. }
  212. }
  213. $query = $webServicePath;
  214. $body = (empty($body) ? null : ($rawBody ? $body : json_encode($body)));
  215. if ($debug) {
  216. $this->logger->info("WS $method $query with args : " . print_r($params), []);
  217. }
  218. if (!empty($params)) {
  219. $params = $this->queryParametersNormalizer->normalize($params);
  220. $query .= "?" . http_build_query($params, null, '&', PHP_QUERY_RFC3986);
  221. }
  222. $error_message = "";
  223. $httpStatusCode = "";
  224. $responseBody = "";
  225. try {
  226. $wsResponse = $this->callRest($method, $query, $headers, $body);
  227. if ($wsResponse->code == 503 &&
  228. (isset($wsResponse->body->message) && ($wsResponse->body->message == 'TICKETING_NOT_ACTIVE' ||
  229. $wsResponse->body->message == 'FINE_NOT_ACTIVE'))) {
  230. throw new RedirectException(
  231. new RedirectResponse(
  232. $this->router->generate(
  233. "message_info",
  234. array('message' => $wsResponse->body->message)
  235. )
  236. )
  237. );
  238. }
  239. $responseBody = null;
  240. $httpStatusCode = $wsResponse->code;
  241. if (substr($httpStatusCode, 0, 1) != "2") { // status != 2xx
  242. $responseBody = $wsResponse->body;
  243. $this->logger->error("WS $method $webServicePath (" . $httpStatusCode . ")");
  244. } else {
  245. $responseBody = $wsResponse->body;
  246. }
  247. if ($debug) {
  248. $this->logger->info("WS $method $query returns ($httpStatusCode) " . print_r($responseBody, true));
  249. }
  250. } catch (Unirest\Exception $exc) {
  251. $error_message = $exc;
  252. $this->logger->error("WS $method $webServicePath : error : " . $error_message);
  253. }
  254. if (!empty($responseBody) && (is_array($responseBody) || is_object($responseBody))) {
  255. $responseBody = $this->parseResponse($responseBody);
  256. }
  257. return new WsResponse($responseBody, $httpStatusCode, $error_message);
  258. }
  259. /**
  260. * @return mixed|null|String
  261. * @throws \Exception
  262. */
  263. public function getUserAuthToken()
  264. {
  265. $result = $this->requestStack->getSession()->get(static::USER_ACCESS_TOKEN);
  266. if (!$result) {
  267. $token = static::getPublicAuthToken();
  268. return $token;
  269. }
  270. return $result;
  271. }
  272. /**
  273. * @return mixed|null|String
  274. * @throws \Exception
  275. */
  276. public function getPublicAuthToken()
  277. {
  278. $session = $this->requestStack->getSession();
  279. $token = $session->get(static::PUBLIC_ACCESS_TOKEN);
  280. if ($this->params->get('global.login_mode') == KasUserAuthenticator::LOGIN_MODE) {
  281. if ($token) {
  282. $now = time();
  283. $diff = $token->expirationDate - $now;
  284. if ($diff > 30 && $diff < 90) {
  285. $token = $this->getKasAppToken();
  286. } elseif ($diff < 0) {
  287. $session->invalidate();
  288. $token = $this->getKasAppToken();
  289. }
  290. } else {
  291. $token = $this->getKasAppToken();
  292. }
  293. } elseif (!$token) {
  294. $clientId = $this->params->get('ws_client_id');
  295. $secret = $this->params->get('ws_secret');
  296. $hash_auth = base64_encode($clientId . ":" . $secret);
  297. $route = '';
  298. $url = $this->getApiUserUrl() . '/oauth/token';
  299. $headers = ['Authorization' => 'Basic ' . $hash_auth];
  300. $params = ['grant_type' => 'client_credentials'];
  301. $wsResponse = $this->call(
  302. null,
  303. 'GET',
  304. $route,
  305. $url,
  306. $headers,
  307. $params
  308. );
  309. if ($wsResponse->isError()) {
  310. $this->logger->error(
  311. '[ERROR] Get public token : ' . $wsResponse->errorMessage . ' (' . $wsResponse->status . ')'
  312. );
  313. return null;
  314. }
  315. $token = $wsResponse->body;
  316. $session->set(static::PUBLIC_ACCESS_TOKEN, $token);
  317. }
  318. return $token;
  319. }
  320. public function getKasAppToken()
  321. {
  322. $networkId = $this->params->get('network_id');
  323. $url = $this->getApiKasUrl() . '/v1/networks/' . $networkId . '/login';
  324. $body = [
  325. 'clientId' => $this->params->get('kasClientId'),
  326. 'clientSecret' => $this->params->get('kasSecret'),
  327. 'grantType' => 'client_credentials'
  328. ];
  329. $wsResponse = $this->call(
  330. null,
  331. 'POST',
  332. '',
  333. $url,
  334. [],
  335. [],
  336. $body
  337. );
  338. if ($wsResponse->isError()) {
  339. $this->logger->error(
  340. '[ERROR] Get public token : ' . $wsResponse->errorMessage . ' (' . $wsResponse->status . ')'
  341. );
  342. return null;
  343. }
  344. $token = $this->encryptResponseToken($wsResponse->body);
  345. $this->requestStack->getSession()->set(static::PUBLIC_ACCESS_TOKEN, $token);
  346. return $token;
  347. }
  348. private function encryptResponseToken($response)
  349. {
  350. $token = $response->accessToken;
  351. $tokenParts = explode(".", $token);
  352. $tokenPayload = base64_decode($tokenParts[1]);
  353. $jwtPayload = json_decode($tokenPayload);
  354. $response->expirationDate = $jwtPayload->exp;
  355. return $response;
  356. }
  357. /**
  358. * @param String|null $callerType
  359. * @param String $method
  360. * @param String $webServiceRoute
  361. * @param String $webServicePath
  362. * @param array $headers
  363. * @param array $params
  364. * @param array|string $body
  365. * @param array|null $options
  366. *
  367. * @return WsResponse
  368. *
  369. * @throws \Exception
  370. */
  371. public function call(
  372. $callerType,
  373. string $method,
  374. string $webServiceRoute,
  375. string $webServicePath,
  376. array $headers,
  377. array $params,
  378. $body = null,
  379. array $options = null
  380. ): WsResponse {
  381. return call_user_func(
  382. [$this, self::CALL_FUNCTION_NAME[$this->env]],
  383. $callerType,
  384. $method,
  385. $webServiceRoute,
  386. $webServicePath,
  387. $headers,
  388. $params,
  389. $body,
  390. $options
  391. );
  392. }
  393. public function getApiUserUrl()
  394. {
  395. return $this->params->get('api_user_url');
  396. }
  397. /**
  398. * @return mixed|null|String
  399. * @throws \Exception
  400. */
  401. public function getAppAuthToken()
  402. {
  403. $session = $this->requestStack->getSession();
  404. $token = $session->get(static::APP_ACCESS_TOKEN);
  405. if ($token == null) {
  406. $clientId = "instant-core";
  407. $secret = "Ohsaisoh5yai";
  408. $hash_auth = base64_encode($clientId . ":" . $secret);
  409. $route = '';
  410. $url = $this->getApiUserUrl() . '/oauth/token';
  411. $headers = ['Authorization' => 'Basic ' . $hash_auth];
  412. $params = ['grant_type' => 'client_credentials'];
  413. $wsResponse = $this->call(
  414. null,
  415. 'GET',
  416. $route,
  417. $url,
  418. $headers,
  419. $params
  420. );
  421. if ($wsResponse->isError()) {
  422. $this->logger->error(
  423. '[ERROR] Get public token : ' . $wsResponse->errorMessage . ' (' . $wsResponse->status . ')'
  424. );
  425. return null;
  426. }
  427. $token = $wsResponse->body;
  428. $session->set(static::APP_ACCESS_TOKEN, $token);
  429. }
  430. return $token;
  431. }
  432. /**
  433. * @param string $mode
  434. * @param string $url
  435. * @param array $headers
  436. * @param array|string $body
  437. * @return Unirest\Response
  438. */
  439. static function callRest($mode, $url, $headers, $body)
  440. {
  441. // Disables SSL cert validation temporary
  442. Unirest\Request::verifyPeer(false);
  443. switch (\strtoupper($mode)) {
  444. case "GET":
  445. $response = Unirest\Request::get($url, $headers, $body);
  446. break;
  447. case "POST":
  448. $response = Unirest\Request::post($url, $headers, $body);
  449. break;
  450. case "PUT":
  451. $response = Unirest\Request::put($url, $headers, $body);
  452. break;
  453. case "DELETE":
  454. $response = Unirest\Request::delete($url, $headers, $body);
  455. break;
  456. default:
  457. $response = [];
  458. break;
  459. }
  460. return $response;
  461. }
  462. private function parseResponse($responseBody)
  463. {
  464. return $this->updateDateTimeField($responseBody);
  465. }
  466. private function updateDateTimeField(&$responseBody)
  467. {
  468. $fieldsFound = [];
  469. // Search DateTimeIso8601 field on the body response
  470. foreach ($responseBody as $k => &$v) {
  471. if (is_array($v)) {
  472. $v = $this->updateDateTimeField($v);
  473. } else {
  474. if (preg_match('/(.*DateTimeIso8601.*)/i', $k, $matches)) {
  475. $fieldsFound[] = $matches[1];
  476. }
  477. }
  478. }
  479. // Replace values of xxxDateTime by xxxDateTimeIso8601
  480. if (!empty($fieldsFound)) {
  481. foreach ($fieldsFound as $index) {
  482. $responseBody = $this->replaceDateTimeInsensitive($responseBody, $index);
  483. }
  484. }
  485. return $responseBody;
  486. }
  487. private function replaceDateTimeInsensitive($responseBody, $subject)
  488. {
  489. $replaces = [
  490. str_replace('Iso8601', '', $subject), // Original field is xxxDateTime
  491. str_replace('TimeIso8601', '', $subject), // Sometimes original fields have xxxDate instead of xxxDateTime
  492. str_replace('DateTimeIso8601', '', $subject), // Sometimes original field have xxx instead of xxxDateTime
  493. ];
  494. foreach ($replaces as $replaceIndex) {
  495. // Break when found occurence, no need to go further
  496. if (isset($responseBody->{$replaceIndex})) {
  497. // Match case
  498. $responseBody->{$replaceIndex} = $responseBody->{$subject};
  499. break;
  500. } elseif (isset($responseBody->{strtolower($replaceIndex)})) {
  501. // Case insensitive
  502. $responseBody->{strtolower($replaceIndex)} = $responseBody->{$subject};
  503. break;
  504. }
  505. }
  506. return $responseBody;
  507. }
  508. public function getApiCoreUrl()
  509. {
  510. return $this->params->get('api_core_url');
  511. }
  512. public function getApiDataUrl()
  513. {
  514. return $this->params->get('api_data_url');
  515. }
  516. public function getApiNotificationUrl()
  517. {
  518. return $this->params->get('api_notification_url');
  519. }
  520. public function getApiMaasUrl()
  521. {
  522. return $this->params->get('api_maas_url');
  523. }
  524. public function getUserWebUrl()
  525. {
  526. return $this->params->get('userweb_url');
  527. }
  528. public function getApiKasUrl()
  529. {
  530. return $this->params->get('api_kas_url');
  531. }
  532. public function getApiAddressUrl()
  533. {
  534. return $this->params->get('api_address_url');
  535. }
  536. }