src/Service/Internal/CoreApiService.php line 404

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. $cookieValue = $request->cookies->get('applicativeToken');
  197. $applicativeToken = $cookieValue !== null ? json_decode($cookieValue) : null;
  198. $applicativeToken = $applicativeToken ?? $session->get(static::PUBLIC_ACCESS_TOKEN);
  199. if ($userToken) {
  200. $token = $userToken;
  201. $accessToken = $userToken->accessToken ?? $userToken->access_token ?? null;
  202. $rawType = $userToken->tokenType ?? $userToken->token_type ?? '';
  203. $tokenType = (empty($rawType)) ? 'Bearer' : $rawType;
  204. $headers['Authorization'] = $tokenType . ' ' . $accessToken;
  205. } elseif ($applicativeToken) {
  206. $accessToken = $applicativeToken->accessToken ?? $applicativeToken->access_token ?? null;
  207. $rawType = $applicativeToken->tokenType ?? $applicativeToken->token_type ?? '';
  208. $tokenType = (empty($rawType)) ? 'Bearer' : $rawType;
  209. $headers['Authorization'] = $tokenType . ' ' . $accessToken;
  210. }
  211. }
  212. if (empty($token)) {
  213. $this->logger->error('[ERROR] WebService : token not found');
  214. }
  215. }
  216. $query = $webServicePath;
  217. $body = (empty($body) ? null : ($rawBody ? $body : json_encode($body)));
  218. if ($debug) {
  219. $this->logger->info("WS $method $query with args : " . print_r($params), []);
  220. }
  221. if (!empty($params)) {
  222. $params = $this->queryParametersNormalizer->normalize($params);
  223. $query .= "?" . http_build_query($params, '', '&', PHP_QUERY_RFC3986);
  224. }
  225. $error_message = "";
  226. $httpStatusCode = "";
  227. $responseBody = "";
  228. try {
  229. $wsResponse = $this->callRest($method, $query, $headers, $body);
  230. if ($wsResponse->code == 503 &&
  231. (isset($wsResponse->body->message) && ($wsResponse->body->message == 'TICKETING_NOT_ACTIVE' ||
  232. $wsResponse->body->message == 'FINE_NOT_ACTIVE'))) {
  233. throw new RedirectException(
  234. new RedirectResponse(
  235. $this->router->generate(
  236. "message_info",
  237. array('message' => $wsResponse->body->message)
  238. )
  239. )
  240. );
  241. }
  242. $responseBody = null;
  243. $httpStatusCode = $wsResponse->code;
  244. if (substr($httpStatusCode, 0, 1) != "2") { // status != 2xx
  245. $responseBody = $wsResponse->body;
  246. $this->logger->error("WS $method $webServicePath (" . $httpStatusCode . ")");
  247. } else {
  248. $responseBody = $wsResponse->body;
  249. }
  250. if ($debug) {
  251. $this->logger->info("WS $method $query returns ($httpStatusCode) " . print_r($responseBody, true));
  252. }
  253. } catch (Unirest\Exception $exc) {
  254. $error_message = $exc;
  255. $this->logger->error("WS $method $webServicePath : error : " . $error_message);
  256. }
  257. if (!empty($responseBody) && (is_array($responseBody) || is_object($responseBody))) {
  258. $responseBody = $this->parseResponse($responseBody);
  259. }
  260. return new WsResponse($responseBody, $httpStatusCode, $error_message);
  261. }
  262. /**
  263. * @return mixed|null|String
  264. * @throws \Exception
  265. */
  266. public function getUserAuthToken()
  267. {
  268. $result = $this->requestStack->getSession()->get(static::USER_ACCESS_TOKEN);
  269. if (!$result) {
  270. $token = static::getPublicAuthToken();
  271. return $token;
  272. }
  273. return $result;
  274. }
  275. /**
  276. * @return mixed|null|String
  277. * @throws \Exception
  278. */
  279. public function getPublicAuthToken()
  280. {
  281. $session = $this->requestStack->getSession();
  282. $token = $session->get(static::PUBLIC_ACCESS_TOKEN);
  283. if ($this->params->get('global.login_mode') == KasUserAuthenticator::LOGIN_MODE) {
  284. if ($token) {
  285. $now = time();
  286. $diff = $token->expirationDate - $now;
  287. if ($diff > 30 && $diff < 90) {
  288. $token = $this->getKasAppToken();
  289. } elseif ($diff < 0) {
  290. $session->invalidate();
  291. $token = $this->getKasAppToken();
  292. }
  293. } else {
  294. $token = $this->getKasAppToken();
  295. }
  296. } elseif (!$token) {
  297. $clientId = $this->params->get('ws_client_id');
  298. $secret = $this->params->get('ws_secret');
  299. $hash_auth = base64_encode($clientId . ":" . $secret);
  300. $route = '';
  301. $url = $this->getApiUserUrl() . '/oauth/token';
  302. $headers = ['Authorization' => 'Basic ' . $hash_auth];
  303. $params = ['grant_type' => 'client_credentials'];
  304. $wsResponse = $this->call(
  305. null,
  306. 'GET',
  307. $route,
  308. $url,
  309. $headers,
  310. $params
  311. );
  312. if ($wsResponse->isError()) {
  313. $this->logger->error(
  314. '[ERROR] Get public token : ' . $wsResponse->errorMessage . ' (' . $wsResponse->status . ')'
  315. );
  316. return null;
  317. }
  318. $token = $wsResponse->body;
  319. $session->set(static::PUBLIC_ACCESS_TOKEN, $token);
  320. }
  321. return $token;
  322. }
  323. public function getKasAppToken()
  324. {
  325. $networkId = $this->params->get('network_id');
  326. $url = $this->getApiKasUrl() . '/v1/networks/' . $networkId . '/login';
  327. $body = [
  328. 'clientId' => $this->params->get('kasClientId'),
  329. 'clientSecret' => $this->params->get('kasSecret'),
  330. 'grantType' => 'client_credentials'
  331. ];
  332. $wsResponse = $this->call(
  333. null,
  334. 'POST',
  335. '',
  336. $url,
  337. [],
  338. [],
  339. $body
  340. );
  341. if ($wsResponse->isError()) {
  342. $this->logger->error(
  343. '[ERROR] Get public token : ' . $wsResponse->errorMessage . ' (' . $wsResponse->status . ')'
  344. );
  345. return null;
  346. }
  347. $token = $this->encryptResponseToken($wsResponse->body);
  348. $this->requestStack->getSession()->set(static::PUBLIC_ACCESS_TOKEN, $token);
  349. return $token;
  350. }
  351. private function encryptResponseToken($response)
  352. {
  353. $token = $response->accessToken ?? $response->access_token ?? null;
  354. if (!$token) {
  355. return $response;
  356. }
  357. $tokenParts = explode(".", $token);
  358. $tokenPayload = base64_decode($tokenParts[1]);
  359. $jwtPayload = json_decode($tokenPayload);
  360. $response->expirationDate = $jwtPayload->exp ?? null;
  361. return $response;
  362. }
  363. /**
  364. * @param String|null $callerType
  365. * @param String $method
  366. * @param String $webServiceRoute
  367. * @param String $webServicePath
  368. * @param array $headers
  369. * @param array $params
  370. * @param array|string $body
  371. * @param array|null $options
  372. *
  373. * @return WsResponse
  374. *
  375. * @throws \Exception
  376. */
  377. public function call(
  378. $callerType,
  379. string $method,
  380. string $webServiceRoute,
  381. string $webServicePath,
  382. array $headers,
  383. array $params,
  384. $body = null,
  385. array $options = null
  386. ): WsResponse {
  387. return call_user_func(
  388. [$this, self::CALL_FUNCTION_NAME[$this->env]],
  389. $callerType,
  390. $method,
  391. $webServiceRoute,
  392. $webServicePath,
  393. $headers,
  394. $params,
  395. $body,
  396. $options
  397. );
  398. }
  399. public function getApiUserUrl()
  400. {
  401. return $this->params->get('api_user_url');
  402. }
  403. /**
  404. * @return mixed|null|String
  405. * @throws \Exception
  406. */
  407. public function getAppAuthToken()
  408. {
  409. $session = $this->requestStack->getSession();
  410. $token = $session->get(static::APP_ACCESS_TOKEN);
  411. if ($token == null) {
  412. $clientId = "instant-core";
  413. $secret = "Ohsaisoh5yai";
  414. $hash_auth = base64_encode($clientId . ":" . $secret);
  415. $route = '';
  416. $url = $this->getApiUserUrl() . '/oauth/token';
  417. $headers = ['Authorization' => 'Basic ' . $hash_auth];
  418. $params = ['grant_type' => 'client_credentials'];
  419. $wsResponse = $this->call(
  420. null,
  421. 'GET',
  422. $route,
  423. $url,
  424. $headers,
  425. $params
  426. );
  427. if ($wsResponse->isError()) {
  428. $this->logger->error(
  429. '[ERROR] Get public token : ' . $wsResponse->errorMessage . ' (' . $wsResponse->status . ')'
  430. );
  431. return null;
  432. }
  433. $token = $wsResponse->body;
  434. $session->set(static::APP_ACCESS_TOKEN, $token);
  435. }
  436. return $token;
  437. }
  438. /**
  439. * @param string $mode
  440. * @param string $url
  441. * @param array $headers
  442. * @param array|string $body
  443. * @return Unirest\Response
  444. */
  445. static function callRest($mode, $url, $headers, $body)
  446. {
  447. // Disables SSL cert validation temporary
  448. Unirest\Request::verifyPeer(false);
  449. switch (\strtoupper($mode)) {
  450. case "GET":
  451. $response = Unirest\Request::get($url, $headers, $body);
  452. break;
  453. case "POST":
  454. $response = Unirest\Request::post($url, $headers, $body);
  455. break;
  456. case "PUT":
  457. $response = Unirest\Request::put($url, $headers, $body);
  458. break;
  459. case "DELETE":
  460. $response = Unirest\Request::delete($url, $headers, $body);
  461. break;
  462. default:
  463. $response = [];
  464. break;
  465. }
  466. return $response;
  467. }
  468. private function parseResponse($responseBody)
  469. {
  470. return $this->updateDateTimeField($responseBody);
  471. }
  472. private function updateDateTimeField(&$responseBody)
  473. {
  474. $fieldsFound = [];
  475. // Search DateTimeIso8601 field on the body response
  476. foreach ($responseBody as $k => &$v) {
  477. if (is_array($v)) {
  478. $v = $this->updateDateTimeField($v);
  479. } else {
  480. if (preg_match('/(.*DateTimeIso8601.*)/i', $k, $matches)) {
  481. $fieldsFound[] = $matches[1];
  482. }
  483. }
  484. }
  485. // Replace values of xxxDateTime by xxxDateTimeIso8601
  486. if (!empty($fieldsFound)) {
  487. foreach ($fieldsFound as $index) {
  488. $responseBody = $this->replaceDateTimeInsensitive($responseBody, $index);
  489. }
  490. }
  491. return $responseBody;
  492. }
  493. private function replaceDateTimeInsensitive($responseBody, $subject)
  494. {
  495. $replaces = [
  496. str_replace('Iso8601', '', $subject), // Original field is xxxDateTime
  497. str_replace('TimeIso8601', '', $subject), // Sometimes original fields have xxxDate instead of xxxDateTime
  498. str_replace('DateTimeIso8601', '', $subject), // Sometimes original field have xxx instead of xxxDateTime
  499. ];
  500. foreach ($replaces as $replaceIndex) {
  501. // Break when found occurence, no need to go further
  502. if (isset($responseBody->{$replaceIndex})) {
  503. // Match case
  504. $responseBody->{$replaceIndex} = $responseBody->{$subject};
  505. break;
  506. } elseif (isset($responseBody->{strtolower($replaceIndex)})) {
  507. // Case insensitive
  508. $responseBody->{strtolower($replaceIndex)} = $responseBody->{$subject};
  509. break;
  510. }
  511. }
  512. return $responseBody;
  513. }
  514. public function getApiCoreUrl()
  515. {
  516. return $this->params->get('api_core_url');
  517. }
  518. public function getApiDataUrl()
  519. {
  520. return $this->params->get('api_data_url');
  521. }
  522. public function getApiNotificationUrl()
  523. {
  524. return $this->params->get('api_notification_url');
  525. }
  526. public function getApiMaasUrl()
  527. {
  528. return $this->params->get('api_maas_url');
  529. }
  530. public function getUserWebUrl()
  531. {
  532. return $this->params->get('userweb_url');
  533. }
  534. public function getApiKasUrl()
  535. {
  536. return $this->params->get('api_kas_url');
  537. }
  538. public function getApiAddressUrl()
  539. {
  540. return $this->params->get('api_address_url');
  541. }
  542. }