<?php
namespace App\Service;
use Articque\Cd7IHMBundle\Entity\CDOnline\User;
use Articque\Cd7IHMBundle\Service\PermissionManager;
use Firebase\JWT\JWT;
class DataPrepClientService {
const JWT_TTL = 10 * 60; // durée de vie d'un jeton JWT en secondes (= 10 minutes)
const METHOD_GET = "GET";
const METHOD_POST = "POST";
const METHOD_PUT = "PUT";
const METHOD_DELETE = "DELETE";
const IFEXIST_REPLACE = "replace";
const IFEXIST_RENAME = "rename";
const IFEXIST_FAIL = "fail";
public bool $debug = false; // affiche des infos de debug pour chaque appel au WS
public string $locale = "fr-FR"; // fr-FR ou en-US
protected PermissionManager $permissionManager;
protected DomainService $domainService;
protected string $secret;
protected string $baseUrl;
protected int $userId = 0; // jwt / userId en cache si plusieurs appels successifs pour le même utilisateur
protected string $jwt = "";
/**
* @param PermissionManager $permissionManager
* @param DomainService $domainService
* @param string $secret
* @param $dataprepWebService
*/
public function __construct(PermissionManager $permissionManager, DomainService $domainService, $secret, $dataprepWebService) {
$this->permissionManager = $permissionManager;
$this->domainService = $domainService;
$this->secret = $secret;
$this->baseUrl = $dataprepWebService["local_url"] ?? "";
}
/**
* Génération d'un token JWT pour l'identification auprès du WS Dataprep
* @param User $user l'utilisateur pour lequel créer un token
* @return string le token JWT
*/
public function generateJWT(User $user): string {
$payload = [];
$url = $this->domainService->getRemoteApplicationDomain();
$now = time();
$payload = [
"iss" => $url, // issuer : domaine qui a généré le token
"aud" => $url, // audience : domaine depuis lequel est utilisé le token
"iat" => $now, // issued at : timestamp de l'heure de génération du token
"exp" => $now + self::JWT_TTL, // expiration time : timestamp d'expiration du token
"roles" => [$user->getRoleToDisplayCDO()],
"userId" => $user->getId(), // identifiant utilisateur
"groups" => $this->permissionManager->getPermissionForJWT($user),
];
return JWT::encode($payload, $this->secret, 'HS256');
}
/**
* appel générique au webservice
* @param User $user l'utilisateur qui fait l'appel
* @param string $method la méthode HTTP (GET, POST, PUT, DELETE). Utiliser les constantes self::METHOD_*
* @param string $url url de la fonction du webservice à appeler
* @param array $data les données à joindre à l'appel (POST ou GET selon la méthode)
* @param int $attempt comptage du nombre d'essai (permet de retenter un appel en cas de péremption du token JWT)
* @return array un tableau contenant les clés suivantes :
* * status : [int] le status HTTP de la réponse (200, 404, etc.)
* * message : [string] le message d'erreur si le status est différent de 200
* * body : [object] le corps de la réponse (json décodé sous forme d'objet php)
*/
private function call(User $user, string $method, string $url, array $data = [], int $attempt = 0): array {
if ($this->userId != $user->getId() || empty($this->jwt)) {
$this->userId = $user->getId();
$this->jwt = $this->generateJWT($user);
if ($this->debug) {
echo "generate JWT for user {$this->userId}\n";
}
}
$headers = [
"Accept: application/json, text/plain",
"Content-Type: application/json;charset=UTF-8",
"Authorization: Bearer {$this->jwt}",
];
$query = ["culture" => $this->locale];
if ($method == self::METHOD_GET) {
$query = array_merge($query, $data);
$dataString = "";
} else {
$dataString = json_encode($data);
}
$queryString = http_build_query($query);
$absUrl = "{$this->baseUrl}{$url}?{$queryString}";
$ch = curl_init($absUrl);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
if ($method == self::METHOD_POST || $method == self::METHOD_PUT) {
curl_setopt($ch, CURLOPT_POSTFIELDS, $dataString);
$headers[] = "Content-Length: " . strlen($dataString);
}
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
curl_setopt($ch, CURLOPT_HEADER, 1);
$response = curl_exec($ch);
$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
$headers = substr($response, 0, $header_size);
$body = substr($response, $header_size);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$message = ($status == 200 || $status == 202) ? "" : substr(substr(strstr(strstr($headers, "\n", true), $status), 4), 0, -1);
curl_close($ch);
if ($this->debug) {
echo "$method {$absUrl}" .
(empty($dataString) ? "" : "\n -> " . var_export($dataString, true)) .
"\n <- $status $message " . var_export($body, true) . "\n\n";
}
if ($status == 202) {
preg_match('/^Location: (.*)[\r\n]$/m', $headers, $locationMatches); // extraction du header Location
if (empty($locationMatches[1])) {
$bodyJson = json_decode($body);
$location = "/tasks/poll/{$bodyJson->taskId}";
} else {
$location = $locationMatches[1];
}
return $this->call($user, self::METHOD_GET, $location, []);
}
if ($status == 401) { // retour Unauthorized du WebService => le jeton JWT est probablement expiré
if ($attempt == 0) { // premier essai => on invalide le jeton et on retente. Sinon, on abandonne
$this->jwt = ""; // supprime le jeton. Un nouveau sera demandé dans l'appel récursif
return $this->call($user, $method, $url, $data, $attempt + 1);
}
}
return [
"status" => $status,
"message" => $message,
"body" => json_decode($body),
];
}
/**
* Obtention du status du webservice
* @return string
*/
public function getWsStatus()
{
$ch = curl_init($this->baseUrl . '/admin/status');
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);
$result = curl_exec($ch);
curl_close($ch);
return $result;
}
/**
* Retourne la liste des datacompositions d'un groupe
* @param User $user l'utilisateur qui fait l'appel
* @param int $groupId Identifiant du groupe
* @return array voir la méthode `call` pour le détail du tableau retourné
*/
public function getSchemaList(User $user, int $groupId): array {
return $this->call($user, self::METHOD_GET, "/datacompositions", ["groupId" => $groupId]);
}
/**
* Créé une nouvelle datacomposition
* @param User $user l'utilisateur qui fait l'appel
* @param string $name le nom de nouvelle datacomposition
* @param int $groupId Identifiant du groupe
* @param int $templateId Identifiant de la datacomposition à copier dans le cas d'une copie. Si `null`, création d'une nouvelle datacomposition vide
* @param string $ifExistAction Action à effectuer si une datacomposition du même nom existe déjà dans le groupe.
* Valeurs possibles : DataPrepClientService::IFEXIST_REPLACE, DataPrepClientService::IFEXIST_RENAME ou DataPrepClientService::IFEXIST_FAIL
* @return array voir la méthode `call` pour le détail du tableau retourné
*/
public function create(User $user, string $name, int $groupId, int $templateId = null, string $ifExistAction = null) {
$postData = ["groupId" => $groupId, "name" => $name];
if (! empty($templateId)) $postData["templateId"] = $templateId;
if (! empty($ifExistAction)) $postData["ifExists"] = $ifExistAction;
return $this->call($user, self::METHOD_POST, "/datacompositions", $postData);
}
/**
* Chargement d'une séquence d'opération complète. La datacomposition doit être vide
* @param User $user l'utilisateur qui fait l'appel
* @param int $workflowId Identifiant de la datacomposition
* @param array $transformations liste des transformations (même formalisme que le résultat de getWorkflow)
* @return array voir la méthode `call` pour le détail du tableau retourné
*/
public function setWorkflow(User $user, int $workflowId, array $transformations) {
return $this->call($user, self::METHOD_POST, "/workflows/{$workflowId}", $transformations);
}
/**
* Chargement d'une séquence d'opération complète. La datacomposition doit être vide
* @param User $user l'utilisateur qui fait l'appel
* @param int $id Identifiant de la datacomposition
* @param array $dumpInfos { dataPath: string } chemin relatif à files/app_data/tmp/ vers le dossier où créer les dumps
* @return array voir la méthode `call` pour le détail du tableau retourné
*/
public function dumpTablesToFiles(User $user, int $id, array $dumpInfos) {
return $this->call($user, self::METHOD_POST, "/datacompositions/{$id}/tables/dump-to-files", $dumpInfos);
}
/**
* Chargement d'une séquence d'opération complète. La datacomposition doit être vide
* @param User $user l'utilisateur qui fait l'appel
* @param int $id Identifiant de la datacomposition
* @param array $dumpInfos { tables: object, dataPath: string }
* * tables : liste des tables (même formalisme que le résultat de getTables)
* * chemin relatif à files/app_data/tmp/ vers le dossier où sont présents les dumps
* @return array voir la méthode `call` pour le détail du tableau retourné
*/
public function loadTablesFromFiles(User $user, int $id, array $dumpInfos) {
return $this->call($user, self::METHOD_POST, "/datacompositions/{$id}/tables/load-from-files", $dumpInfos);
}
/**
* Supprimer une datacomposition
* @param User $user l'utilisateur qui fait l'appel
* @param int $id Identifiant de la datacomposition à supprimer
* @return array voir la méthode `call` pour le détail du tableau retourné
*/
public function delete(User $user, int $id): array {
return $this->call($user, self::METHOD_DELETE, "/datacompositions/{$id}");
}
/**
* Exécuter une séquence
* @param User $user l'utilisateur qui fait l'appel
* @param int $workflowId Identifiant de la séquence
* @return array voir la méthode `call` pour le détail du tableau retourné
*/
public function executeWorkflow(User $user, int $workflowId): array {
return $this->call($user, self::METHOD_GET, "/workflows/{$workflowId}/execute");
}
/**
* Retourne la liste des tables d'une datacomposition
* @param User $user l'utilisateur qui fait l'appel
* @param int $id Identifiant de la datacomposition
* @return array un tableau de json contenant les transformations :
* * status : [int] le status HTTP de la réponse (200, 404, etc.)
* * message : [string] le message d'erreur si le status est différent de 200
* * body : [object] le corps de la réponse (json décodé sous forme d'objet php)
*/
public function getTables(User $user, int $id): array {
return $this->call($user, self::METHOD_GET, "/datacompositions/{$id}/tables");
}
/**
* Retourne la table de cache géoloc d'une datacomposition
* @param User $user l'utilisateur qui fait l'appel
* @param int $id Identifiant de la datacomposition
* @return array un tableau de json contenant les transformations :
* * status : [int] le status HTTP de la réponse (200, 404, etc.)
* * message : [string] le message d'erreur si le status est différent de 200
* * body : [object] le corps de la réponse (json décodé sous forme d'objet php)
*/
public function getCacheGeolocJson(User $user, int $id): array {
return $this->call($user, self::METHOD_GET, "/datacompositions/{$id}/cache-geoloc-json");
}
/**
* Retourne le workflow d'une datacomposition
* @param User $user l'utilisateur qui fait l'appel
* @param int $id Identifiant de la datacomposition
* @return array un tableau contenant les clés suivantes :
* * status : [int] le status HTTP de la réponse (200, 404, etc.)
* * message : [string] le message d'erreur si le status est différent de 200
* * body : [object] le corps de la réponse (json décodé sous forme d'objet php)
*/
public function getWorkflow(User $user, int $id): array {
return $this->call($user, self::METHOD_GET, "/workflows/{$id}");
}
/**
* Modifier une datacomposition
* @param User $user l'utilisateur qui fait l'appel
* @param int $id Identifiant de la datacomposition à modifier
* @param array $data Body de la requête correspond à l'entrée update de l'API -> datacompositions
* @return array un tableau contenant les clés suivantes :
* * status : [int] le status HTTP de la réponse (200, 404, etc.)
* * message : [string] le message d'erreur si le status est différent de 200
* * body : [object] le corps de la réponse (json décodé sous forme d'objet php)
*/
public function update(User $user, int $id, array $data): array {
return $this->call($user, self::METHOD_PUT, "/datacompositions/{$id}", $data);
}
}
/**
* Description of ArticqueException
*
* @author lmagniez
*/
class DataPrepClientException extends \Exception{
/**
* @param string $message
* @param int $code
* @param \Throwable $previous
*/
public function __construct(string $message = "", int $code = 0, \Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
}
}
?>