src/Service/Internal/CoreApiService.php line 134

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