src\Service\DataPrepClientService.php line 182

Open in your IDE?
  1. <?php
  2. namespace App\Service;
  3. use Articque\Cd7IHMBundle\Entity\CDOnline\User;
  4. use Articque\Cd7IHMBundle\Service\PermissionManager;
  5. use Firebase\JWT\JWT;
  6. class DataPrepClientService {
  7. const JWT_TTL = 10 * 60; // durée de vie d'un jeton JWT en secondes (= 10 minutes)
  8. const METHOD_GET = "GET";
  9. const METHOD_POST = "POST";
  10. const METHOD_PUT = "PUT";
  11. const METHOD_DELETE = "DELETE";
  12. const IFEXIST_REPLACE = "replace";
  13. const IFEXIST_RENAME = "rename";
  14. const IFEXIST_FAIL = "fail";
  15. public bool $debug = false; // affiche des infos de debug pour chaque appel au WS
  16. public string $locale = "fr-FR"; // fr-FR ou en-US
  17. protected PermissionManager $permissionManager;
  18. protected DomainService $domainService;
  19. protected string $secret;
  20. protected string $baseUrl;
  21. protected int $userId = 0; // jwt / userId en cache si plusieurs appels successifs pour le même utilisateur
  22. protected string $jwt = "";
  23. /**
  24. * @param PermissionManager $permissionManager
  25. * @param DomainService $domainService
  26. * @param string $secret
  27. * @param $dataprepWebService
  28. */
  29. public function __construct(PermissionManager $permissionManager, DomainService $domainService, $secret, $dataprepWebService) {
  30. $this->permissionManager = $permissionManager;
  31. $this->domainService = $domainService;
  32. $this->secret = $secret;
  33. $this->baseUrl = $dataprepWebService["local_url"] ?? "";
  34. }
  35. /**
  36. * Génération d'un token JWT pour l'identification auprès du WS Dataprep
  37. * @param User $user l'utilisateur pour lequel créer un token
  38. * @return string le token JWT
  39. */
  40. public function generateJWT(User $user): string {
  41. $payload = [];
  42. $url = $this->domainService->getRemoteApplicationDomain();
  43. $now = time();
  44. $payload = [
  45. "iss" => $url, // issuer : domaine qui a généré le token
  46. "aud" => $url, // audience : domaine depuis lequel est utilisé le token
  47. "iat" => $now, // issued at : timestamp de l'heure de génération du token
  48. "exp" => $now + self::JWT_TTL, // expiration time : timestamp d'expiration du token
  49. "roles" => [$user->getRoleToDisplayCDO()],
  50. "userId" => $user->getId(), // identifiant utilisateur
  51. "groups" => $this->permissionManager->getPermissionForJWT($user),
  52. ];
  53. return JWT::encode($payload, $this->secret, 'HS256');
  54. }
  55. /**
  56. * appel générique au webservice
  57. * @param User $user l'utilisateur qui fait l'appel
  58. * @param string $method la méthode HTTP (GET, POST, PUT, DELETE). Utiliser les constantes self::METHOD_*
  59. * @param string $url url de la fonction du webservice à appeler
  60. * @param array $data les données à joindre à l'appel (POST ou GET selon la méthode)
  61. * @param int $attempt comptage du nombre d'essai (permet de retenter un appel en cas de péremption du token JWT)
  62. * @return array un tableau contenant les clés suivantes :
  63. * * status : [int] le status HTTP de la réponse (200, 404, etc.)
  64. * * message : [string] le message d'erreur si le status est différent de 200
  65. * * body : [object] le corps de la réponse (json décodé sous forme d'objet php)
  66. */
  67. private function call(User $user, string $method, string $url, array $data = [], int $attempt = 0): array {
  68. if ($this->userId != $user->getId() || empty($this->jwt)) {
  69. $this->userId = $user->getId();
  70. $this->jwt = $this->generateJWT($user);
  71. if ($this->debug) {
  72. echo "generate JWT for user {$this->userId}\n";
  73. }
  74. }
  75. $headers = [
  76. "Accept: application/json, text/plain",
  77. "Content-Type: application/json;charset=UTF-8",
  78. "Authorization: Bearer {$this->jwt}",
  79. ];
  80. $query = ["culture" => $this->locale];
  81. if ($method == self::METHOD_GET) {
  82. $query = array_merge($query, $data);
  83. $dataString = "";
  84. } else {
  85. $dataString = json_encode($data);
  86. }
  87. $queryString = http_build_query($query);
  88. $absUrl = "{$this->baseUrl}{$url}?{$queryString}";
  89. $ch = curl_init($absUrl);
  90. curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
  91. if ($method == self::METHOD_POST || $method == self::METHOD_PUT) {
  92. curl_setopt($ch, CURLOPT_POSTFIELDS, $dataString);
  93. $headers[] = "Content-Length: " . strlen($dataString);
  94. }
  95. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  96. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
  97. curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
  98. curl_setopt($ch, CURLOPT_HEADER, 1);
  99. $response = curl_exec($ch);
  100. $header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
  101. $headers = substr($response, 0, $header_size);
  102. $body = substr($response, $header_size);
  103. $status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
  104. $message = ($status == 200 || $status == 202) ? "" : substr(substr(strstr(strstr($headers, "\n", true), $status), 4), 0, -1);
  105. curl_close($ch);
  106. if ($this->debug) {
  107. echo "$method {$absUrl}" .
  108. (empty($dataString) ? "" : "\n -> " . var_export($dataString, true)) .
  109. "\n <- $status $message " . var_export($body, true) . "\n\n";
  110. }
  111. if ($status == 202) {
  112. preg_match('/^Location: (.*)[\r\n]$/m', $headers, $locationMatches); // extraction du header Location
  113. if (empty($locationMatches[1])) {
  114. $bodyJson = json_decode($body);
  115. $location = "/tasks/poll/{$bodyJson->taskId}";
  116. } else {
  117. $location = $locationMatches[1];
  118. }
  119. return $this->call($user, self::METHOD_GET, $location, []);
  120. }
  121. if ($status == 401) { // retour Unauthorized du WebService => le jeton JWT est probablement expiré
  122. if ($attempt == 0) { // premier essai => on invalide le jeton et on retente. Sinon, on abandonne
  123. $this->jwt = ""; // supprime le jeton. Un nouveau sera demandé dans l'appel récursif
  124. return $this->call($user, $method, $url, $data, $attempt + 1);
  125. }
  126. }
  127. return [
  128. "status" => $status,
  129. "message" => $message,
  130. "body" => json_decode($body),
  131. ];
  132. }
  133. /**
  134. * Obtention du status du webservice
  135. * @return string
  136. */
  137. public function getWsStatus()
  138. {
  139. $ch = curl_init($this->baseUrl . '/admin/status');
  140. curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
  141. curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
  142. $result = curl_exec($ch);
  143. curl_close($ch);
  144. return $result;
  145. }
  146. /**
  147. * Retourne la liste des datacompositions d'un groupe
  148. * @param User $user l'utilisateur qui fait l'appel
  149. * @param int $groupId Identifiant du groupe
  150. * @return array voir la méthode `call` pour le détail du tableau retourné
  151. */
  152. public function getSchemaList(User $user, int $groupId): array {
  153. return $this->call($user, self::METHOD_GET, "/datacompositions", ["groupId" => $groupId]);
  154. }
  155. /**
  156. * Créé une nouvelle datacomposition
  157. * @param User $user l'utilisateur qui fait l'appel
  158. * @param string $name le nom de nouvelle datacomposition
  159. * @param int $groupId Identifiant du groupe
  160. * @param int $templateId Identifiant de la datacomposition à copier dans le cas d'une copie. Si `null`, création d'une nouvelle datacomposition vide
  161. * @param string $ifExistAction Action à effectuer si une datacomposition du même nom existe déjà dans le groupe.
  162. * Valeurs possibles : DataPrepClientService::IFEXIST_REPLACE, DataPrepClientService::IFEXIST_RENAME ou DataPrepClientService::IFEXIST_FAIL
  163. * @return array voir la méthode `call` pour le détail du tableau retourné
  164. */
  165. public function create(User $user, string $name, int $groupId, int $templateId = null, string $ifExistAction = null) {
  166. $postData = ["groupId" => $groupId, "name" => $name];
  167. if (! empty($templateId)) $postData["templateId"] = $templateId;
  168. if (! empty($ifExistAction)) $postData["ifExists"] = $ifExistAction;
  169. return $this->call($user, self::METHOD_POST, "/datacompositions", $postData);
  170. }
  171. /**
  172. * Chargement d'une séquence d'opération complète. La datacomposition doit être vide
  173. * @param User $user l'utilisateur qui fait l'appel
  174. * @param int $workflowId Identifiant de la datacomposition
  175. * @param array $transformations liste des transformations (même formalisme que le résultat de getWorkflow)
  176. * @return array voir la méthode `call` pour le détail du tableau retourné
  177. */
  178. public function setWorkflow(User $user, int $workflowId, array $transformations) {
  179. return $this->call($user, self::METHOD_POST, "/workflows/{$workflowId}", $transformations);
  180. }
  181. /**
  182. * Chargement d'une séquence d'opération complète. La datacomposition doit être vide
  183. * @param User $user l'utilisateur qui fait l'appel
  184. * @param int $id Identifiant de la datacomposition
  185. * @param array $dumpInfos { dataPath: string } chemin relatif à files/app_data/tmp/ vers le dossier où créer les dumps
  186. * @return array voir la méthode `call` pour le détail du tableau retourné
  187. */
  188. public function dumpTablesToFiles(User $user, int $id, array $dumpInfos) {
  189. return $this->call($user, self::METHOD_POST, "/datacompositions/{$id}/tables/dump-to-files", $dumpInfos);
  190. }
  191. /**
  192. * Chargement d'une séquence d'opération complète. La datacomposition doit être vide
  193. * @param User $user l'utilisateur qui fait l'appel
  194. * @param int $id Identifiant de la datacomposition
  195. * @param array $dumpInfos { tables: object, dataPath: string }
  196. * * tables : liste des tables (même formalisme que le résultat de getTables)
  197. * * chemin relatif à files/app_data/tmp/ vers le dossier où sont présents les dumps
  198. * @return array voir la méthode `call` pour le détail du tableau retourné
  199. */
  200. public function loadTablesFromFiles(User $user, int $id, array $dumpInfos) {
  201. return $this->call($user, self::METHOD_POST, "/datacompositions/{$id}/tables/load-from-files", $dumpInfos);
  202. }
  203. /**
  204. * Supprimer une datacomposition
  205. * @param User $user l'utilisateur qui fait l'appel
  206. * @param int $id Identifiant de la datacomposition à supprimer
  207. * @return array voir la méthode `call` pour le détail du tableau retourné
  208. */
  209. public function delete(User $user, int $id): array {
  210. return $this->call($user, self::METHOD_DELETE, "/datacompositions/{$id}");
  211. }
  212. /**
  213. * Exécuter une séquence
  214. * @param User $user l'utilisateur qui fait l'appel
  215. * @param int $workflowId Identifiant de la séquence
  216. * @return array voir la méthode `call` pour le détail du tableau retourné
  217. */
  218. public function executeWorkflow(User $user, int $workflowId): array {
  219. return $this->call($user, self::METHOD_GET, "/workflows/{$workflowId}/execute");
  220. }
  221. /**
  222. * Retourne la liste des tables d'une datacomposition
  223. * @param User $user l'utilisateur qui fait l'appel
  224. * @param int $id Identifiant de la datacomposition
  225. * @return array un tableau de json contenant les transformations :
  226. * * status : [int] le status HTTP de la réponse (200, 404, etc.)
  227. * * message : [string] le message d'erreur si le status est différent de 200
  228. * * body : [object] le corps de la réponse (json décodé sous forme d'objet php)
  229. */
  230. public function getTables(User $user, int $id): array {
  231. return $this->call($user, self::METHOD_GET, "/datacompositions/{$id}/tables");
  232. }
  233. /**
  234. * Retourne la table de cache géoloc d'une datacomposition
  235. * @param User $user l'utilisateur qui fait l'appel
  236. * @param int $id Identifiant de la datacomposition
  237. * @return array un tableau de json contenant les transformations :
  238. * * status : [int] le status HTTP de la réponse (200, 404, etc.)
  239. * * message : [string] le message d'erreur si le status est différent de 200
  240. * * body : [object] le corps de la réponse (json décodé sous forme d'objet php)
  241. */
  242. public function getCacheGeolocJson(User $user, int $id): array {
  243. return $this->call($user, self::METHOD_GET, "/datacompositions/{$id}/cache-geoloc-json");
  244. }
  245. /**
  246. * Retourne le workflow d'une datacomposition
  247. * @param User $user l'utilisateur qui fait l'appel
  248. * @param int $id Identifiant de la datacomposition
  249. * @return array un tableau contenant les clés suivantes :
  250. * * status : [int] le status HTTP de la réponse (200, 404, etc.)
  251. * * message : [string] le message d'erreur si le status est différent de 200
  252. * * body : [object] le corps de la réponse (json décodé sous forme d'objet php)
  253. */
  254. public function getWorkflow(User $user, int $id): array {
  255. return $this->call($user, self::METHOD_GET, "/workflows/{$id}");
  256. }
  257. /**
  258. * Modifier une datacomposition
  259. * @param User $user l'utilisateur qui fait l'appel
  260. * @param int $id Identifiant de la datacomposition à modifier
  261. * @param array $data Body de la requête correspond à l'entrée update de l'API -> datacompositions
  262. * @return array un tableau contenant les clés suivantes :
  263. * * status : [int] le status HTTP de la réponse (200, 404, etc.)
  264. * * message : [string] le message d'erreur si le status est différent de 200
  265. * * body : [object] le corps de la réponse (json décodé sous forme d'objet php)
  266. */
  267. public function update(User $user, int $id, array $data): array {
  268. return $this->call($user, self::METHOD_PUT, "/datacompositions/{$id}", $data);
  269. }
  270. }
  271. /**
  272. * Description of ArticqueException
  273. *
  274. * @author lmagniez
  275. */
  276. class DataPrepClientException extends \Exception{
  277. /**
  278. * @param string $message
  279. * @param int $code
  280. * @param \Throwable $previous
  281. */
  282. public function __construct(string $message = "", int $code = 0, \Throwable $previous = null)
  283. {
  284. parent::__construct($message, $code, $previous);
  285. }
  286. }
  287. ?>